fix(cron): deliver spawned child results when scheduled agents emit no text (#117308)

* fix(cron): deliver spawned child results when parents emit no text

* test(cron): cover spawn-only scheduled child delivery end to end
This commit is contained in:
Peter Steinberger
2026-08-01 04:16:10 -07:00
committed by GitHub
parent 4e47c4cabf
commit e9e4023e59
6 changed files with 552 additions and 8 deletions
@@ -0,0 +1,194 @@
title: Isolated cron spawn-only child delivery
scenario:
id: cron-isolated-spawn-only-child-delivery
surface: cron
coverage:
primary:
- automation.isolated-cron-execution
- agent-runtime.subagent-turns-delivery
secondary:
- agent-runtime.subagent-turns-subagents
- automation.run-history-cron
- channels.qa-channel-final-reply
regressionRefs:
- openclaw/openclaw#117308
objective: Verify a naturally fired isolated one-shot cron with a spawn-only parent delivers its child's exact reply before retiring.
successCriteria:
- The isolated parent successfully spawns exactly one child and finishes without producing assistant text.
- The child completes successfully and its exact reply reaches the QA channel once.
- Cron history records successful delivery before the one-shot job is retired.
docsRefs:
- docs/help/testing.md
- docs/channels/qa-channel.md
- docs/tools/subagents.md
codeRefs:
- src/cron/isolated-agent/run-finalize.ts
- src/cron/isolated-agent/delivery-dispatch.ts
- src/cron/service/timer-outcomes.ts
- extensions/qa-lab/src/providers/mock-openai/server.ts
execution:
kind: flow
suiteIsolation: isolated
isolationReason: Owns a naturally scheduled one-shot job, subagent lifecycle, and exact QA-channel delivery.
channel: qa-channel
retryCount: 0
timeoutMs: 180000
summary: Naturally fire a spawn-only isolated cron and require one exact child delivery before one-shot retirement.
config:
requiredProviderMode: mock-openai
channelId: qa-room
fireDelayMs: 12000
duplicateWindowMs: 2000
promptSnippet: Subagent direct fallback QA check
workerSnippet: Subagent direct fallback worker
expectedLabel: qa-direct-fallback-worker
expectedMarker: QA-SUBAGENT-DIRECT-FALLBACK-OK
prompt: "Subagent direct fallback QA check: spawn one native subagent worker. The worker must finish with exactly QA-SUBAGENT-DIRECT-FALLBACK-OK. After the spawn is accepted, finish without assistant text. Empty response exhaustion QA check. Do not use ACP."
flow:
steps:
- name: naturally fires an isolated spawn-only one-shot job
actions:
- call: waitForGatewayHealthy
args:
- ref: env
- 120000
- call: waitForQaChannelReady
args:
- ref: env
- 120000
- call: reset
- set: requestCursorBefore
value:
expr: "(await fetchJson(`${env.mock.baseUrl}/debug/request-cursor`)).cursor"
- set: runStartedAt
value:
expr: "Date.now()"
- set: scheduledFor
value:
expr: "new Date(runStartedAt + config.fireDelayMs).toISOString()"
- call: env.gateway.call
saveAs: addedJob
args:
- cron.add
- agentId: qa
name:
expr: "`qa-cron-spawn-only-${randomUUID()}`"
enabled: true
deleteAfterRun: true
schedule:
kind: at
at:
ref: scheduledFor
sessionTarget: isolated
wakeMode: now
payload:
kind: agentTurn
timeoutSeconds: 90
toolsAllow:
- sessions_spawn
- read
message:
ref: config.prompt
delivery:
mode: announce
channel: qa-channel
to:
expr: "`channel:${config.channelId}`"
- timeoutMs: 30000
- set: jobId
value:
expr: "addedJob.id"
- assert:
expr: "Boolean(jobId) && addedJob.sessionTarget === 'isolated' && addedJob.deleteAfterRun === true && JSON.stringify(addedJob.payload?.toolsAllow) === JSON.stringify(['sessions_spawn', 'read'])"
message:
expr: "`expected an isolated delete-after-run cron job restricted to sessions_spawn and read, got ${JSON.stringify(addedJob)}`"
- call: waitForCronRunCompletion
saveAs: completedRun
args:
- callGateway:
expr: "env.gateway.call.bind(env.gateway)"
jobId:
ref: jobId
afterTs:
ref: runStartedAt
timeoutMs:
expr: "liveTurnTimeoutMs(env, config.fireDelayMs + 90000)"
- assert:
expr: "Date.now() >= new Date(scheduledFor).getTime()"
message: one-shot cron completed before its natural scheduled time
- assert:
expr: "completedRun.status === 'ok' && completedRun.delivered === true && completedRun.deliveryStatus === 'delivered'"
message:
expr: "`expected successful delivered spawn-only cron run, got ${JSON.stringify(completedRun)}`"
- assert:
expr: "String(completedRun.summary ?? '').trim() === config.expectedMarker"
message:
expr: "`cron summary did not preserve the child's exact reply: ${JSON.stringify(completedRun)}`"
- name: proves the parent accepted its child without producing assistant text
actions:
- set: scenarioRequests
value:
expr: "await fetchJson(`${env.mock.baseUrl}/debug/requests?after=${requestCursorBefore}`)"
- set: parentRequests
value:
expr: "scenarioRequests.filter((request) => String(request.prompt ?? '').includes(config.promptSnippet))"
- set: spawnRequests
value:
expr: "parentRequests.filter((request) => request.plannedToolName === 'sessions_spawn')"
- set: terminalRequests
value:
expr: "parentRequests.filter((request) => !request.plannedToolName)"
- assert:
expr: "parentRequests.length === 2 && spawnRequests.length === 1 && terminalRequests.length === 1 && spawnRequests[0].plannedToolArgs?.label === config.expectedLabel && spawnRequests[0].cursor < terminalRequests[0].cursor && /empty response exhaustion qa check/i.test(String(terminalRequests[0].allInputText ?? ''))"
message:
expr: "`expected one child spawn followed by an empty tool-free parent response, saw ${JSON.stringify(parentRequests.map((request) => ({ tool: request.plannedToolName ?? null, label: request.plannedToolArgs?.label ?? null })))}`"
- set: acceptedSpawn
value:
expr: "(() => { let result = JSON.parse(String(terminalRequests[0].toolOutput ?? 'null')); if (result?.status === 'completed') result = result.value; return typeof result === 'string' ? JSON.parse(result) : result; })()"
- assert:
expr: "acceptedSpawn?.status === 'accepted' && Boolean(String(acceptedSpawn.childSessionKey ?? '').trim()) && Boolean(String(acceptedSpawn.runId ?? '').trim())"
message:
expr: "`parent did not receive a successful child-spawn result before its empty response: ${JSON.stringify(acceptedSpawn)}`"
- set: childRequests
value:
expr: "scenarioRequests.filter((request) => String(request.prompt ?? '').includes(config.workerSnippet))"
- assert:
expr: "childRequests.length === 1 && !childRequests[0].plannedToolName"
message:
expr: "`expected one child assistant reply without additional tools, saw ${JSON.stringify(childRequests.map((request) => request.plannedToolName ?? null))}`"
- name: delivers the exact child reply once and retires the one-shot job
actions:
- call: waitForOutboundMessage
saveAs: deliveredReply
args:
- ref: state
- lambda:
params: [candidate]
expr: "candidate.conversation.id === config.channelId && String(candidate.text ?? '').trim() === config.expectedMarker"
- expr: "liveTurnTimeoutMs(env, 30000)"
- call: sleep
args:
- expr: "config.duplicateWindowMs"
- set: deliveredReplies
value:
expr: "getTransportSnapshot().messages.filter((message) => message.direction === 'outbound' && message.conversation.id === config.channelId && String(message.text ?? '').trim() === config.expectedMarker)"
- assert:
expr: "deliveredReplies.length === 1"
message:
expr: "`expected exactly one child reply on qa-channel, saw ${deliveredReplies.length}: ${JSON.stringify(deliveredReplies.map((message) => message.text))}`"
- call: waitForCondition
saveAs: retiredJobs
args:
- lambda:
async: true
expr: "env.gateway.call('cron.list', { includeDisabled: true }, { timeoutMs: 30000 }).then((page) => page.jobs.some((job) => job.id === jobId) ? undefined : page.jobs)"
- expr: "liveTurnTimeoutMs(env, 30000)"
- 250
- assert:
expr: "!retiredJobs.some((job) => job.id === jobId)"
message: successful one-shot job was not retired after exact child delivery
detailsExpr: "`child=${config.expectedLabel} accepted=true parentVisibleText=false delivery=${completedRun.deliveryStatus} reply=${deliveredReply.text} outboundCount=${deliveredReplies.length} retired=true`"
@@ -27,6 +27,7 @@ export type DispatchCronDeliveryParams = {
resolvedDelivery: DeliveryTargetResolution;
deliveryRequested: boolean;
skipHeartbeatDelivery: boolean;
spawnOnlyHandoff: boolean;
sourceDeliveryOutcome: SourceDeliveryOutcome;
deliveryBestEffort: boolean;
deliveryPayloadHasStructuredContent: boolean;
@@ -214,6 +214,7 @@ function makeBaseParams(overrides: {
runStartedAt?: number;
sessionTarget?: string;
deliveryBestEffort?: boolean;
spawnOnlyHandoff?: boolean;
runSessionKey?: string;
resolvedDeliveryMode?: "explicit" | "implicit";
}): Parameters<typeof dispatchCronDelivery>[0] {
@@ -245,6 +246,7 @@ function makeBaseParams(overrides: {
resolvedDelivery,
deliveryRequested: overrides.deliveryRequested ?? true,
skipHeartbeatDelivery: false,
spawnOnlyHandoff: overrides.spawnOnlyHandoff ?? false,
sourceDeliveryOutcome: {
visibleDeliveries: [],
verifiedMessageToolDelivery: false,
@@ -1101,6 +1103,169 @@ describe("dispatchCronDelivery — double-announce guard", () => {
});
});
it.each([
{
name: "active direct",
activeDescendants: true,
threadId: undefined,
deliveryBestEffort: false,
},
{
name: "active threaded",
activeDescendants: true,
threadId: "42",
deliveryBestEffort: false,
},
{
name: "completed direct",
activeDescendants: false,
threadId: undefined,
deliveryBestEffort: false,
},
{
name: "completed threaded",
activeDescendants: false,
threadId: "42",
deliveryBestEffort: false,
},
{
name: "active best-effort direct",
activeDescendants: true,
threadId: undefined,
deliveryBestEffort: true,
},
])(
"delivers $name accepted child results without parent text",
async ({ activeDescendants, deliveryBestEffort, threadId }) => {
const childReply = "Completed child result visible to the user.";
if (activeDescendants) {
vi.mocked(countActiveDescendantRuns).mockReturnValueOnce(1).mockReturnValueOnce(0);
} else {
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
}
vi.mocked(waitForDescendantSubagentSummary).mockResolvedValue(undefined);
vi.mocked(readDescendantSubagentFallbackReply).mockResolvedValue(childReply);
const params = makeBaseParams({
spawnOnlyHandoff: true,
deliveryBestEffort,
synthesizedText: "",
});
params.synthesizedText = undefined;
params.deliveryPayloads = [];
params.summary = undefined;
params.outputText = undefined;
params.resolvedDelivery = makeResolvedDelivery({ threadId });
const state = await dispatchCronDelivery(params);
expect(waitForDescendantSubagentSummary).toHaveBeenCalledTimes(activeDescendants ? 1 : 0);
expect(readDescendantSubagentFallbackReply).toHaveBeenCalledWith({
sessionKey: params.runSessionKey,
runStartedAt: params.runStartedAt,
});
expect(deliverOutboundPayloads).toHaveBeenCalledTimes(1);
expectDeliveryCall(0, {
channel: "telegram",
to: "123456",
...(threadId === undefined ? {} : { threadId }),
payloads: [{ text: childReply }],
});
expect(state.delivered).toBe(true);
expect(state.deliveryAttempted).toBe(true);
},
);
it("preserves a substantive parent synthesis after an accepted child has completed", async () => {
const parentReply = "Combined parent summary already includes every child result.";
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
const state = await dispatchCronDelivery(
makeBaseParams({ spawnOnlyHandoff: false, synthesizedText: parentReply }),
);
expect(readDescendantSubagentFallbackReply).not.toHaveBeenCalled();
expectDeliveryCall(0, { payloads: [{ text: parentReply }] });
expect(state.delivered).toBe(true);
});
it("immediately delivers a substantive threaded parent while its accepted child runs", async () => {
const parentReply = "Parent summary is ready for the existing thread.";
vi.mocked(countActiveDescendantRuns).mockReturnValue(1);
const params = makeBaseParams({ spawnOnlyHandoff: false, synthesizedText: parentReply });
params.resolvedDelivery = makeResolvedDelivery({ threadId: "42" });
const state = await dispatchCronDelivery(params);
expect(waitForDescendantSubagentSummary).not.toHaveBeenCalled();
expect(deliverOutboundPayloads).toHaveBeenCalledTimes(1);
expectDeliveryCall(0, { threadId: "42", payloads: [{ text: parentReply }] });
expect(state.delivered).toBe(true);
});
it.each([
{
name: "active child times out",
activeDescendants: 1,
error: "cron child-session handoff timed out before producing a final assistant payload",
},
{
name: "completed child has no output",
activeDescendants: 0,
error: "cron child-session handoff completed without a final assistant payload",
},
])("fails an accepted spawn-only handoff when $name", async ({ activeDescendants, error }) => {
vi.mocked(countActiveDescendantRuns).mockReturnValue(activeDescendants);
const params = makeBaseParams({ spawnOnlyHandoff: true, synthesizedText: "" });
params.synthesizedText = undefined;
params.deliveryPayloads = [];
params.summary = undefined;
params.outputText = undefined;
const state = await dispatchCronDelivery(params);
expectResultFields(state.result, {
status: "error",
error,
delivered: false,
deliveryAttempted: true,
});
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
});
it("preserves abort precedence when an accepted child handoff is interrupted", async () => {
const abortReason = "scheduled run aborted while waiting for its child";
vi.mocked(countActiveDescendantRuns).mockReturnValue(1);
const params = makeBaseParams({ spawnOnlyHandoff: true, synthesizedText: "" });
params.synthesizedText = undefined;
params.deliveryPayloads = [];
params.summary = undefined;
params.outputText = undefined;
params.isAborted = () => true;
params.abortReason = () => abortReason;
const state = await dispatchCronDelivery(params);
expect(waitForDescendantSubagentSummary).toHaveBeenCalledTimes(1);
expectResultFields(state.result, { status: "error", error: abortReason });
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
});
it("keeps an empty no-spawn parent silent", async () => {
const params = makeBaseParams({ synthesizedText: "" });
params.synthesizedText = undefined;
params.deliveryPayloads = [];
params.summary = undefined;
params.outputText = undefined;
const state = await dispatchCronDelivery(params);
expect(waitForDescendantSubagentSummary).not.toHaveBeenCalled();
expect(readDescendantSubagentFallbackReply).not.toHaveBeenCalled();
expect(deliverOutboundPayloads).not.toHaveBeenCalled();
expect(state.deliveryAttempted).toBe(false);
});
it("uses the run-scoped session key for isolated cron descendant fallback delivery", async () => {
const runStartedAt = 1_000;
const agentSessionKey = "agent:main:cron:daily-monitor";
+29 -6
View File
@@ -573,10 +573,11 @@ export async function dispatchCronDelivery(
const finalizeTextDelivery = async (
delivery: SuccessfulCronDeliveryTarget,
): Promise<RunCronAgentTurnResult | null> => {
if (!synthesizedText) {
if (!synthesizedText && !params.spawnOnlyHandoff) {
return null;
}
const initialSynthesizedText = synthesizedText.trim();
const initialSynthesizedText = synthesizedText?.trim() ?? "";
const spawnOnlyHandoff = params.spawnOnlyHandoff;
const expectedSubagentFollowup = expectsSubagentFollowup(initialSynthesizedText);
const subagentRegistryRuntime = await loadDeliverySubagentRegistryRuntime();
const subagentFollowupSessionKey = params.runSessionKey;
@@ -584,7 +585,8 @@ export async function dispatchCronDelivery(
subagentFollowupSessionKey,
);
const shouldCheckCompletedDescendants =
activeSubagentRuns === 0 && isLikelyInterimCronMessage(initialSynthesizedText);
activeSubagentRuns === 0 &&
(spawnOnlyHandoff || isLikelyInterimCronMessage(initialSynthesizedText));
const needsSubagentFollowupRuntime =
shouldCheckCompletedDescendants || activeSubagentRuns > 0 || expectedSubagentFollowup;
const subagentFollowupRuntime = needsSubagentFollowupRuntime
@@ -602,7 +604,10 @@ export async function dispatchCronDelivery(
})
: undefined;
const hadDescendants = activeSubagentRuns > 0 || Boolean(completedDescendantReply);
if (!params.deliveryBestEffort && (activeSubagentRuns > 0 || expectedSubagentFollowup)) {
if (
(!params.deliveryBestEffort || spawnOnlyHandoff) &&
(activeSubagentRuns > 0 || expectedSubagentFollowup)
) {
let finalReply = await subagentFollowupRuntime?.waitForDescendantSubagentSummary({
sessionKey: subagentFollowupSessionKey,
initialReply: initialSynthesizedText,
@@ -632,6 +637,23 @@ export async function dispatchCronDelivery(
synthesizedText = completedDescendantReply;
deliveryPayloads = [{ text: completedDescendantReply }];
}
if (spawnOnlyHandoff && !synthesizedText?.trim()) {
// An accepted spawn is the turn's only completion; retiring it without
// child output permanently loses one-shot scheduled work.
const error = params.isAborted()
? params.abortReason()
: activeSubagentRuns > 0
? "cron child-session handoff timed out before producing a final assistant payload"
: "cron child-session handoff completed without a final assistant payload";
deliveryAttempted = true;
return params.withRunSession({
status: "error",
error,
delivered: false,
deliveryAttempted,
...params.telemetry,
});
}
if (!params.deliveryBestEffort && activeSubagentRuns > 0) {
// Parent orchestration is still in progress; avoid announcing a partial
// update to the main requester. Mark deliveryAttempted so the timer does
@@ -647,7 +669,7 @@ export async function dispatchCronDelivery(
}
if (
hadDescendants &&
synthesizedText.trim() === initialSynthesizedText &&
synthesizedText?.trim() === initialSynthesizedText &&
isLikelyInterimCronMessage(initialSynthesizedText) &&
!isSilentReplyText(initialSynthesizedText, SILENT_REPLY_TOKEN)
) {
@@ -715,7 +737,8 @@ export async function dispatchCronDelivery(
// send through the real outbound adapter so delivered=true always reflects
// an actual channel send instead of internal announce routing.
const useDirectDelivery =
params.deliveryPayloadHasStructuredContent || params.resolvedDelivery.threadId != null;
params.deliveryPayloadHasStructuredContent ||
(params.resolvedDelivery.threadId != null && !params.spawnOnlyHandoff);
if (useDirectDelivery) {
const directResult = await deliverViaDirectAndCleanup(params.resolvedDelivery);
if (directResult) {
+13 -1
View File
@@ -333,9 +333,15 @@ export async function finalizeCronRun(params: {
}
};
const acceptedSessionSpawn = hasAcceptedSessionSpawn(finalRunResult.acceptedSessionSpawns);
const spawnOnlyHandoff =
acceptedSessionSpawn &&
deliveryPayloads.length === 0 &&
normalizeOptionalString(synthesizedText) === undefined;
const skipHeartbeatDelivery =
prepared.deliveryRequested &&
!hasFatalErrorPayload &&
!spawnOnlyHandoff &&
isHeartbeatOnlyResponse(deliveryPayloads, resolveHeartbeatAckMaxChars(prepared.agentCfg));
const sourceDeliveryOutcome = resolveSourceDeliveryOutcome(prepared.sourceDelivery, {
didSendViaMessageTool: finalRunResult.didSendViaMessagingTool,
@@ -356,7 +362,7 @@ export async function finalizeCronRun(params: {
const hasCommittedTerminalProgress =
hasCommittedMessagingToolDeliveryEvidence(finalRunResult) ||
finalRunResult.didSendDeterministicApprovalPrompt === true ||
hasAcceptedSessionSpawn(finalRunResult.acceptedSessionSpawns) ||
acceptedSessionSpawn ||
(finalRunResult.successfulCronAdds ?? 0) > 0;
const hasIntentionalSilentReply =
finalRunResult.meta?.terminalReplyKind === "silent-empty" ||
@@ -432,6 +438,7 @@ export async function finalizeCronRun(params: {
resolvedDelivery: prepared.resolvedDelivery,
deliveryRequested: prepared.deliveryRequested,
skipHeartbeatDelivery,
spawnOnlyHandoff,
sourceDeliveryOutcome,
deliveryBestEffort: resolveCronDeliveryBestEffort(prepared.input.job),
deliveryPayloadHasStructuredContent,
@@ -483,6 +490,10 @@ export async function finalizeCronRun(params: {
resultWithDeliveryMeta.delivered ?? deliveryResult.delivered,
);
if (!hasFatalErrorPayload) {
// Spawn-only turns are incomplete until a child produces output; keeping
// their failure visible prevents a one-shot job from being retired.
const incompleteSpawnOnlyHandoff =
spawnOnlyHandoff && normalizeOptionalString(deliveryResult.synthesizedText) === undefined;
// A successful isolated agent turn must keep `status: "ok"` even when the
// post-run delivery phase fails. Collapsing the delivery error into the
// execution status made the outer scheduled run report `status=error`
@@ -492,6 +503,7 @@ export async function finalizeCronRun(params: {
if (
deliveryResult.result.status === "error" &&
deliveryResult.result.errorKind !== "delivery-target" &&
!incompleteSpawnOnlyHandoff &&
!params.isAborted()
) {
const failedDeliveryError = resultWithDeliveryMeta.error;
@@ -5,6 +5,7 @@ import { setupRunCronIsolatedAgentTurnSuite } from "./run.suite-helpers.js";
import {
cleanupDirectCronSessionMock,
dispatchCronDeliveryMock,
isHeartbeatOnlyResponseMock,
loadRunCronIsolatedAgentTurn,
resolveCronDeliveryPlanMock,
resolveCronPayloadOutcomeMock,
@@ -293,6 +294,7 @@ describe("runCronIsolatedAgentTurn - meta.error status propagation", () => {
});
it("does not mark empty accepted child-session handoffs as cron errors", async () => {
isHeartbeatOnlyResponseMock.mockReturnValue(true);
runWithModelFallbackMock.mockResolvedValueOnce({
result: {
payloads: [],
@@ -325,11 +327,158 @@ describe("runCronIsolatedAgentTurn - meta.error status propagation", () => {
const result = await runCronIsolatedAgentTurn(makeIsolatedAgentParamsFixture());
expect(dispatchCronDeliveryMock).toHaveBeenCalled();
expect(dispatchCronDeliveryMock).toHaveBeenCalledWith(
expect.objectContaining({
spawnOnlyHandoff: true,
skipHeartbeatDelivery: false,
deliveryPayloads: [],
synthesizedText: undefined,
}),
);
expect(result.status).toBe("ok");
expect(result.error).toBeUndefined();
});
it("preserves incomplete accepted child-session handoffs as cron errors", async () => {
const error = "cron child-session handoff timed out before producing a final assistant payload";
runWithModelFallbackMock.mockResolvedValueOnce({
result: {
payloads: [],
acceptedSessionSpawns: [{ runId: "run-child", childSessionKey: "agent:default:child" }],
meta: {
agentMeta: { usage: { input: 10, output: 0 } },
},
},
provider: "anthropic",
model: "claude-opus-4-8",
attempts: [],
});
resolveCronDeliveryPlanMock.mockReturnValue({
requested: true,
mode: "announce",
channel: "messagechat",
to: "test-target",
});
resolveCronPayloadOutcomeMock.mockReturnValue({
summary: undefined,
outputText: undefined,
synthesizedText: undefined,
deliveryPayload: undefined,
deliveryPayloads: [],
deliveryPayloadHasStructuredContent: false,
hasFatalErrorPayload: false,
hasFatalStructuredErrorPayload: false,
embeddedRunError: undefined,
});
dispatchCronDeliveryMock.mockImplementationOnce(({ withRunSession }) => ({
result: withRunSession({ status: "error", error, deliveryAttempted: true }),
delivered: false,
deliveryAttempted: true,
cronRunSessionCleanupAttempted: false,
summary: undefined,
outputText: undefined,
synthesizedText: undefined,
deliveryPayloads: [],
}));
const result = await runCronIsolatedAgentTurn(makeIsolatedAgentParamsFixture());
expect(result.status).toBe("error");
expect(result.error).toBe(error);
expect(result.delivered).toBe(false);
});
it("keeps actual heartbeat acknowledgements silent after an accepted child spawn", async () => {
const heartbeatPayload = { text: "HEARTBEAT_OK" };
isHeartbeatOnlyResponseMock.mockReturnValue(true);
runWithModelFallbackMock.mockResolvedValueOnce({
result: {
payloads: [heartbeatPayload],
acceptedSessionSpawns: [{ runId: "run-child", childSessionKey: "agent:default:child" }],
meta: { agentMeta: { usage: { input: 10, output: 1 } } },
},
provider: "anthropic",
model: "claude-opus-4-8",
attempts: [],
});
resolveCronDeliveryPlanMock.mockReturnValue({
requested: true,
mode: "announce",
channel: "messagechat",
to: "test-target",
});
resolveCronPayloadOutcomeMock.mockReturnValue({
summary: heartbeatPayload.text,
outputText: heartbeatPayload.text,
synthesizedText: heartbeatPayload.text,
deliveryPayload: heartbeatPayload,
deliveryPayloads: [heartbeatPayload],
deliveryPayloadHasStructuredContent: false,
hasFatalErrorPayload: false,
hasFatalStructuredErrorPayload: false,
embeddedRunError: undefined,
});
await runCronIsolatedAgentTurn(makeIsolatedAgentParamsFixture());
expect(dispatchCronDeliveryMock).toHaveBeenCalledWith(
expect.objectContaining({ spawnOnlyHandoff: false, skipHeartbeatDelivery: true }),
);
});
it("preserves structured-parent delivery failures after accepting a child", async () => {
const mediaPayload = { mediaUrl: "https://example.invalid/chart.png" };
const error = "Structured message failed";
runWithModelFallbackMock.mockResolvedValueOnce({
result: {
payloads: [mediaPayload],
acceptedSessionSpawns: [{ runId: "run-child", childSessionKey: "agent:default:child" }],
meta: { agentMeta: { usage: { input: 10, output: 1 } } },
},
provider: "anthropic",
model: "claude-opus-4-8",
attempts: [],
});
resolveCronDeliveryPlanMock.mockReturnValue({
requested: true,
mode: "announce",
channel: "messagechat",
to: "test-target",
});
resolveCronPayloadOutcomeMock.mockReturnValue({
summary: undefined,
outputText: undefined,
synthesizedText: undefined,
deliveryPayload: mediaPayload,
deliveryPayloads: [mediaPayload],
deliveryPayloadHasStructuredContent: true,
hasFatalErrorPayload: false,
hasFatalStructuredErrorPayload: false,
embeddedRunError: undefined,
});
dispatchCronDeliveryMock.mockImplementationOnce(({ withRunSession }) => ({
result: withRunSession({ status: "error", error, deliveryAttempted: true }),
delivered: false,
deliveryAttempted: true,
cronRunSessionCleanupAttempted: false,
summary: undefined,
outputText: undefined,
synthesizedText: undefined,
deliveryPayloads: [mediaPayload],
}));
const result = await runCronIsolatedAgentTurn(makeIsolatedAgentParamsFixture());
expect(dispatchCronDeliveryMock).toHaveBeenCalledWith(
expect.objectContaining({
spawnOnlyHandoff: false,
deliveryPayloadHasStructuredContent: true,
}),
);
expect(result.status).toBe("ok");
expect(result.deliveryError).toBe(error);
});
it("does not mark empty successful cron-add completions as cron errors", async () => {
runWithModelFallbackMock.mockResolvedValueOnce({
result: {