Skip to content

Commit 0311ff0

Browse files
joeykrugsteipete
authored andcommitted
fix: address Greptile review feedback
- Remove unrelated pnpm-lock.yaml changes - Move abortedLastRun flag clearing to AFTER successful resume (prevents permanent session loss on transient gateway failures) - Use dynamic import for orphan recovery module to avoid startup memory overhead - Add test assertion that flag is preserved on resume failure
1 parent 304703f commit 0311ff0

3 files changed

Lines changed: 38 additions & 19 deletions

File tree

src/agents/subagent-orphan-recovery.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ describe("subagent-orphan-recovery", () => {
186186
expect(gateway.callGateway).toHaveBeenCalledTimes(2);
187187
});
188188

189-
it("handles callGateway failure gracefully", async () => {
189+
it("handles callGateway failure gracefully and preserves abortedLastRun flag", async () => {
190190
const sessions = await import("../config/sessions.js");
191191
const gateway = await import("../gateway/call.js");
192192

@@ -211,6 +211,10 @@ describe("subagent-orphan-recovery", () => {
211211

212212
expect(result.recovered).toBe(0);
213213
expect(result.failed).toBe(1);
214+
215+
// abortedLastRun flag should NOT be cleared on failure,
216+
// so the next restart can retry the recovery
217+
expect(sessions.updateSessionStore).not.toHaveBeenCalled();
214218
});
215219

216220
it("returns empty results when no active runs exist", async () => {
@@ -246,8 +250,12 @@ describe("subagent-orphan-recovery", () => {
246250
expect(gateway.callGateway).not.toHaveBeenCalled();
247251
});
248252

249-
it("clears abortedLastRun flag before resuming", async () => {
253+
it("clears abortedLastRun flag after successful resume", async () => {
250254
const sessions = await import("../config/sessions.js");
255+
const gateway = await import("../gateway/call.js");
256+
257+
// Ensure callGateway succeeds for this test
258+
vi.mocked(gateway.callGateway).mockResolvedValue({ runId: "resumed-run" } as never);
251259

252260
vi.mocked(sessions.loadSessionStore).mockReturnValue({
253261
"agent:main:subagent:test-session-1": {
@@ -266,7 +274,7 @@ describe("subagent-orphan-recovery", () => {
266274
getActiveRuns: () => activeRuns,
267275
});
268276

269-
// updateSessionStore should have been called to clear the flag
277+
// updateSessionStore should have been called AFTER successful resume to clear the flag
270278
expect(sessions.updateSessionStore).toHaveBeenCalledOnce();
271279
const calls = vi.mocked(sessions.updateSessionStore).mock.calls;
272280
const [storePath, updater] = calls[0];

src/agents/subagent-orphan-recovery.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -129,25 +129,31 @@ export async function recoverOrphanedSubagentSessions(params: {
129129

130130
log.info(`found orphaned subagent session: ${childSessionKey} (run=${runId})`);
131131

132-
// Clear the aborted flag before resuming
133-
await updateSessionStore(storePath, (currentStore) => {
134-
const current = currentStore[childSessionKey];
135-
if (current) {
136-
current.abortedLastRun = false;
137-
current.updatedAt = Date.now();
138-
currentStore[childSessionKey] = current;
139-
}
140-
});
141-
142-
// Resume the session with the original task context
132+
// Resume the session with the original task context.
133+
// We intentionally do NOT clear abortedLastRun before attempting
134+
// the resume — if callGateway fails (e.g. gateway still booting),
135+
// the flag stays true so the next restart can retry.
143136
const resumed = await resumeOrphanedSession({
144137
sessionKey: childSessionKey,
145138
task: runRecord.task,
146139
});
147140

148141
if (resumed) {
142+
// Only clear the aborted flag after confirmed successful resume.
143+
await updateSessionStore(storePath, (currentStore) => {
144+
const current = currentStore[childSessionKey];
145+
if (current) {
146+
current.abortedLastRun = false;
147+
current.updatedAt = Date.now();
148+
currentStore[childSessionKey] = current;
149+
}
150+
});
149151
result.recovered++;
150152
} else {
153+
// Flag stays as abortedLastRun=true so next restart can retry
154+
log.warn(
155+
`resume failed for ${childSessionKey}; abortedLastRun flag preserved for retry on next restart`,
156+
);
151157
result.failed++;
152158
}
153159
} catch (err) {

src/agents/subagent-registry.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import {
3030
SUBAGENT_ENDED_REASON_KILLED,
3131
type SubagentLifecycleEndedReason,
3232
} from "./subagent-lifecycle-events.js";
33-
import { scheduleOrphanRecovery } from "./subagent-orphan-recovery.js";
3433
import {
3534
resolveCleanupCompletionReason,
3635
resolveDeferredCleanupDecision,
@@ -688,10 +687,16 @@ function restoreSubagentRunsOnce() {
688687

689688
// Schedule orphan recovery for subagent sessions that were aborted
690689
// by a SIGUSR1 reload. This runs after a short delay to let the
691-
// gateway fully bootstrap first. (#47711)
692-
scheduleOrphanRecovery({
693-
getActiveRuns: () => subagentRuns,
694-
});
690+
// gateway fully bootstrap first. Dynamic import to avoid increasing
691+
// startup memory footprint. (#47711)
692+
void import("./subagent-orphan-recovery.js").then(
693+
({ scheduleOrphanRecovery }) => {
694+
scheduleOrphanRecovery({ getActiveRuns: () => subagentRuns });
695+
},
696+
() => {
697+
// Ignore import failures — orphan recovery is best-effort.
698+
},
699+
);
695700
} catch {
696701
// ignore restore failures
697702
}

0 commit comments

Comments
 (0)