Summary
execution-projection.ts documents capText (in packages/mcp/src/tools/shared.ts) as a "hard char budget" primitive: "the result is bounded for ANY argument combination." The implementation didn't honor that. It sliced text to exactly budget characters, then appended a truncation marker (e.g. …[truncated 48000 chars — open https://... for the full value]) AFTER the slice — so the returned string was budget + marker.length characters, not budget.
Reproduction
capText("x".repeat(50000), 2000, "https://app.sapiom.ai/runs/exec-123/steps/4").length
// -> 2094, not 2000
Verified with a 240-case brute-force sweep across budgets (including the small ones actually used in production: PREVIEW_BUDGET = 2000, DEFAULT_FIELD_BUDGET = 32000) and a range of text lengths — the old implementation violated the bound in every case where truncation kicked in.
Impact
Low in this specific call site today: CEILING in the test suite is 250,000 chars, and the marker overrun is on the order of tens to ~100 chars per field, so it's very unlikely to blow the overall response ceiling in practice. But it directly contradicts the module's own documented "hard budget... bounded for ANY argument combination" guarantee, and any future caller (or a lowered CEILING/per-field budget) relying on that documented guarantee would be silently wrong.
Suggested fix
Since the marker's own length depends on the dropped-char count, which depends on where the text is sliced, which depends on the marker's length, this needs a small converging loop (shrink the slice point until slice + marker fits within budget) rather than a single subtraction, plus a hard .slice(0, budget) backstop for degenerate tiny budgets. I have a fix ready with a regression test suite (shared.test.ts, which didn't exist for this module before) that fails 4/6 cases against the current implementation and passes 6/6 against the fix. Happy to open a PR.
Summary
execution-projection.tsdocumentscapText(inpackages/mcp/src/tools/shared.ts) as a "hard char budget" primitive: "the result is bounded for ANY argument combination." The implementation didn't honor that. It sliced text to exactlybudgetcharacters, then appended a truncation marker (e.g.…[truncated 48000 chars — open https://... for the full value]) AFTER the slice — so the returned string wasbudget + marker.lengthcharacters, notbudget.Reproduction
Verified with a 240-case brute-force sweep across budgets (including the small ones actually used in production:
PREVIEW_BUDGET = 2000,DEFAULT_FIELD_BUDGET = 32000) and a range of text lengths — the old implementation violated the bound in every case where truncation kicked in.Impact
Low in this specific call site today:
CEILINGin the test suite is 250,000 chars, and the marker overrun is on the order of tens to ~100 chars per field, so it's very unlikely to blow the overall response ceiling in practice. But it directly contradicts the module's own documented "hard budget... bounded for ANY argument combination" guarantee, and any future caller (or a loweredCEILING/per-field budget) relying on that documented guarantee would be silently wrong.Suggested fix
Since the marker's own length depends on the dropped-char count, which depends on where the text is sliced, which depends on the marker's length, this needs a small converging loop (shrink the slice point until slice + marker fits within budget) rather than a single subtraction, plus a hard
.slice(0, budget)backstop for degenerate tiny budgets. I have a fix ready with a regression test suite (shared.test.ts, which didn't exist for this module before) that fails 4/6 cases against the current implementation and passes 6/6 against the fix. Happy to open a PR.