feat(context): persist context engine snapshots across sessions

This commit implements an opaque state export/import pattern for the
ContextManager to ensure expensive LLM-derived snapshots are properly
rehydrated upon session resume.

The ContextManager now exposes `exportState` and `restoreState` methods,
delegating structural validation to the `SnapshotStateHelper`.
During active chat, the GeminiClient routinely passes the finalized
context state down to the ChatRecordingService, which seamlessly
embeds it into the existing JSONL metadata payload. Upon resume, the
saved snapshot is re-published as a draft to the LiveInbox, allowing
the synchronous pipeline to automatically and deterministically splice
it back into the raw graph without an additional LLM call.
This commit is contained in:
Your Name
2026-05-12 19:09:58 +00:00
parent 488d71b8c9
commit 65d4bdfc24
9 changed files with 233 additions and 65 deletions
@@ -16,6 +16,8 @@ import { HistoryObserver } from './historyObserver.js';
import { render } from './graph/render.js';
import { ContextWorkingBufferImpl } from './pipeline/contextWorkingBuffer.js';
import { debugLogger } from '../utils/debugLogger.js';
import { SnapshotStateHelper } from './utils/snapshotGenerator.js';
import type { ContextEngineState } from '../services/chatRecordingTypes.js';
import { hardenHistory } from '../utils/historyHardening.js';
import { checkContextInvariants } from './utils/invariantChecker.js';
import type { AdvancedTokenCalculator } from './utils/contextTokenCalculator.js';
@@ -429,4 +431,29 @@ export class ContextManager {
);
}
}
exportState(): ContextEngineState {
return SnapshotStateHelper.exportState(this.buffer.nodes);
}
async restoreState(state: ContextEngineState): Promise<void> {
if (!state) return;
SnapshotStateHelper.restoreState(state, this.env.inbox);
// Explicitly run the initialization trigger to eagerly splice the restored snapshot
// into the graph *before* the first user message creates cache artifacts.
const nodes = this.buffer.nodes;
const hydratedNodes = await this.orchestrator.executeTriggerSync(
'initialization',
nodes,
new Set(), // No trigger targets needed, it just reads the inbox
);
// Create a pseudo-processor result to apply the hydration without duplicating logic
this.buffer = this.buffer.applyProcessorResult(
'StateSnapshotHydration',
nodes,
hydratedNodes,
);
}
}