refactor(context): implement commit-on-render strategy for GC backstop

This commit is contained in:
Your Name
2026-05-13 23:56:20 +00:00
parent 0214e3fc77
commit 4f6186f6f1
2 changed files with 29 additions and 8 deletions
+26 -6
View File
@@ -47,8 +47,6 @@ export class ContextManager {
};
};
private lastAppliedNodes?: readonly ConcreteNode[];
private hasPerformedHotStart = false;
constructor(
@@ -80,6 +78,21 @@ export class ContextManager {
this.evaluateTriggers(event.newNodes);
});
this.eventBus.onProcessorResult((event) => {
// Defensive: Verify all targets are still present in the buffer.
// If a synchronous render or a previous async task already removed them,
// this result is stale and should be dropped.
const currentIds = new Set(this.buffer.nodes.map((n) => n.id));
const allTargetsPresent = event.targets.every((t) =>
currentIds.has(t.id),
);
if (!allTargetsPresent) {
debugLogger.log(
`[ContextManager] Dropping stale processor result from ${event.processorId}. One or more targets were already removed.`,
);
return;
}
this.buffer = this.buffer.applyProcessorResult(
event.processorId,
event.targets,
@@ -371,7 +384,16 @@ export class ContextManager {
processedNodes,
} = renderResult;
this.lastAppliedNodes = processedNodes;
if (didApplyManagement) {
// Commit the GC backstop results back to the master buffer.
// We filter out preview nodes because they are ephemeral and will be
// added to history naturally by the client after the turn completes.
this.buffer = this.buffer.applyProcessorResult(
'sync_backstop',
this.buffer.nodes,
processedNodes.filter((n) => !previewNodeIds.has(n.id)),
);
}
// Structural validation in debug mode
checkContextInvariants(this.buffer.nodes, 'RenderHistory');
@@ -444,9 +466,7 @@ export class ContextManager {
}
exportState(): ContextEngineState {
return SnapshotStateHelper.exportState(
this.lastAppliedNodes || this.buffer.nodes,
);
return SnapshotStateHelper.exportState(this.buffer.nodes);
}
restoreState(state: ContextEngineState): void {
+3 -2
View File
@@ -23,6 +23,7 @@ export function fromGraph(nodes: readonly ConcreteNode[]): Content[] {
for (const node of nodes) {
const turnId = node.turnId;
const partWithId = { ...node.payload, _synthId: node.id };
// We start a new turn if:
// 1. We don't have a current turn.
@@ -35,12 +36,12 @@ export function fromGraph(nodes: readonly ConcreteNode[]): Content[] {
) {
currentTurn = {
role: node.role,
parts: [node.payload],
parts: [partWithId],
_turnId: turnId,
};
history.push(currentTurn);
} else {
currentTurn.parts = [...(currentTurn.parts || []), node.payload];
currentTurn.parts = [...(currentTurn.parts || []), partWithId];
}
}