Skip to content

Commit 8be2194

Browse files
Kaspreclaude
andcommitted
fix(delivery): add deliverySucceeded compat field alongside deliveryStatus
ClawSweeper re-review (2026-05-08) caught that the slimmed return object dropped the top-level `deliverySucceeded` field. `src/agents/agent-command.ts` reads `deliveryResult?.deliverySucceeded === true` to decide whether to clear the durable `pendingFinalDelivery` retry marker — without it, a successful delivery never clears the marker and a restart can replay the already-delivered reply. Restore `deliverySucceeded: boolean` as a top-level field on the return object alongside `deliveryStatus`. Backward-compatible — agent-command.ts needs no changes. Test asserts the field mirrors `deliveryStatus.succeeded` on the success and missing-target paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7b84d3b commit 8be2194

2 files changed

Lines changed: 119 additions & 18 deletions

File tree

src/agents/command/delivery.test.ts

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,15 @@ function logMessages(runtime: ReturnType<typeof createRuntime>): string[] {
447447
return runtime.log.mock.calls.map((c: unknown[]) => String(c[0]));
448448
}
449449

450+
// The `[delivery] delivery requested but not completed` warning is routed to
451+
// runtime.error (with runtime.log fallback when error is unavailable). Tests
452+
// that look for that warning should use this helper rather than logMessages.
453+
function diagnosticMessages(runtime: ReturnType<typeof createRuntime>): string[] {
454+
return [...runtime.error.mock.calls, ...runtime.log.mock.calls].map((c: unknown[]) =>
455+
String(c[0]),
456+
);
457+
}
458+
450459
// ---------------------------------------------------------------------------
451460
// Tests
452461
// ---------------------------------------------------------------------------
@@ -477,6 +486,9 @@ describe("deliverAgentCommandResult — delivery status tracking", () => {
477486
attempted: true,
478487
succeeded: true,
479488
});
489+
// Backward-compat field consumed by agent-command.ts to clear
490+
// pendingFinalDelivery — must mirror deliveryStatus.succeeded.
491+
expect(result.deliverySucceeded).toBe(true);
480492
// No warning log on success
481493
expect(logMessages(runtime).some((msg) => msg.includes("[delivery]"))).toBe(false);
482494
});
@@ -510,8 +522,9 @@ describe("deliverAgentCommandResult — delivery status tracking", () => {
510522
attempted: false,
511523
succeeded: false,
512524
});
525+
expect(result.deliverySucceeded).toBe(false);
513526
expect(
514-
logMessages(runtime).some((msg) =>
527+
diagnosticMessages(runtime).some((msg) =>
515528
msg.includes("[delivery] delivery requested but not completed"),
516529
),
517530
).toBe(true);
@@ -533,9 +546,9 @@ describe("deliverAgentCommandResult — delivery status tracking", () => {
533546
attempted: true,
534547
succeeded: false,
535548
});
536-
expect(logMessages(runtime).some((msg) => msg.includes("delivery returned zero results"))).toBe(
537-
true,
538-
);
549+
expect(
550+
diagnosticMessages(runtime).some((msg) => msg.includes("delivery returned zero results")),
551+
).toBe(true);
539552
});
540553

541554
it("catches thrown error in bestEffort mode without re-throwing", async () => {
@@ -597,7 +610,7 @@ describe("deliverAgentCommandResult — delivery status tracking", () => {
597610
expect(result.deliveryStatus).toBeUndefined();
598611
});
599612

600-
it("returns succeeded=true with hadPartialFailure when onError fires but results exist", async () => {
613+
it("returns succeeded=false with hadPartialFailure when onError fires under best-effort", async () => {
601614
deliverSpy.mockImplementation(async (opts) => {
602615
// Simulate partial failure: onError fires for one payload, but results still returned
603616
opts.onError?.(new Error("Payload 2 failed"), { text: "hello" } as never);
@@ -609,16 +622,25 @@ describe("deliverAgentCommandResult — delivery status tracking", () => {
609622
deliver: true,
610623
channel: "discord",
611624
to: "channel:123456",
625+
bestEffortDeliver: true,
612626
});
613627

628+
// Partial failures count as not-succeeded so `agent-command.ts` keeps the
629+
// durable `pendingFinalDelivery` retry marker; matches upstream's pre-PR
630+
// `!deliveryHadError` semantics.
614631
expect(result.deliveryStatus).toEqual({
615632
requested: true,
616633
attempted: true,
617-
succeeded: true,
634+
succeeded: false,
618635
hadPartialFailure: true,
619636
});
620-
// No [delivery] warning — succeeded is true
621-
expect(logMessages(runtime).some((msg) => msg.includes("[delivery]"))).toBe(false);
637+
expect(result.deliverySucceeded).toBe(false);
638+
// [delivery] warning fires because succeeded is false
639+
expect(
640+
diagnosticMessages(runtime).some((msg) =>
641+
msg.includes("[delivery] delivery requested but not completed"),
642+
),
643+
).toBe(true);
622644
});
623645

624646
it("logs warning when channel resolves to internal", async () => {
@@ -642,8 +664,68 @@ describe("deliverAgentCommandResult — delivery status tracking", () => {
642664
succeeded: false,
643665
error: true,
644666
});
645-
expect(logMessages(runtime).some((msg) => msg.includes("channel resolved to internal"))).toBe(
667+
expect(
668+
diagnosticMessages(runtime).some((msg) => msg.includes("channel resolved to internal")),
669+
).toBe(true);
670+
});
671+
672+
it("reports preflight rejection (not zero results) when explicit target fails resolution", async () => {
673+
// Simulate best-effort preflight rejection: deliveryPlan still resolves a
674+
// target string, but resolveAgentOutboundTarget returns ok:false. The
675+
// delivery code marks hadPreflightError and skips the deliver call; the
676+
// diagnostic must report the preflight reason, not "zero results".
677+
outboundTargetSpy.mockReturnValue({
678+
resolvedTarget: {
679+
ok: false as const,
680+
error: new Error("target rejected by plugin"),
681+
} as ReturnType<typeof agentDeliveryModule.resolveAgentOutboundTarget>["resolvedTarget"],
682+
resolvedTo: "channel:123456",
683+
targetMode: "explicit" as const,
684+
});
685+
686+
const { result, runtime } = await runDelivery({
687+
message: "hello",
688+
deliver: true,
689+
bestEffortDeliver: true,
690+
channel: "discord",
691+
to: "channel:123456",
692+
});
693+
694+
expect(deliverSpy).not.toHaveBeenCalled();
695+
expect(result.deliveryStatus).toEqual({
696+
requested: true,
697+
attempted: false,
698+
succeeded: false,
699+
error: true,
700+
});
701+
const diagnostics = diagnosticMessages(runtime);
702+
expect(diagnostics.some((msg) => msg.includes("preflight rejected delivery target"))).toBe(
646703
true,
647704
);
705+
expect(diagnostics.some((msg) => msg.includes("delivery returned zero results"))).toBe(false);
706+
});
707+
708+
it("emits the [delivery] warning on stderr (runtime.error), not stdout", async () => {
709+
// Diagnostics belong on stderr so scripts that pipe stdout for payload
710+
// text don't conflate it with delivery status. Tested by verifying
711+
// runtime.error received the message and runtime.log did not.
712+
deliverSpy.mockResolvedValue([]);
713+
714+
const { runtime } = await runDelivery({
715+
message: "hello",
716+
deliver: true,
717+
channel: "discord",
718+
to: "channel:123456",
719+
});
720+
721+
const errorMessages = runtime.error.mock.calls.map((c: unknown[]) => String(c[0]));
722+
expect(
723+
errorMessages.some((msg) => msg.includes("[delivery] delivery requested but not completed")),
724+
).toBe(true);
725+
expect(
726+
logMessages(runtime).some((msg) =>
727+
msg.includes("[delivery] delivery requested but not completed"),
728+
),
729+
).toBe(false);
648730
});
649731
});

src/agents/command/delivery.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,8 @@ export async function deliverAgentCommandResult(params: {
385385
deliver &&
386386
deliveryChannel &&
387387
!isInternalMessageChannel(deliveryChannel) &&
388-
deliveryPayloads.length > 0
388+
deliveryPayloads.length > 0 &&
389+
!hadPreflightError
389390
) {
390391
if (deliveryTarget) {
391392
deliveryAttempted = true;
@@ -413,7 +414,13 @@ export async function deliverAgentCommandResult(params: {
413414
// since catching silent failures is this patch's primary goal. A future
414415
// change to deliverOutboundPayloads could expose cancellation metadata to
415416
// distinguish the two cases.
416-
deliverySucceeded = results.length > 0;
417+
//
418+
// Partial failures also count as not-succeeded: matches upstream's
419+
// pre-PR `!deliveryHadError` semantics so `agent-command.ts` keeps the
420+
// durable `pendingFinalDelivery` retry marker when any payload errored
421+
// under best-effort. Without this, a partial-failure final-delivery
422+
// would never be retried after restart.
423+
deliverySucceeded = results.length > 0 && !hadPartialFailure;
417424
} catch (err) {
418425
if (!bestEffortDeliver) {
419426
throw err;
@@ -435,14 +442,21 @@ export async function deliverAgentCommandResult(params: {
435442
? "channel resolved to internal"
436443
: !deliveryTarget
437444
? "no delivery target resolved"
438-
: deliveryThrewError
439-
? "delivery threw an error"
440-
: "delivery returned zero results";
441-
runtime.log(
445+
: hadPreflightError
446+
? "preflight rejected delivery target"
447+
: deliveryThrewError
448+
? "delivery threw an error"
449+
: hadPartialFailure
450+
? "partial failure during best-effort delivery"
451+
: "delivery returned zero results";
452+
const message =
442453
`[delivery] delivery requested but not completed: ${reason} ` +
443-
`(session=${effectiveSessionKey ?? "unknown"} channel=${deliveryChannel ?? "none"} ` +
444-
`target=${deliveryTarget ?? "none"} payloads=${deliveryPayloads.length})`,
445-
);
454+
`(session=${effectiveSessionKey ?? "unknown"} channel=${deliveryChannel ?? "none"} ` +
455+
`target=${deliveryTarget ?? "none"} payloads=${deliveryPayloads.length})`;
456+
runtime.error?.(message);
457+
if (!runtime.error) {
458+
runtime.log(message);
459+
}
446460
}
447461

448462
const deliveryStatusResult = deliver
@@ -458,6 +472,11 @@ export async function deliverAgentCommandResult(params: {
458472
return {
459473
payloads: normalizedPayloads,
460474
meta: resultMeta,
475+
// Backward-compat field for `src/agents/agent-command.ts` which reads
476+
// `deliveryResult?.deliverySucceeded === true` to decide whether to
477+
// clear the durable `pendingFinalDelivery` retry marker. Keeping this
478+
// top-level alongside `deliveryStatus` avoids touching agent-command.ts.
479+
deliverySucceeded,
461480
deliveryStatus: deliveryStatusResult,
462481
};
463482
}

0 commit comments

Comments
 (0)