



















@@ -2,7 +2,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22import * as sessions from "../config/sessions.js";
33import * as gateway from "../gateway/call.js";
44import * as sessionUtils from "../gateway/session-utils.fs.js";
5-import { recoverOrphanedSubagentSessions } from "./subagent-orphan-recovery.js";
5+import * as announceDelivery from "./subagent-announce-delivery.js";
6+import {
7+recoverOrphanedSubagentSessions,
8+scheduleOrphanRecovery,
9+} from "./subagent-orphan-recovery.js";
610import * as subagentRegistrySteerRuntime from "./subagent-registry-steer-runtime.js";
711import type { SubagentRunRecord } from "./subagent-registry.types.js";
812@@ -28,8 +32,19 @@ vi.mock("../gateway/session-utils.fs.js", () => ({
2832readSessionMessages: vi.fn(() => []),
2933}));
303435+vi.mock("./subagent-announce-delivery.js", () => ({
36+deliverSubagentAnnouncement: vi.fn(async () => ({ delivered: true, path: "direct" })),
37+isInternalAnnounceRequesterSession: vi.fn(() => false),
38+loadRequesterSessionEntry: vi.fn(() => ({ entry: {} })),
39+}));
40+41+vi.mock("./subagent-announce-origin.js", () => ({
42+resolveAnnounceOrigin: vi.fn((entry, requesterOrigin) => requesterOrigin),
43+}));
44+3145vi.mock("./subagent-registry-steer-runtime.js", () => ({
3246replaceSubagentRunAfterSteer: vi.fn(() => true),
47+finalizeInterruptedSubagentRun: vi.fn(async () => 1),
3348}));
34493550function createTestRunRecord(overrides: Partial<SubagentRunRecord> = {}): SubagentRunRecord {
@@ -84,10 +99,12 @@ function getResumeMessage() {
849985100describe("subagent-orphan-recovery", () => {
86101beforeEach(() => {
102+vi.useFakeTimers();
87103vi.clearAllMocks();
88104});
8910590106afterEach(() => {
107+vi.useRealTimers();
91108vi.restoreAllMocks();
92109});
93110@@ -262,6 +279,13 @@ describe("subagent-orphan-recovery", () => {
262279263280expect(result.recovered).toBe(0);
264281expect(result.failed).toBe(1);
282+expect(result.failedRuns).toEqual([
283+expect.objectContaining({
284+runId: "run-1",
285+childSessionKey: "agent:main:subagent:test-session-1",
286+error: "gateway unavailable",
287+}),
288+]);
265289266290// abortedLastRun flag should NOT be cleared on failure,
267291// so the next restart can retry the recovery
@@ -369,6 +393,38 @@ describe("subagent-orphan-recovery", () => {
369393expect(message).toContain("config changes from your previous run were already applied");
370394});
371395396+it("announces recovery-in-progress once when a later retry is attempting resume", async () => {
397+mockSingleAbortedSession();
398+399+const activeRuns = createActiveRuns(createTestRunRecord());
400+const notifiedRecoverySessionKeys = new Set<string>();
401+402+await recoverOrphanedSubagentSessions({
403+getActiveRuns: () => activeRuns,
404+attemptNumber: 2,
405+maxAttempts: 4,
406+ notifiedRecoverySessionKeys,
407+});
408+409+expect(announceDelivery.deliverSubagentAnnouncement).toHaveBeenCalledOnce();
410+expect(announceDelivery.deliverSubagentAnnouncement).toHaveBeenCalledWith(
411+expect.objectContaining({
412+requesterSessionKey: "agent:main:quietchat:direct:+1234567890",
413+triggerMessage: expect.stringContaining("Automatic recovery is already in progress"),
414+}),
415+);
416+expect(notifiedRecoverySessionKeys).toEqual(new Set(["agent:main:subagent:test-session-1"]));
417+418+await recoverOrphanedSubagentSessions({
419+getActiveRuns: () => activeRuns,
420+attemptNumber: 3,
421+maxAttempts: 4,
422+ notifiedRecoverySessionKeys,
423+});
424+425+expect(announceDelivery.deliverSubagentAnnouncement).toHaveBeenCalledOnce();
426+});
427+372428it("prevents duplicate resume when updateSessionStore fails", async () => {
373429vi.mocked(gateway.callGateway).mockResolvedValue({ runId: "new-run" } as never);
374430vi.mocked(sessions.updateSessionStore).mockRejectedValue(new Error("write failed"));
@@ -429,4 +485,41 @@ describe("subagent-orphan-recovery", () => {
429485expect(gateway.callGateway).toHaveBeenCalledOnce();
430486expect(sessions.updateSessionStore).toHaveBeenCalledOnce();
431487});
488+489+it("finalizes interrupted runs with a readable failure after recovery retries are exhausted", async () => {
490+vi.mocked(sessions.loadSessionStore).mockReturnValue({
491+"agent:main:subagent:test-session-1": {
492+sessionId: "session-abc",
493+updatedAt: Date.now(),
494+abortedLastRun: true,
495+},
496+});
497+vi.mocked(gateway.callGateway).mockRejectedValue(new Error("service restart"));
498+499+const activeRuns = createActiveRuns(createTestRunRecord());
500+501+scheduleOrphanRecovery({
502+getActiveRuns: () => activeRuns,
503+delayMs: 1,
504+maxRetries: 1,
505+});
506+507+await vi.advanceTimersByTimeAsync(1);
508+await Promise.resolve();
509+await vi.advanceTimersByTimeAsync(2);
510+await Promise.resolve();
511+512+expect(subagentRegistrySteerRuntime.finalizeInterruptedSubagentRun).toHaveBeenCalledWith(
513+expect.objectContaining({
514+runId: "run-1",
515+childSessionKey: "agent:main:subagent:test-session-1",
516+error: expect.stringContaining("Automatic recovery failed after 2 attempts"),
517+}),
518+);
519+expect(subagentRegistrySteerRuntime.finalizeInterruptedSubagentRun).toHaveBeenCalledWith(
520+expect.objectContaining({
521+error: expect.stringContaining("service restart"),
522+}),
523+);
524+});
432525});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。