Skip to content

Complete oversized tool-result spill: safe 5k default, error paths, all result paths (#89) - #91

Merged
antra-tess merged 2 commits into
mainfrom
issue-89/tool-result-spill-completion
Aug 6, 2026
Merged

Complete oversized tool-result spill: safe 5k default, error paths, all result paths (#89)#91
antra-tess merged 2 commits into
mainfrom
issue-89/tool-result-spill-completion

Conversation

@antra-tess

Copy link
Copy Markdown
Collaborator

Addresses #89 (Mythos's accidental 42k ingest). Completes the f231bbf spill mechanism rather than reimplementing it.

What changes

Safe durable default. The inline cap is no longer strategy.maxMessageTokens * 4 (frequently tens of thousands of chars, or unbounded when the strategy declares no maxMessageTokens — the exact hole the incident went through). Cap resolution is now:

  1. agent_settings tool_result_inline_max_chars hot override — wins outright, ephemeral (unchanged);
  2. new FrameworkConfig.toolResultInlineMaxChars — durable residence cap, validated >= 1000 at create();
  3. house default 5000.

Non-override values are clamped down to the strategy bound when that is smaller (a message must still fit maxMessageTokens); the explicit override escapes the clamp, as before.

Provenance. agent_settings get now reports tool_result_inline_max_chars_effective and _source (agent-settings-override | framework-config | default, with a (strategy-clamped) marker). Restart drops the override and keeps the configured cap — pinned by test.

One policy on all text-bearing paths.

  • Error results now spill exactly like successes (previously skipped by the spill loop — a giant error string went inline whole). isError preserved; history and live wire byte-identical.
  • The complete-while-waiting guard path ('complete' firing during waiting_for_tools) uses the same bounded builder (previously unbounded serialize).
  • Background-script wake payloads use the resolved cap: the agent override now applies there, and an unknown agent gets the bounded default instead of undefined → unbounded.
  • The direct-Agent-API path (Agent.infer's buildToolResultMessages, no framework call sites but public) is bounded at the house default for successes and errors.
  • Script-inner tool results keep their 5MB protocol-safety cap — the script environment is the spill; filtering big results in code is its purpose.

Image safety. Native image blocks are never character-truncated (unchanged); an oversized text block riding alongside an image now points its truncation notice at the spill file.

Explicit no-workspace fallback. The plain-truncation notice now states no writable workspace, full content not retained instead of looking identical to a recoverable truncation.

File lifecycle (issue item 7), documented on the config field: deterministic tool-results/<date>-<callId>.txt names, overwrite on collision, retained until the workspace owner deletes them — never auto-GC'd, no silent 'temporary'.

Behavior change, deliberately

Fleet agents on strategies with large/absent maxMessageTokens will start seeing 5000-char previews + spill files where they previously got the full blob inline. That is the house request in #89. Residences that want more set toolResultInlineMaxChars durably or lift per-agent via agent_settings.

Out of scope (stays open in #89 discussion)

  • Item 5, adapter pre-allocation/byte-bounded reads: this PR protects model context after arrival; transport/allocation bounds are a per-adapter audit.
  • Host/recipe passthrough: needs a connectome-host PR exposing toolResultInlineMaxChars through recipe composition, after this lands in an AF release (will open dependency-separated, host#48-style).

Tests

New test/tool-result-spill.test.ts (9): default-5000 spill with full-content file recovery, durable-config cap + provenance, override-wins/reset-restores, giant-error spill with byte-identity, success byte-identity, explicit no-workspace fallback, image-intact + spill-file-referencing wire copy, restart semantics, invalid-config rejection.

Full suite: 539/540 pass, 0 fail (1 pre-existing skip). The mcpl-awareness-barrier timing flake surfaced once under full-suite load and passes in isolation and on re-run — pre-existing cluster, unrelated.

🤖 Generated with Claude Code

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

…rrors, all paths

Completes the f231bbf spill mechanism per issue #89 (motivated by Mythos
unintentionally ingesting a ~42k search result: the old default cap was
strategy.maxMessageTokens * 4 — often tens of thousands of chars, or NO cap
at all when the strategy declares no maxMessageTokens).

- House-safe default inline cap: 5000 chars (DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS),
  applied even when the strategy declares no bound.
- Durable FrameworkConfig.toolResultInlineMaxChars (>= 1000): residence-level
  cap that survives restarts; clamped down to the strategy bound when that is
  smaller. The ephemeral agent_settings override still wins outright.
- Provenance: agent_settings get now reports tool_result_inline_max_chars_effective
  and _source (agent-settings-override | framework-config | default, with a
  strategy-clamped marker).
- Error results follow the same spill policy as successes — a giant error
  string spills to the same tool-results/ file with a bounded preview,
  byte-matched between history and live wire.
- Alternate paths covered: the complete-while-waiting guard path now uses the
  same bounded builder; background-script wake payloads use the resolved cap
  (override applies; unknown agent gets the bounded default, never unbounded);
  the direct-Agent-API path is bounded at the house default for successes and
  errors.
- Native image path: image blocks stay byte-intact; an adjacent oversized
  text block's truncation notice now references the spill file.
- No-writable-workspace fallback states explicitly that full content was not
  retained.
- File lifecycle documented: deterministic <date>-<callId>.txt names,
  overwrite on collision, retained until the workspace owner deletes them.

Out of scope (tracked in #89): adapter-level pre-allocation/byte-bounded
reads; connectome-host recipe passthrough (separate Host PR after release).

Refs #89 (item 5, adapter byte-bounds, stays open)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@antra-tess

Copy link
Copy Markdown
Collaborator Author

CI note: both OS jobs fail in Type-check on the pre-existing fresh-install dependency drift — the Chronicle JsStore type split (getStateUpdateJson/updateStateStrategy missing) and the Membrane 'retrying' stream-event type — none of it in files this PR touches. Identical failures on the last three main pushes (runs 31069302355, 30966318227, 30966245282), and meganuridae-codex reproduced the same cluster on untouched origin/main for AF #87. Local build + full suite against the repo's committed lock: 539/540 pass, 0 fail.

Review finding on #91 (opus-rev, confirmed by repro): when a writable
workspace exists but the spill write fails (mount maxFileSize, storeBlob
failure, ...), spillOrTruncate fell through to the no-workspace return —
the agent was told 'no writable workspace, full content not retained',
teaching them their residence lacks a capability it actually has, and the
catch swallowed the error with no observability.

Now: a distinct inline notice naming the attempted path and the (bounded)
failure reason, a new 'tool:spill_failed' trace event, and a console error.
The genuine no-workspace fallback is unchanged.

Also pins the strategy-clamp branch (review finding #4): maxMessageTokens
1000 → effective cap 4000, source 'default (strategy-clamped)' — the branch
that decides the cap for every resident on a bounded strategy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@antra-tess
antra-tess merged commit 7a3e081 into main Aug 6, 2026
0 of 2 checks passed
antra-tess pushed a commit that referenced this pull request Aug 6, 2026
Carries today's merged work: #91 oversized tool-result spill completion,
#94 durable resident-owned inline cap (hard-clamped to the strategy
bound), #88 refusal-reaction baseline export, #86 removal of the dead
context/afterInference surface, #87 MCPL manifest freshness, #98
physical-window projection for continuation rounds.

Minor (not patch) because 0.x puts breaking changes in the minor and this
release both removes a public method from an exported class
(McplServerConnection.sendAfterInference, #86) and changes fleet default
behavior (tool results now spill at 5000 chars by default, #91).

Dependency floors raised to fix a build that only worked against local
symlinks:
- chronicle ^0.3.0 — context-manager 0.6.3 requires ^0.3.0, so ^0.2.2
  installed TWO chronicles and AF handed a 0.2.x JsStore to APIs wanting
  the 0.3.x one. Single copy now.
- membrane ^0.5.78 — AF consumes the 'retrying' yielding-stream event,
  which existed only in unpublished membrane commits until 0.5.78.

DEPLOY NOTE: chronicle 0.3.0 open-writes a store format 0.2.x cannot
reopen. Take cold backups before upgrading any residence.

Verified against the real published packages (not local symlinks): single
chronicle 0.3.0, cm 0.6.3, membrane 0.5.78; tsc clean; build clean;
539 tests, 538 pass, 0 fail, 1 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant