fix(agent): respect delivery status evidence

This commit is contained in:
Peter Steinberger
2026-05-10 12:00:14 +01:00
parent be63feacf7
commit 335e5456d0
6 changed files with 81 additions and 0 deletions

View File

@@ -624,6 +624,9 @@ terminal summary, and sanitized error text.
- `agent` requests can include `deliver=true` to request outbound delivery.
- `bestEffortDeliver=false` keeps strict behavior: unresolved or internal-only delivery targets return `INVALID_REQUEST`.
- `bestEffortDeliver=true` allows fallback to session-only execution when no external deliverable route can be resolved (for example internal/webchat sessions or ambiguous multi-channel configs).
- Final `agent` results may include `result.deliveryStatus` when delivery was
requested, using the same `sent`, `suppressed`, `partial_failed`, and `failed`
statuses documented for [`openclaw agent --json --deliver`](/cli/agent#json-delivery-status).
## Versioning

View File

@@ -77,6 +77,9 @@ programmatic delivery.
preserve isolation; direct chats collapse to `main`).
- Thinking and verbose flags persist into the session store.
- Output: plain text by default, or `--json` for structured payload + metadata.
- With `--json --deliver`, the JSON includes delivery status for sent,
suppressed, partial, and failed sends. See
[JSON delivery status](/cli/agent#json-delivery-status).
## Examples

View File

@@ -11,6 +11,10 @@ type AgentPayloadLike = {
export type AgentDeliveryEvidence = {
payloads?: unknown;
deliveryStatus?: {
status?: unknown;
errorMessage?: unknown;
};
didSendViaMessagingTool?: unknown;
messagingToolSentTexts?: unknown;
messagingToolSentMediaUrls?: unknown;
@@ -106,3 +110,15 @@ export function hasOutboundDeliveryEvidence(result: AgentDeliveryEvidence): bool
hasPositiveNumber(result.meta?.toolSummary?.calls)
);
}
export function getAgentCommandDeliveryFailure(result: AgentDeliveryEvidence): string | undefined {
const status = result.deliveryStatus?.status;
if (status !== "failed" && status !== "partial_failed") {
return undefined;
}
const message = result.deliveryStatus?.errorMessage;
if (hasNonEmptyString(message)) {
return message;
}
return status === "partial_failed" ? "agent delivery partially failed" : "agent delivery failed";
}

View File

@@ -732,6 +732,48 @@ describe("deliverSubagentAnnouncement completion delivery", () => {
expect(sendMessage).not.toHaveBeenCalled();
});
it("reports requester-agent delivery failure even when output stayed visible", async () => {
const callGateway = createGatewayMock({
result: {
payloads: [{ text: "Tests passed and the PR is ready for review." }],
deliveryStatus: {
status: "failed",
errorMessage: "Slack send failed: channel not found",
},
},
});
const sendMessage = createSendMessageMock();
const result = await deliverSlackThreadAnnouncement({
callGateway,
sendMessage,
sessionId: "requester-session-4",
isActive: false,
expectsCompletionMessage: true,
directIdempotencyKey: "announce-thread-delivery-status-failed",
internalEvents: [
{
type: "task_completion",
source: "subagent",
childSessionKey: "agent:worker:subagent:child",
childSessionId: "child-session-id",
announceType: "subagent task",
taskLabel: "thread completion smoke",
status: "ok",
statusLabel: "completed successfully",
result: "child completion output",
replyInstruction: "Summarize the result.",
},
],
});
expectRecordFields(result, {
delivered: false,
path: "direct",
error: "Slack send failed: channel not found",
});
expect(sendMessage).not.toHaveBeenCalled();
});
it("does not raw-send grouped child results when requester-agent output is empty", async () => {
const callGateway = createGatewayMock({
result: {

View File

@@ -23,6 +23,7 @@ import {
import { buildAnnounceIdempotencyKey, resolveQueueAnnounceId } from "./announce-idempotency.js";
import type { AgentInternalEvent } from "./internal-events.js";
import {
getAgentCommandDeliveryFailure,
getGatewayAgentResult,
hasMessagingToolDeliveryEvidence,
hasVisibleAgentPayload,
@@ -571,6 +572,11 @@ function hasGatewayAgentMessagingToolDelivery(response: unknown): boolean {
return Boolean(result && hasMessagingToolDeliveryEvidence(result));
}
function getGatewayAgentCommandDeliveryFailure(response: unknown): string | undefined {
const result = getGatewayAgentResult(response);
return result ? getAgentCommandDeliveryFailure(result) : undefined;
}
function isGatewayAgentRunPending(response: unknown): boolean {
if (!response || typeof response !== "object") {
return false;
@@ -850,6 +856,16 @@ async function sendSubagentAnnounceDirectly(params: {
error: "completion agent did not deliver through the message tool",
};
}
const directDeliveryFailure = shouldDeliverAgentFinal
? getGatewayAgentCommandDeliveryFailure(directAnnounceResponse)
: undefined;
if (directDeliveryFailure) {
return {
delivered: false,
path: "direct",
error: directDeliveryFailure,
};
}
if (
params.expectsCompletionMessage &&
shouldDeliverAgentFinal &&

View File

@@ -23,6 +23,7 @@ type AgentGatewayResult = {
mediaUrl?: string | null;
mediaUrls?: string[];
}>;
deliveryStatus?: unknown;
meta?: unknown;
};