Skip to content

feat(compression): tool-prose hoist fallback rung - #106

Open
antra-tess wants to merge 4 commits into
mainfrom
feat/compression-tool-prose-fallback
Open

antra-tess wants to merge 4 commits into
mainfrom
feat/compression-tool-prose-fallback

Conversation

@antra-tess

Copy link
Copy Markdown
Contributor

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 refused reasoning_extraction regardless of content (sill, 2026-09-19 — every chunk, canonical and source-only; canary record in src/tool-prose-hoist.ts). See anima-research/agent-framework#161 for the finding and the journal tool.

What

Opt-in compressionToolProseFallback: { intoTool, fromTools, field?, result?, minChars? }.

  • Ladder: canonical → tool-prose-hoist (only on a canonical refusal, ahead of the recall variants, which cannot dodge a carrier that lives in the chunk's own tool calls) → recall variants → source-only-final → source-only-tool-prose-hoist → split-stitch → quarantine.
  • The rewrite moves each qualifying argument into its own round of intoTool just before the original call and leaves a short stub. Nothing the agent wrote is dropped. Deterministic ids → stable request hash.
  • Skipped unless intoTool is 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.
  • fromTools is required and never defaults to "all": moving a send_message body into a journal would author a false memory.
  • The normalized options are part of the quarantine regime, so enabling it gives already-quarantined chunks a fresh bounded attempt without a manual clear.
  • Off by default; with it unset, canonical bytes, request hashes and quarantine identity are unchanged (pinned by test).

Validation

Built code + the real agent-framework journal definition 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-char skip_reply.reason that is itself a trigger (under minChars); hoisting it too, or rewording it, passes.

Tests: test/tool-prose-hoist.test.ts (10). Full suite 806/806.

🤖 Generated with Claude Code

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>
antra-tess and others added 3 commits September 19, 2026 10:32
…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 Anarchid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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 slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_errorfour calls.

The fix is one variable and one condition, built and verified, stacked on this head, ready to lift:

slimepriestess/context-manager@9d19f75compare · 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.

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.

3 participants