basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import * as fs from 'node:fs/promises';8import type { Config } from '../config/config.js';9import { atomicWriteFile } from '../utils/atomicFileWrite.js';10import { getAutoMemoryMetadataPath } from './paths.js';11import { planManagedAutoMemoryDreamByAgent } from './dreamAgentPlanner.js';12import { rebuildManagedAutoMemoryIndex } from './indexer.js';13import { ensureAutoMemoryScaffold } from './store.js';14import {15 AUTO_MEMORY_TYPES,16 type AutoMemoryMetadata,17 type AutoMemoryType,18} from './types.js';19import { logMemoryDream, MemoryDreamEvent } from '../telemetry/index.js';20 21export interface AutoMemoryDreamResult {22 touchedTopics: AutoMemoryType[];23 dedupedEntries: number;24 systemMessage?: string;25}26 27async function runDreamByAgent(28 projectRoot: string,29 config: Config,30 abortSignal?: AbortSignal,31 options: { suppressChatRecording?: boolean } = {},32): Promise<AutoMemoryDreamResult> {33 const result = await planManagedAutoMemoryDreamByAgent(34 config,35 projectRoot,36 abortSignal,37 { suppressChatRecording: options.suppressChatRecording },38 );39 40 // Infer which topics were touched from the file paths41 const touchedTopics = new Set<AutoMemoryType>();42 for (const filePath of result.filesTouched) {43 const normalized = filePath.replace(/\\/g, '/');44 for (const type of AUTO_MEMORY_TYPES) {45 if (normalized.includes(`/${type}/`)) {46 touchedTopics.add(type);47 }48 }49 }50 51 const summary = result.finalText52 ? result.finalText.trim().slice(0, 300)53 : `updated ${result.filesTouched.length} file(s)`;54 55 return {56 touchedTopics: [...touchedTopics],57 dedupedEntries: 0,58 systemMessage: `Managed auto-memory dream (agent): ${summary}`,59 };60}61 62export async function runManagedAutoMemoryDream(63 projectRoot: string,64 now = new Date(),65 config?: Config,66 abortSignal?: AbortSignal,67 options: {68 trigger?: 'auto' | 'manual';69 recordMetadata?: boolean;70 suppressChatRecording?: boolean;71 } = {},72): Promise<AutoMemoryDreamResult> {73 await ensureAutoMemoryScaffold(projectRoot, now);74 const t0 = Date.now();75 76 if (!config) {77 throw new Error(78 'Managed auto-memory dream requires config for forked-agent execution.',79 );80 }81 82 const agentResult = await runDreamByAgent(projectRoot, config, abortSignal, {83 suppressChatRecording: options.suppressChatRecording,84 });85 // Cancel-aware ordering:86 // 1. If aborted before this point, return the agent's partial result87 // WITHOUT rebuilding the index — index rebuild can be expensive88 // and re-running a cancelled dream cycle next time will rebuild89 // against the latest topic files anyway.90 // 2. If still alive, rebuild the index (informational, powers91 // recall) — but only when topics actually changed.92 // Scheduler-gating metadata (`lastDreamAt`, `lastDreamSessionId`,93 // `lastDreamTouchedTopics`, `lastDreamStatus`) is intentionally NOT94 // written here — `MemoryManager.runDream` owns the atomic95 // status-flip + metadata-write sequence to close the cancel race96 // window where a writeFile finishing concurrently with a cancel97 // could persist gating metadata for a record the manager is about98 // to mark `'cancelled'`.99 if (abortSignal?.aborted) return agentResult;100 if (agentResult.touchedTopics.length > 0) {101 await rebuildManagedAutoMemoryIndex(projectRoot);102 }103 if (options.recordMetadata) {104 await updateDreamMetadataResult(105 projectRoot,106 now,107 agentResult.touchedTopics,108 );109 }110 111 logMemoryDream(112 config,113 new MemoryDreamEvent({114 trigger: options.trigger ?? 'auto',115 status: agentResult.touchedTopics.length > 0 ? 'updated' : 'noop',116 deduped_entries: agentResult.dedupedEntries,117 touched_topics: agentResult.touchedTopics,118 duration_ms: Date.now() - t0,119 }),120 );121 return agentResult;122}123 124async function updateDreamMetadataResult(125 projectRoot: string,126 now: Date,127 touchedTopics: AutoMemoryType[],128 sessionId?: string,129): Promise<void> {130 const metadataPath = getAutoMemoryMetadataPath(projectRoot);131 try {132 const content = await fs.readFile(metadataPath, 'utf-8');133 const metadata = JSON.parse(content) as AutoMemoryMetadata;134 metadata.updatedAt = now.toISOString();135 metadata.lastDreamAt = now.toISOString();136 metadata.lastDreamTouchedTopics = touchedTopics;137 metadata.lastDreamStatus = touchedTopics.length > 0 ? 'updated' : 'noop';138 if (sessionId !== undefined) {139 metadata.lastDreamSessionId = sessionId;140 metadata.recentSessionIdsSinceDream = [];141 }142 await atomicWriteFile(143 metadataPath,144 `${JSON.stringify(metadata, null, 2)}\n`,145 { encoding: 'utf-8' },146 );147 } catch {148 // Best-effort metadata bump.149 }150}151 152/**153 * Record that the user manually ran /dream. Called from the CLI command's154 * onComplete callback after the main agent turn finishes writing memory files.155 * Writes lastDreamAt, lastDreamSessionId, and resets recentSessionIdsSinceDream156 * so that the scheduler's same-session dedupe check prevents a redundant157 * auto-dream from firing in the same session.158 */159export async function writeDreamManualRunToMetadata(160 projectRoot: string,161 sessionId: string,162 now = new Date(),163): Promise<void> {164 return updateDreamMetadataResult(projectRoot, now, [], sessionId);165}166 