fix: contain whole-response tool wrappers emitted as prose - #142
Conversation
Anarchid
left a comment
There was a problem hiding this comment.
🟢 CLEAR
Reviewer: Codex (GPT-5.6 Sol)
Reviewed head: dce7eacced743ea17409f9b48f0503c26db3048d
No material findings. The guard is fail-closed at the whole-response boundary, limits authority to the exact tool set sent on the inference, preserves genuine structured-tool history across retries/restarts, and prevents wrapper prose from reaching continuity, module speech, authoritative channel delivery, or speculative outgoing streaming. The generation-ownership changes also close the reviewed ephemeral disposal seams without letting late tool events act on a removed or replacement agent.
Tooling results:
./node_modules/.bin/tsc --noEmit— passed.npm run build— passed.node --import tsx --test test/framework.test.ts— passed (1/1 test file).node --import tsx --test test/tool-wrapper-prose-guard.test.ts test/tool-wrapper-prose-guard-integration.test.ts— passed (2/2 test files).node --import tsx --test --test-concurrency=1 test/ephemeral-settle.test.ts test/deferred-wake-integrity.test.ts test/channel-lifecycle-provenance.test.ts test/event-gate-passthrough.test.ts test/event-gate-policy-mutation.test.ts test/tool-wrapper-prose-guard-integration.test.ts— passed (6/6 test files).git diff --check 635f540c9fc3f2206d7369e23f8da656c42cb24b...HEAD— passed.npm cicould not be used because the repository has no lockfile. A normal-sandboxnpm install --ignore-scripts --no-package-lock --no-audit --no-fundcould not complete with restricted registry access, so checks used the existing workspace dependency installation plus the mapped local Chronicle, Context Manager, and Membrane packages.- The repository-wide compiled test command was attempted both normally and with
--test-concurrency=1; in this sandbox it stalled when reaching the unrelatedcode-execution.test.jsafter the first 9 files passed, and was interrupted. The PR's GitHub CI matrix is green; the changed guard and its immediate lifecycle/EventGate dependencies were exercised locally as listed above.
Verdict: clear to proceed on the inspected head. No blockers or non-blocking follow-up from this review; confidence is limited only by the sandbox-specific full-suite stall described above.
— Reviewed by GPT-5.6 Sol via OpenAI Codex.
slimepriestess
left a comment
There was a problem hiding this comment.
Read against head dce7eac; CI green, Anarchid's Codex pass already clear. Approving, with three notes for the record rather than asks.
The guard core is right and narrow. detectKnownToolWrapperProse matches only when every visible block is text (signed thinking allowed), the joined body is exactly one wrapper shape, and the tag names a tool from the set sent on that inference. The unit test's negative table covers the cases I'd have worried about (quoted, fenced, prefixed, mixed, unclosed, mismatched close, structured tool_use, empty tool set). Integration test 913 pins the request-disallowed case, which is the one that matters for authority. The containment path stores only the fixed receipt with the tool name in metadata, and allText becomes empty so nothing reaches speech or the outgoing surfaces. Good.
Note 1, operator-facing cost worth stating where the switch lives. With toolWrapperProseGuard: true, proseStream is never constructed for that resident, so speak-while-acting, typing preview, and any streamed outgoing delivery are off for every turn, not just wrapper turns; ordinary prose arrives at turn completion (test 976). The docs page says so; the recipe-side field comment in host #115 says only "default-off containment". Since the recipe is where an operator flips it, a clause there ("disables streamed prose delivery for this agent") would save someone a puzzled hour. Not blocking.
Note 2, single-generation names verified safe for current callers. usedEphemeralAgentNames is never released on successful disposal (only on creation failure), so a name can't recur for the framework's lifetime. I checked the two things that could bite: conversation forks carry a generation counter in the name (forkAgentName(channelId, generation)), so an idle-TTL close followed by a fresh message spawns a new name, no collision; and the host's subagent module names spawns spawn-<name>-<Date.now()> and forks <name>-d<depth>[-retry<n>]-<Date.now()>. The spawn site's retry loop recomputes only the timestamp, so a retry landing in the same millisecond as a failed run would now throw where it previously overwrote; that's a host-side nit for its own retry naming (the fork site already carries retry${attempt}), not this PR's problem.
Note 3, scope. The title is the guard; roughly two thirds of the framework diff is ephemeral/conversation generation ownership (generation tickets, single-generation names, late-event discard by Agent identity, disposal owning terminal typing/outgoing close). I spot-read the disposal path and the generationLost check and they hang together; I did not re-run the red mutation controls the body cites. Anarchid's pass covered that half, so I'm counting on it rather than duplicating it.
Two conflicts against the 7 commits main gained since 7324521 (anima-research#141 turn-provenance batched wakes, anima-research#142, anima-research#143, 0.12.0), both keep-both: - driveStream pre-stream: main's ephemeral-disposal ownership check runs first, then this branch's dispositions injection for the subconscious. - driveStream finally: main's `frameReachedTerminal` guard on the deferred flush, over this branch's per-agent `drainDeferredFor`. The auto-merged channel-incoming wake loop keeps anima-research#141's `counterparty` / `addressed` provenance under this branch's `targetAgents` routing. Bug fixed while reviewing: `getAllTools()` pushed TUNE_OUT_TOOL onto the array `getChannelTools()` returns, which is the registry's module constant CHANNEL_TOOL_DEFINITIONS — every call appended another `tune_out` to the shared list. The array is now copied before the push; tune-out-e2e asserts the resident's board carries `tune_out` exactly once across repeated calls. Verified against a build of context-manager#54 (main 92f5a65 + ef11162): tsc clean, suite 725 / 0 fail / 4 skipped, tune-out suites 22/22. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bm7wvGLvxF6RGAKCRB6MH7
Summary
Adds default-off
toolWrapperProseGuardfor the failure mode where a provider emits a complete textual rendering of an available tool invocation instead of structuredtool_use.On an exact whole-visible-response match, the framework executes nothing, publishes nothing, omits the wrapper from assistant continuity, preserves raw provider tracing, and records only
[tool-boundary] No tool was called.as a system boundary. Unknown tools, quotation/code/mixed prose, genuine structured calls, and ordinary prose remain unchanged.Lifecycle safety
Evidence
Exact head:
dce7eacced743ea17409f9b48f0503c26db3048d25/25721 total / 717 pass / 0 fail / 4 skipchunk -> stop typing -> outgoing completeBoundaries
Default off. This PR does not enable the guard for any resident, alter any resident store, or authorize deployment/restart. Guarded turns deliberately do not expose speculative outgoing preview/TTS before whole-response classification; authoritative ordinary delivery occurs at turn completion.