Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/node/services/agentSession.autoCompaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,60 @@ describe("AgentSession on-send auto-compaction snapshot deferral", () => {
session.dispose();
});

test("tracks a pending compaction request across a crash-recovery [CONTINUE] sentinel", async () => {
const model = "openai:gpt-4o";
const { session } = await createSessionHarness({
workspaceId: "ws-auto-compaction-request-continue-sentinel",
});
const compactionMetadata = {
type: "compaction-request" as const,
rawCommand: "/compact",
parsed: {},
};
const compactionRequest = createMuxMessage(
"compaction-request",
"user",
"Summarize the conversation",
{
synthetic: true,
muxMetadata: compactionMetadata,
}
);
// Crash mid-compaction persists an orphaned assistant row; recovery appends a
// synthetic [CONTINUE] sentinel after it. The resumed stream sends without
// compaction options but must still correlate with the pending request.
const orphanedAssistant = createMuxMessage("orphaned-summary", "assistant", "## Summary", {});
const continueSentinel = createMuxMessage("continue-sentinel", "user", "[CONTINUE]", {
synthetic: true,
});
const internals = session as unknown as {
resolveCompactionRequest: (
history: MuxMessage[],
modelString: string,
options?: SendMessageOptions
) => { id: string } | undefined;
};

const request = internals.resolveCompactionRequest(
[compactionRequest, orphanedAssistant, continueSentinel],
model,
{ model, agentId: "default" }
);

expect(request).toMatchObject({ id: compactionRequest.id });

// A real user message after the request must stop correlation.
const realUser = createMuxMessage("real-user", "user", "thanks", {});
const stopped = internals.resolveCompactionRequest(
[compactionRequest, orphanedAssistant, continueSentinel, realUser],
model,
{ model, agentId: "default" }
);
expect(stopped).toBeUndefined();

session.dispose();
});

test("does not materialize skill snapshots (or run their directives) on deferred on-send compaction turns", async () => {
const workspaceId = "ws-auto-compaction-skill-snapshot-deferral";

Expand Down
10 changes: 5 additions & 5 deletions src/node/services/agentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4828,8 +4828,6 @@ export class AgentSession {
source?: "idle-compaction" | "auto-compaction";
}
| undefined {
const streamIsCompaction = isCompactionRequestMetadata(options?.muxMetadata);

for (let index = history.length - 1; index >= 0; index -= 1) {
const message = history[index];
if (message.role !== "user") {
Expand All @@ -4845,9 +4843,11 @@ export class AgentSession {
};
}

// Snapshot rows can follow a synthetic compaction request before stream startup.
// Skip only those rows when the current send options identify this stream as compaction.
if (!streamIsCompaction || message.metadata?.synthetic !== true) {
// Synthetic rows can follow a compaction request: prompt snapshots land before
// stream startup, and crash recovery can append a [CONTINUE] sentinel when the
// app restarts mid-compaction. Skip them regardless of what the current stream
// is, so the pending request stays correlated; stop at the first real user row.
if (message.metadata?.synthetic !== true) {
Comment thread
coadler marked this conversation as resolved.
Outdated
return undefined;
}
}
Expand Down
Loading