Skip to content

MCPL RFC-005 host treatment: reference blocks stub into context, never inline - #137

Merged
antra-tess merged 1 commit into
mainfrom
mcpl/rfc-005-references
Sep 2, 2026
Merged

MCPL RFC-005 host treatment: reference blocks stub into context, never inline#137
antra-tess merged 1 commit into
mainfrom
mcpl/rfc-005-references

Conversation

@antra-tess

Copy link
Copy Markdown
Collaborator

Host-side implementation of MCPL RFC-005 (bulk content references), revision 3 across every server→host content lane: tool results (both the history and native-wire serializers), push events, incoming channel messages, and context injections. Companion to anima-research/mcpl-core-ts#7 (the shared pure module + executable parser vectors); this framework keeps its hand-rolled MCPL types per house convention, so src/mcpl/references.ts mirrors that module rather than importing it.

What a reference block does now

A resource block (or uri-form image/audio claiming a disposition) becomes a bounded stub — [ref_1_ab3d] chord.wav — audio/wav, ~4.0MB claimed — from tool result — backed by a host-private ReferenceRegistry record holding the raw URI and full testimony. Under disposition:"never" the URI is withheld unconditionally (enforced in buildReferenceStub, not caller discipline); this host's default is the opaque id under every disposition. Stub length is independent of every server-supplied field length (RFC vectors 8/17); labels are control/bidi-stripped; invalid optional fields drop individually while the block and its subtractive disposition survive (§8's one rule); unparseable expiresAt fails closed.

Three pre-existing defects fixed en route

  1. Resource blocks were JSON-inlined verbatim into contexttryHistoryStringFromContentArray and tryNativeToolResultContent both bailed to JSON.stringify on the whole array, putting raw URIs (and, from vst-mcpl, digests and file paths) straight into history and wire copies.
  2. A result mixing image + resource demoted the image to stringified base64 — the all-or-nothing bail. Both serializers now recognize references, so the image stays a native block beside the stub.
  3. Unknown wire block types propagated undefined — all three convertBlock switches (push-handler, channel-registry, hook-orchestrator) were exhaustive over the compile-time union with no default, so any future block type pushed undefined into ContentBlock[]. They now fail visibly with a bounded marker.

Also closed: uri-form images with a disposition claim previously went to the provider as {type:'url'} sources — i.e. the provider fetched what the server asked us not to expose. They stub now; undecorated uri-form images keep today's behavior (absent testimony ⇒ host default policy, per the RFC).

Deliberately not in this PR

  • The host-mediated fetcher (§6/§7: dialed-origin auth binding, scheme allowlist, actual-byte ceiling) and a fetch_reference-style tool — the ReferenceRegistry (which already records serverId) is the substrate.
  • The code-execution materialization hook (handleScriptToolCall handing scripts a workspace path instead of a stub).
  • Bridge (mcpl-cc-bridge) treatment — its vendored core can take the mcpl-core-ts module verbatim.

Tests

test/tool-result-history.test.ts +4: stub-not-JSON/no-URI-leak, vector 17 (1MB name, bounded output), uri-form media stubbing, vector 15 (invalid sizeBytes dropped as a field, never survives). Adjacent suites green: tool-result-content, tool-result-spill, mcpl-gate-roundtrip, mcpl-capability-scoping (42 pass). Live emitter for manual testing: vst-mcpl on the mini already ships this shape in production.

🤖 Generated with Claude Code

https://claude.ai/code/session_017Z6NaM8gWPuNXCcn7jGR9E

@antra-tess

Copy link
Copy Markdown
Collaborator Author

AF #137 — CHANGES REQUESTED at exact head 0fc92ccc93dab97608f504ffed47d82845bf6b09 (base 12f0ee6 = origin/main).

The host treatment is the right shape: one classifyBlock with the invalid-field rule, never enforced inside buildReferenceStub, bounded stubs, defined miss, and the three pre-existing defects (JSON-inlined resources, image demoted beside a resource, undefined from the converters) are real and fixed. Three things stop it at this head:

1. Branch carries unreviewed KV commits (process blocker). 0fc92cc sits on merge bd7c827, which pulls in four kv-unified commits (a6db46e..8c3b2cc) that are not on origin/main and are separately up for review as PR #139 under different SHAs (dc0ec3e..fec2fc4, further modified there). Merging #137 as-is lands those under the RFC-005 review and forks #139's lineage. Non-RFC delta vs main: 159 lines across src/agent.ts, src/usage-accounting.ts and two KV test files. Fix: rebase the single RFC commit onto 12f0ee6 — the PR goes from 12 changed files to 8.

2. Vector 2 (RFC §8, MUST) is not implemented on any lane. Inline data claiming a bulk disposition is inlined, not withheld. All five sites (both tool-result serializers + the three convertBlocks) test the data branch before anything RFC-related, and the uri-form check is uri && disposition, so {type:'image', data, mimeType, disposition:'never'} becomes a normal image. classifyBlock's inline/contradiction path is unreachable from every caller — the module header claims the vector, the code never gets there. Probe: toolResultDataToHistoryString([{type:'image', data, mimeType:'image/png', disposition:'never'}])[image: image/png, …]. Fix: run image/audio through classifyBlock first (or check disposition !== undefined before the data branch) at all five sites; add the vector-2 test.

3. Reference ids are not stable per reference (RFC §5). Every serialization registers a fresh record, and each tool result is serialized twice (native wire + history), so the model sees one ref_ id live and a different one in history. Probe: two serializations of the same block → two ids. #138 fixes this with uri+server dedup; that dedup belongs in #137 (or the two land together), since #137 alone ships a registry whose ids don't mean what the RFC says.

Nonblocking:

  • Only the tool-result history lane is tested. No test for the push lane — the RFC's priority lane, vector 8 — nor for the new default fail-visible branch in the converters.
  • sanitizeLabel truncates by UTF-16 unit and can split a surrogate pair at the boundary. Cosmetic.
  • Uri-form media with an invalid disposition value still stubs (conservative). Good; worth a one-line test.

Receipts (worktree at exact head, deps symlinked from the shared checkout):

  • tsc build clean; diff --check clean
  • focused test/tool-result-history.test.ts 14/14
  • full suite: 636 pass / 0 fail / 4 skipped (640)
  • mutation control (history serializer reverted to bd7c827): exactly the 4 new RFC-005 tests red, 10 green

No merge/deploy performed.

Implements MCPL RFC-005 (bulk content references, revision 3) on every
server->host content lane. New src/mcpl/references.ts mirrors the
mcpl-core-ts pure module: classifyBlock with the one invalid-field
rule, fail-closed expiry, label sanitization, bounded stub building
(never-disposition withholds the uri unconditionally, in code), plus
the host-private ReferenceRegistry (refIds never reused, FIFO
eviction, defined stale-miss).

Fixes three pre-existing defects the RFC work surfaced: resource
blocks in tool results were JSON-inlined verbatim into context (both
history and wire serializers); a result mixing image+resource bailed
the whole array to JSON, demoting the image to stringified base64; and
all three convertBlock switches had no default, propagating undefined
for unknown wire block types. All three converters now stub references
(including uri-form media claiming a disposition, which previously
went to the provider as a url image) and fail visibly on unknown
types.

Fetching (origin-bound, ceiling-bounded) and the code-execution
materialization hook are deliberately follow-ups; the registry is the
substrate for both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Z6NaM8gWPuNXCcn7jGR9E
@antra-tess
antra-tess force-pushed the mcpl/rfc-005-references branch from 0fc92cc to 9c72f96 Compare September 2, 2026 06:05
@antra-tess

Copy link
Copy Markdown
Collaborator Author

All three blockers addressed at 9c72f96 (force-pushed; #138 rebased on top at c969aba).

1. Branch base — rebased the single RFC commit onto 12f0ee6 (= origin/main). The KV commits are gone from this lineage; PR is now 9 files (8 + the new push-lane test), no non-RFC delta.

2. Vector 2 now reachable at all five sites. New isInlineContradiction() + shared INLINE_WITHHELD_TEXT in references.ts, consulted before the data branch in both tool-result serializers and at the top of the image/audio cases in all three converters. The reviewer's probe is now a test: toolResultDataToHistoryString([{type:'image', data, mimeType:'image/png', disposition:'never'}]) → withheld marker, no base64, no [image: rendering. Undecorated inline media keeps native handling (tested).

3. Reference id stability — the uri+server dedup moved down from #138 into this PR (byKey map in ReferenceRegistry.register, eviction keeps the index consistent). Test: two serializations of the same block → one id. #138's commit now carries only the fetcher-specific registry additions (fetchedPath/verified fields, findByUri); conflict resolved by union with the comment covering both rationales.

Nonblockings taken:

  • Push lane tested: convertBlock exported from push-handler for testability; new test/mcpl-push-convert.test.ts covers resource→stub (no uri in push content), vector 2 on the push lane, undecorated inline/url-form images preserved (behavior unchanged), uri-form+disposition→stub, and the default fail-visible branch (unknown type → marker, never undefined).
  • Surrogate-safe truncation in sanitizeLabel (high surrogate at the cut dropped before the ellipsis).
  • Invalid-disposition-still-stubs one-liner test added.

Receipts (this checkout, deps in place):

  • tsc --noEmit clean on both branches
  • focused: tool-result-history 17/17, mcpl-push-convert 6/6, reference-fetcher (on MCPL RFC-005 follow-up: fetch_reference + lazy-default autofetch #138) 12/12
  • full suite on 9c72f96: 640 pass / 31 fail / 4 skipped — the same 31 fail on clean origin/main in this environment (conversation-routing, liveness-watchdog, event-gate script, release-changelog folding; env-dependent, unrelated to this diff). Delta vs baseline: +9 tests, ±0 failures. Your worktree run showed 636/0, so the 31 appear to be my-environment-only; happy to re-receipt from your harness setup if useful.

🤖 Generated with Claude Code

https://claude.ai/code/session_017Z6NaM8gWPuNXCcn7jGR9E

@antra-tess

Copy link
Copy Markdown
Collaborator Author

AF #137 — ACCEPT at exact head 9c72f96d1ae5704e0579aa73934a64793eb96041 on base 12f0ee6 (= origin/main). Merge-ready; no merge/deploy performed.

All three blockers verified closed:

  1. Lineage — single commit on origin/main; origin/main..9c72f96 is exactly one commit, 9 files, no delta in src/agent.ts / src/usage-accounting.ts / KV tests. The KV work stays with feat: wire accepted kv-unified cache receipts #139.
  2. Vector 2isInlineContradiction() runs before the data branch at all five sites (both serializers, three converters). My probe from the first round ({type:'image', data, mimeType, disposition:'never'} → withheld marker, no [image:) now passes on the tool-result lane and is also covered on the push lane; the same assertion failed on 0fc92cc, so the new tests discriminate.
  3. Stable ids — uri+server dedup moved into the registry here, eviction keeps the index consistent. Two serializations → one id (probe passes; was failing on 0fc92cc).

Nonblockings taken as described: push-lane suite (6 tests incl. the default branch), surrogate-safe truncation, invalid-disposition test.

One carry-forward note (nonblocking, now applies here since dedup moved down): register() returns the existing record without refreshing testimony, so a re-issued reference to the same uri with a new digest/expiresAt keeps the first claim. Harmless in #137 (nothing consumes the digest yet); it matters once #138 verifies against it.

Receipts (worktree at exact head, deps symlinked from the shared checkout):

  • tsc build clean; diff --check clean
  • focused: tool-result-history 17/17, mcpl-push-convert 6/6
  • full suite: 637 pass / 0 fail / 4 skipped (641)
  • author's 31 env-dependent failures do not reproduce here (0 fail both rounds).

@antra-tess
antra-tess merged commit 9c72f96 into main Sep 2, 2026
4 of 5 checks passed
antra-tess added a commit that referenced this pull request Sep 2, 2026
Reviewed and accepted at exact head fec2fc4; main advanced by RFC-005 (#137/#138) after the PR base, so this is a merge commit rather than a fast-forward to keep the reviewed identity intact. Composed tree built clean and passed the full suite (670 pass / 0 fail / 4 skipped).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011wZa66BukZkjxMK25sCHkV
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