Skip to content

Commit da09972

Browse files
committed
fix: avoid replaying channel restart recovery turns
1 parent 9b536ed commit da09972

2 files changed

Lines changed: 52 additions & 0 deletions

File tree

src/agents/main-session-restart-recovery.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,37 @@ describe("main-session-restart-recovery", () => {
157157
expect(store["agent:main:main"]?.abortedLastRun).toBe(true);
158158
});
159159

160+
it("fails channel-backed sessions instead of replaying a restart-recovery turn", async () => {
161+
const sessionsDir = await makeSessionsDir();
162+
await writeStore(sessionsDir, {
163+
"agent:main:discord:channel:123": {
164+
sessionId: "discord-session",
165+
updatedAt: Date.now() - 10_000,
166+
status: "running",
167+
abortedLastRun: true,
168+
channel: "discord",
169+
lastTo: "channel:123",
170+
origin: {
171+
provider: "discord",
172+
surface: "channel",
173+
},
174+
},
175+
});
176+
await writeTranscript(sessionsDir, "discord-session", [
177+
{ role: "user", content: "do the thing" },
178+
{ role: "assistant", content: [{ type: "toolCall", id: "call-1", name: "exec" }] },
179+
{ role: "toolResult", content: "done" },
180+
]);
181+
182+
const result = await recoverRestartAbortedMainSessions({ stateDir: tmpDir });
183+
184+
expect(result).toEqual({ recovered: 0, failed: 1, skipped: 0 });
185+
expect(callGateway).not.toHaveBeenCalled();
186+
const store = loadSessionStore(path.join(sessionsDir, "sessions.json"));
187+
expect(store["agent:main:discord:channel:123"]?.status).toBe("failed");
188+
expect(store["agent:main:discord:channel:123"]?.abortedLastRun).toBe(true);
189+
});
190+
160191
it("does not scan ordinary running sessions without the restart-aborted marker", async () => {
161192
const sessionsDir = await makeSessionsDir();
162193
await writeStore(sessionsDir, {

src/agents/main-session-restart-recovery.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ function shouldSkipMainRecovery(entry: SessionEntry, sessionKey: string): boolea
3232
);
3333
}
3434

35+
function isChannelBackedMainSession(entry: SessionEntry): boolean {
36+
return Boolean(
37+
entry.channel ||
38+
entry.deliveryContext ||
39+
entry.lastChannel ||
40+
entry.lastTo ||
41+
entry.origin?.surface ||
42+
entry.origin?.provider,
43+
);
44+
}
45+
3546
function sessionIdFromLockPath(lockPath: string): string | undefined {
3647
const fileName = path.basename(lockPath);
3748
if (!fileName.endsWith(".jsonl.lock")) {
@@ -224,6 +235,16 @@ async function recoverStore(params: {
224235
continue;
225236
}
226237

238+
if (isChannelBackedMainSession(entry)) {
239+
await markSessionFailed({
240+
storePath: params.storePath,
241+
sessionKey,
242+
reason: "channel-backed session requires manual restart recovery",
243+
});
244+
result.failed++;
245+
continue;
246+
}
247+
227248
let messages: unknown[];
228249
try {
229250
messages = readSessionMessages(entry.sessionId, params.storePath, entry.sessionFile);

0 commit comments

Comments
 (0)