feat(compression): tool-prose hoist fallback rung - #106
antra-tess wants to merge 4 commits into
Conversation
Long prose in a private-reasoning tool argument (skip_reply.reason, think.content) gets every L1 compression over that history refused reasoning_extraction, independent of content (sill, 2026-09-19: 2-3KB diaries in skip_reply.reason; canonical AND source-only refused on every chunk; tea filler in the same field refuses; <=~100 chars passes). compressionToolProseFallback retries a refused canonical request once with each such argument moved into a call to a note-taking tool the agent really has (agent-framework `journal`), as its own round before the original call, and gives the source-only final rung the same rewrite if it also refuses. The target must be a real, declared tool: the summarizer is the agent and will imitate whatever shape it sees itself use. Validated with the built code on sill's logged refused requests: 12/13 pass (canonical 70-143k tokens and source-only); the 13th carried an operator marker string that is itself a trigger. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rungs An offline drain that silently drops the recipe's refusal-fallback rungs reproduces the live refusals it was stopped to get away from. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…as journal calls The diary carrier blocks PRIMARY turns as well as compression (sill, 2026-09-20): five skip_reply reasons of 400-570 chars in the raw tail got every primary wake refused reasoning_extraction. On the exact refused request, shortening them passes, hoisting them into journal rounds passes, and removing all 174 fleet-watch alert blocks does not. A restart cannot clear it: the reasons sit inside the tail, and each new turn adds another. primaryToolProseHoist applies the existing rewrite on every primary compile, recent turns included. Always-on so the render is deterministic (stable cached prefix); a view only, stored history is untouched; skipped unless the target tool is declared. Inserted ids are now per-call so they do not shift when earlier hoisted calls fold out of the window. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Not wanted: the diaries are rewritten once in the resident's stored history instead of on every render. Returns the branch to the compression-only fallback rung. This reverts commit f0c3409. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Anarchid
left a comment
There was a problem hiding this comment.
🟠 NEEDS ATTENTION
Reviewer: Codex (GPT-5.6 Sol)
Reviewed head: 937472bd9542158a6d57999bb33af7ee7641a8d9
1. The source-only hoist retries after provider errors, not just refusals
src/strategies/autobiographical.ts:6233
// Source-only final refused too...
if (!fallbackResponse && sourceOnlyFallbackRequest) {
const hoistedSourceOnly = this.toolProseHoistedRequest(sourceOnlyFallbackRequest);
if (hoistedSourceOnly) {
await runToolProseRung(hoistedSourceOnly, 'source-only-tool-prose-hoist', ...);
}
}fallbackResponse is also empty when source-only-final throws a provider/transport error or returns another non-valid, non-refusal disposition. The new rung therefore makes another paid request after an infrastructure failure even though rewriting tool prose cannot address that failure. This contradicts both the comment/body's refusal-only contract and the nearby statement that provider errors do not escalate.
I reproduced the sequence with canonical and canonical-hoist refusals followed by a thrown { type: "server_error" } from source-only-final. The reviewed head still made a fourth call:
{"count":4,"names":["skip_reply","journal+skip_reply","skip_reply","journal+skip_reply"]}Track the source-only assessment/outcome and run source-only-tool-prose-hoist only when that attempt specifically ended in refusal. Add a provider-error regression that expects three calls and no source-only-tool-prose-hoist attempt; the same guard should exclude empty/truncated/incomplete source-only results unless there is evidence that the carrier rewrite applies to them.
Tooling results
git diff --check aae303f..HEAD— passed.npm ci --ignore-scripts --prefer-offline— installed exact lockfile dependencies from cache.npm run build— passed (TypeScript compile).node --test dist/test/tool-prose-hoist.test.js— passed.node --test dist/test/compression-recall-curve.test.js— passed.node --test dist/test/split-stitch-l1.test.js— passed.- Direct source-only provider-error reproduction — demonstrated the unintended fourth call shown above.
- All five exact-head GitHub checks are green.
Verdict: the core rewrite and refusal ordering are otherwise well covered, but the new source-only rung should be gated on the failure class it can actually repair before merge.
— Reviewed by GPT-5.6 Sol via OpenAI Codex.
slimepriestess
left a comment
There was a problem hiding this comment.
Reviewed at exact head 937472b (four commits on aae303f, 13 behind main, clean merge). Read as one feature with agent-framework #161 (the journal tool), connectome-host #134 (the recipe key) and #133.
The mechanism is right. The canary record in tool-prose-hoist.ts is the kind of evidence this rung needs, and the design follows from it: only a tool the agent really has, only on a canonical refusal, ahead of the recall variants (which cannot dodge a carrier that lives in the chunk's own calls), fromTools required and never "all", nothing dropped, ids derived from the original so the hash is stable, the rung part of the quarantine regime so enabling it earns a fresh bounded attempt. The pure rewrite keeps assistant/user alternation (the inserted round precedes the whole original message) and leaves unpaired calls alone. With the option unset, bytes and quarantine identity are pinned unchanged.
Sol's finding is real, and it blocks. The source-only hoist was keyed on !fallbackResponse, which is also true after source-only-final throws or comes back truncated or empty, so an infrastructure failure bought a fourth paid call the rewrite cannot help — contradicting the comment two lines up. Reproduced on this head: refusal → hoisted refusal → thrown server_error → four calls.
The fix is one variable and one condition, built and verified, stacked on this head, ready to lift:
slimepriestess/context-manager@9d19f75 — compare · git fetch https://github.com/slimepriestess/context-manager.git fix/tool-prose-source-only-gate && git cherry-pick 9d19f75c55c8fad3ae48336a1b310b0aaa759ed4
Track how the source-only attempt ended (sourceOnlyOutcome: the assessment's outcome, or provider_error from the catch) and run source-only-tool-prose-hoist only when it is 'refusal' — the same gate the canonical hoist already had. New test case GATE: refusal, refusal, thrown server_error → three calls and no hoisted fourth; refusal, refusal, max_tokens → the same.
Receipts (npm ci, Node 22):
this head tool-prose-hoist 10/10 · full suite 806/806
this head + 9d19f75 tool-prose-hoist 11/11 · full suite 807/807
this head + the new test only 10/11 — GATE red: four calls
Two non-blocking notes. toolProseHoistOptions() is called twice in the quarantine-record spread; harmless, one const would read cleaner. And the drain script's passthrough list grew the four split-fallback keys alongside this one — good catch, they were missing, but it's worth a line in the changelog fragment since it changes what drain-autobiographical honours.
Changes requested for the gate; approve on the lift.
Why
Long prose in a private-reasoning tool argument (
skip_reply.reason,think.content) makes replayed history read as a reasoning trace, and the L1 memory-write is refusedreasoning_extractionregardless of content (sill, 2026-09-19 — every chunk, canonical and source-only; canary record insrc/tool-prose-hoist.ts). See anima-research/agent-framework#161 for the finding and thejournaltool.What
Opt-in
compressionToolProseFallback: { intoTool, fromTools, field?, result?, minChars? }.intoTooljust before the original call and leaves a short stub. Nothing the agent wrote is dropped. Deterministic ids → stable request hash.intoToolis among the declared tools and at least one argument qualifies — the summarizer is the agent; never show it a tool it does not have, never burn an identical retry.fromToolsis required and never defaults to "all": moving asend_messagebody into a journal would author a false memory.Validation
Built code + the real agent-framework
journaldefinition against sill's logged refused requests: 12/13 pass (8 canonical 70–104k, 3 × 131–143k, 2 source-only). The 13th carried an operator marker string in a 76-charskip_reply.reasonthat is itself a trigger (underminChars); hoisting it too, or rewording it, passes.Tests:
test/tool-prose-hoist.test.ts(10). Full suite 806/806.🤖 Generated with Claude Code