






















@@ -1,6 +1,10 @@
11import { beforeEach, describe, expect, it, vi } from "vitest";
22import type { CallGatewayOptions } from "../gateway/call.js";
3-import { SUBAGENT_ENDED_REASON_COMPLETE } from "./subagent-lifecycle-events.js";
3+import {
4+SUBAGENT_ENDED_REASON_COMPLETE,
5+SUBAGENT_ENDED_REASON_ERROR,
6+SUBAGENT_ENDED_REASON_KILLED,
7+} from "./subagent-lifecycle-events.js";
48import { createSubagentRegistryLifecycleController } from "./subagent-registry-lifecycle.js";
59import type { SubagentRunRecord } from "./subagent-registry.types.js";
610@@ -653,6 +657,102 @@ describe("subagent registry lifecycle hardening", () => {
653657expect(Number.isNaN(entry.cleanupCompletedAt)).toBe(false);
654658});
655659660+it("suspends successful keep-mode final delivery instead of completing cleanup on retry exhaustion", async () => {
661+const persist = vi.fn();
662+const entry = createRunEntry({
663+endedAt: 4_000,
664+endedReason: SUBAGENT_ENDED_REASON_COMPLETE,
665+expectsCompletionMessage: true,
666+frozenResultText: "final answer",
667+lastAnnounceDeliveryError: "gateway request timeout for agent",
668+outcome: { status: "ok" },
669+retainAttachmentsOnKeep: true,
670+});
671+672+const controller = createLifecycleController({
673+ entry,
674+ persist,
675+captureSubagentCompletionReply: vi.fn(async () => undefined),
676+});
677+678+await controller.finalizeResumedAnnounceGiveUp({
679+runId: entry.runId,
680+ entry,
681+reason: "retry-limit",
682+});
683+684+expect(entry.pendingFinalDelivery).toBe(true);
685+expect(entry.pendingFinalDeliveryPayload).toMatchObject({
686+requesterSessionKey: entry.requesterSessionKey,
687+childSessionKey: entry.childSessionKey,
688+childRunId: entry.runId,
689+frozenResultText: "final answer",
690+});
691+expect(entry.deliverySuspendedAt).toBeTypeOf("number");
692+expect(entry.deliverySuspendedReason).toBe("retry-limit");
693+expect(entry.cleanupHandled).toBe(false);
694+expect(entry.cleanupCompletedAt).toBeUndefined();
695+expect(helperMocks.safeRemoveAttachmentsDir).not.toHaveBeenCalled();
696+expectFields(firstCallArg(taskExecutorMocks.setDetachedTaskDeliveryStatusByRunId), {
697+runId: entry.runId,
698+runtime: "subagent",
699+sessionKey: entry.childSessionKey,
700+deliveryStatus: "failed",
701+error: "gateway request timeout for agent",
702+});
703+expect(persist).toHaveBeenCalled();
704+});
705+706+it.each([
707+{
708+name: "timeout",
709+endedReason: SUBAGENT_ENDED_REASON_COMPLETE,
710+outcome: { status: "timeout" as const },
711+},
712+{
713+name: "error",
714+endedReason: SUBAGENT_ENDED_REASON_ERROR,
715+outcome: { status: "error" as const, error: "child failed" },
716+},
717+{
718+name: "killed",
719+endedReason: SUBAGENT_ENDED_REASON_KILLED,
720+outcome: undefined,
721+},
722+])(
723+"keeps $name completion cleanup terminal on retry exhaustion",
724+async ({ endedReason, outcome }) => {
725+const persist = vi.fn();
726+const entry = createRunEntry({
727+endedAt: 4_000,
728+ endedReason,
729+expectsCompletionMessage: true,
730+lastAnnounceDeliveryError: "gateway request timeout for agent",
731+ outcome,
732+retainAttachmentsOnKeep: true,
733+});
734+735+const controller = createLifecycleController({
736+ entry,
737+ persist,
738+captureSubagentCompletionReply: vi.fn(async () => undefined),
739+});
740+741+await controller.finalizeResumedAnnounceGiveUp({
742+runId: entry.runId,
743+ entry,
744+reason: "retry-limit",
745+});
746+747+expect(entry.pendingFinalDelivery).toBeUndefined();
748+expect(entry.pendingFinalDeliveryPayload).toBeUndefined();
749+expect(entry.deliverySuspendedAt).toBeUndefined();
750+expect(entry.deliverySuspendedReason).toBeUndefined();
751+expect(entry.cleanupCompletedAt).toBeTypeOf("number");
752+expect(persist).toHaveBeenCalled();
753+},
754+);
755+656756it("continues cleanup when delivery-status persistence throws after announce delivery", async () => {
657757const persist = vi.fn();
658758const warn = vi.fn();
@@ -767,7 +867,15 @@ describe("subagent registry lifecycle hardening", () => {
767867expect(entry.lastAnnounceDeliveryError).toBe(
768868"UNAVAILABLE: requester wake failed; direct-primary: UNAVAILABLE: requester wake failed",
769869);
770-expect(entry.cleanupCompletedAt).toBeTypeOf("number");
870+expect(entry.pendingFinalDelivery).toBe(true);
871+expect(entry.pendingFinalDeliveryPayload).toMatchObject({
872+requesterSessionKey: entry.requesterSessionKey,
873+childSessionKey: entry.childSessionKey,
874+childRunId: entry.runId,
875+});
876+expect(entry.deliverySuspendedAt).toBeTypeOf("number");
877+expect(entry.deliverySuspendedReason).toBe("retry-limit");
878+expect(entry.cleanupCompletedAt).toBeUndefined();
771879expect(persist).toHaveBeenCalled();
772880});
773881此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。