




















@@ -342,6 +342,110 @@ describe("subagent-orphan-recovery", () => {
342342expect(mockStore["agent:main:subagent:test-session-1"]?.abortedLastRun).toBe(false);
343343});
344344345+it("persists accepted recovery attempts after successful resume", async () => {
346+vi.mocked(gateway.callGateway).mockResolvedValue({ runId: "resumed-run" } as never);
347+mockSingleAbortedSession();
348+349+await recoverOrphanedSubagentSessions({
350+getActiveRuns: () => createActiveRuns(createTestRunRecord()),
351+});
352+353+const [, updater] = vi.mocked(sessions.updateSessionStore).mock.calls[0];
354+const mockStore: ReturnType<typeof sessions.loadSessionStore> = {
355+"agent:main:subagent:test-session-1": {
356+sessionId: "session-abc",
357+updatedAt: 0,
358+abortedLastRun: true,
359+},
360+};
361+await updater(mockStore);
362+expect(mockStore["agent:main:subagent:test-session-1"]).toMatchObject({
363+abortedLastRun: false,
364+subagentRecovery: {
365+automaticAttempts: 1,
366+lastRunId: "run-1",
367+lastAttemptAt: expect.any(Number),
368+},
369+});
370+});
371+372+it("tombstones rapid repeated accepted recovery before resuming again", async () => {
373+const now = Date.now();
374+mockSingleAbortedSession({
375+subagentRecovery: {
376+automaticAttempts: 2,
377+lastAttemptAt: now - 30_000,
378+lastRunId: "previous-run",
379+},
380+});
381+382+const result = await recoverOrphanedSubagentSessions({
383+getActiveRuns: () => createActiveRuns(createTestRunRecord()),
384+});
385+386+expect(result).toMatchObject({
387+recovered: 0,
388+failed: 0,
389+skipped: 1,
390+failedRuns: [
391+expect.objectContaining({
392+runId: "run-1",
393+childSessionKey: "agent:main:subagent:test-session-1",
394+error: expect.stringContaining("recovery blocked after 2 rapid accepted resume attempts"),
395+}),
396+],
397+});
398+expect(gateway.callGateway).not.toHaveBeenCalled();
399+expect(sessions.updateSessionStore).toHaveBeenCalledOnce();
400+401+const [, updater] = vi.mocked(sessions.updateSessionStore).mock.calls[0];
402+const mockStore: ReturnType<typeof sessions.loadSessionStore> = {
403+"agent:main:subagent:test-session-1": {
404+sessionId: "session-abc",
405+updatedAt: 0,
406+abortedLastRun: true,
407+subagentRecovery: {
408+automaticAttempts: 2,
409+lastAttemptAt: now - 30_000,
410+lastRunId: "previous-run",
411+},
412+},
413+};
414+await updater(mockStore);
415+expect(mockStore["agent:main:subagent:test-session-1"]).toMatchObject({
416+abortedLastRun: false,
417+subagentRecovery: {
418+automaticAttempts: 2,
419+lastRunId: "run-1",
420+wedgedAt: expect.any(Number),
421+wedgedReason: expect.stringContaining("recovery blocked"),
422+},
423+});
424+});
425+426+it("skips already tombstoned wedged sessions without rewriting them", async () => {
427+mockSingleAbortedSession({
428+subagentRecovery: {
429+automaticAttempts: 2,
430+lastAttemptAt: Date.now() - 20_000,
431+lastRunId: "previous-run",
432+wedgedAt: Date.now() - 10_000,
433+wedgedReason: "subagent orphan recovery blocked after 2 rapid accepted resume attempts",
434+},
435+});
436+437+const result = await recoverOrphanedSubagentSessions({
438+getActiveRuns: () => createActiveRuns(createTestRunRecord()),
439+});
440+441+expect(result.recovered).toBe(0);
442+expect(result.failed).toBe(0);
443+expect(result.skipped).toBe(1);
444+expect(result.failedRuns).toHaveLength(1);
445+expect(gateway.callGateway).not.toHaveBeenCalled();
446+expect(sessions.updateSessionStore).not.toHaveBeenCalled();
447+});
448+345449it("truncates long task descriptions in resume message", async () => {
346450mockSingleAbortedSession();
347451此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。