From 368cc975df21412702571c2deba6d0de0a95ae34 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Tue, 29 Sep 2026 17:02:53 -0700 Subject: [PATCH] fix(subagent): count mailbox spill remaining as omitted body --- src/subagent/fleet-dry-drive.ts | 19 ++++-- src/subagent/mailbox-mail-drive.test.ts | 79 +++++++++++++++++++++++++ src/subagent/mailbox-mail-drive.ts | 10 +++- 3 files changed, 101 insertions(+), 7 deletions(-) diff --git a/src/subagent/fleet-dry-drive.ts b/src/subagent/fleet-dry-drive.ts index 502774841..3d39e938f 100644 --- a/src/subagent/fleet-dry-drive.ts +++ b/src/subagent/fleet-dry-drive.ts @@ -211,10 +211,13 @@ function clipDigestSection(text: string): string { return `${text.slice(0, MAILBOX_DIGEST_SECTION_CHARS - 1).trimEnd()}…`; } -function mailboxReportSpillNotice(text: string, uri: string): string { +function mailboxSpillNotice(text: string, uri: string): string { + // Digest inlines the notice only, so remaining is the omitted body — not + // length minus a 2048-char prefix that was never kept. A short spill that + // claimed "0 more chars omitted" looks complete and the parent skips read_file. return truncationNotice({ maxChars: MAILBOX_DIGEST_SECTION_CHARS, - remaining: Math.max(0, text.length - MAILBOX_DIGEST_SECTION_CHARS), + remaining: text.length, fullLength: text.length, contentType: "text/plain", uri, @@ -287,8 +290,14 @@ export async function digestCollectedReport( report.report === undefined ? undefined : reportUri !== undefined - ? mailboxReportSpillNotice(report.report, reportUri) + ? mailboxSpillNotice(report.report, reportUri) : await clipField(report.report, report.agent_id, "report"); + const errorInline = + report.error === undefined + ? undefined + : errorUri !== undefined + ? mailboxSpillNotice(report.error, errorUri) + : clipDigestSection(report.error); return { agent_id: report.agent_id, status: report.status, @@ -300,9 +309,7 @@ export async function digestCollectedReport( ...(blockers !== undefined ? { blockers } : {}), ...(reportInline !== undefined ? { report: reportInline } : {}), ...(reportUri !== undefined ? { report_uri: reportUri } : {}), - ...(report.error !== undefined - ? { error: clipDigestSection(report.error) } - : {}), + ...(errorInline !== undefined ? { error: errorInline } : {}), ...(errorUri !== undefined ? { error_uri: errorUri } : {}), ...(report.hint !== undefined ? { hint: report.hint } : {}), ...(report.provider_failure === true ? { provider_failure: true } : {}), diff --git a/src/subagent/mailbox-mail-drive.test.ts b/src/subagent/mailbox-mail-drive.test.ts index 4d94862f1..d357c91b1 100644 --- a/src/subagent/mailbox-mail-drive.test.ts +++ b/src/subagent/mailbox-mail-drive.test.ts @@ -5,6 +5,7 @@ import { driveMailboxMail, latchMailboxMailDrive, MAILBOX_MAIL_WAKE_PREFIX, + mailboxMailErrorUriHint, mailboxMailReportUriHint, mailboxMailWakeLine, occupancyShouldYieldWait, @@ -14,6 +15,7 @@ import { digestCollectedReports, fleetDrySpillKey, FLEET_DRY_REPORT_CHARS, + MAILBOX_DIGEST_SECTION_CHARS, type FleetDryMailboxRecord, } from "./fleet-dry-drive.js"; import { @@ -125,6 +127,22 @@ describe("buildMailboxMailPrompt", () => { expect(prompt).toContain(uri); }); + test("names error_uri with a read_file truncation notice", () => { + const uri = `tool-output:///${fleetDrySpillKey("worker-1", "error")}`; + const prompt = buildMailboxMailPrompt([ + { + agent_id: "worker-1", + status: "failed", + error_uri: uri, + }, + ]); + expect(prompt).toContain(mailboxMailErrorUriHint()); + expect(prompt).toContain( + "use read_file with that URI (offset/limit supported)", + ); + expect(prompt).toContain(uri); + }); + test("does not paste a duplicate agent_id", () => { const prompt = buildMailboxMailPrompt([ { agent_id: "w1", status: "done" }, @@ -163,6 +181,10 @@ describe("digestCollectedReports", () => { "use read_file with that URI (offset/limit supported)", ); expect(digest?.report).not.toContain("SECRET_FINDINGS_BODY"); + expect(digest?.report).toContain( + `${ENVELOPE_REPORT.length.toLocaleString()} more chars omitted here`, + ); + expect(digest?.report).not.toMatch(/— 0 more chars omitted/); expect( new TextDecoder().decode( store.blobs.get(fleetDrySpillKey("worker-1", "report"))?.bytes ?? @@ -222,6 +244,63 @@ describe("digestCollectedReports", () => { expect(digest?.report).toContain("SECRET_FINDINGS_BODY"); expect(digest?.summary).toBe("Shipped the digest."); }); + + test("spill notice remaining is the omitted body when no prefix is kept", async () => { + const store = fakeBlobStore(); + const short = "short spilled body"; + const [digest] = await digestCollectedReports( + [{ agent_id: "short", status: "done", report: short }], + store.writeBlob, + ); + expect(short.length).toBeLessThan(MAILBOX_DIGEST_SECTION_CHARS); + expect(digest?.report).toContain( + `${short.length.toLocaleString()} more chars omitted here`, + ); + expect(digest?.report).not.toMatch(/— 0 more chars omitted/); + expect(digest?.report).not.toContain(short); + expect(digest?.report_uri).toBe( + `tool-output:///${fleetDrySpillKey("short", "report")}`, + ); + }); + + test("oversized spilled report remaining is the full body, not length minus the digest cap", async () => { + const store = fakeBlobStore(); + const original = "x".repeat(MAILBOX_DIGEST_SECTION_CHARS + 80); + const [digest] = await digestCollectedReports( + [{ agent_id: "long", status: "done", report: original }], + store.writeBlob, + ); + expect(digest?.report).toContain( + `${original.length.toLocaleString()} more chars omitted here`, + ); + expect(digest?.report).not.toContain( + `${(original.length - MAILBOX_DIGEST_SECTION_CHARS).toLocaleString()} more chars omitted here`, + ); + }); + + test("spilled error names error_uri with truncation-notice language", async () => { + const store = fakeBlobStore(); + const error = "provider boom"; + const [digest] = await digestCollectedReports( + [{ agent_id: "fail", status: "failed", error }], + store.writeBlob, + ); + const errorUri = `tool-output:///${fleetDrySpillKey("fail", "error")}`; + expect(digest?.error_uri).toBe(errorUri); + expect(digest?.error).toContain("[output truncated"); + expect(digest?.error).toContain(errorUri); + expect(digest?.error).toContain( + `${error.length.toLocaleString()} more chars omitted here`, + ); + expect(digest?.error).not.toMatch(/— 0 more chars omitted/); + expect(digest?.error).not.toContain("provider boom"); + expect( + new TextDecoder().decode( + store.blobs.get(fleetDrySpillKey("fail", "error"))?.bytes ?? + new Uint8Array(), + ), + ).toBe(error); + }); }); describe("driveMailboxMail", () => { diff --git a/src/subagent/mailbox-mail-drive.ts b/src/subagent/mailbox-mail-drive.ts index 070fbc3b2..2c9af6215 100644 --- a/src/subagent/mailbox-mail-drive.ts +++ b/src/subagent/mailbox-mail-drive.ts @@ -35,6 +35,11 @@ export function mailboxMailReportUriHint(): string { return "report_uri names a spilled full report — use read_file with that URI (offset/limit supported) to see the rest."; } +/** Names error_uri with truncation-notice language so the parent may fetch it. */ +export function mailboxMailErrorUriHint(): string { + return "error_uri names a spilled full error — use read_file with that URI (offset/limit supported) to see the rest."; +} + /** * Whether inbound text is occupancy's mailbox mail. Internal runtime→agent * traffic — the fleet board already owns worker status and the payload is @@ -85,13 +90,16 @@ export function occupancyShouldYieldWait( } export function buildMailboxMailPrompt< - T extends { agent_id: string; report_uri?: string }, + T extends { agent_id: string; report_uri?: string; error_uri?: string }, >(reports: readonly T[]): string { const unique = dedupeByAgentId(reports); const lines = [mailboxMailWakeLine()]; if (unique.some((report) => report.report_uri !== undefined)) { lines.push(mailboxMailReportUriHint()); } + if (unique.some((report) => report.error_uri !== undefined)) { + lines.push(mailboxMailErrorUriHint()); + } lines.push(JSON.stringify(unique)); return lines.join("\n"); }