feat(fusion): a stalled review must delegate or reply; a request-named input is not replaced without overwrite (F41, F51) - #459
Merged
Conversation
…t overwrite: true F36 announced a replacement and saved the previous content; live, five first attempts across two local models still rewrote the file the request named as the input (projects.json, sales.csv) from memory, read the warning, and went on. A warning is advice; the write had landed. New `fs-input-guard.ts`, run by `os.fs.write` (mode replace) before the approval prompt: a non-empty file that existed, was not created by this session (the F36 store's created set) and whose basename appears as a whole name in the turn's pinned request is refused with the counts and the way onward — `refused: sales.csv is an input the request names (2,401 lines → 10); edit it in place (os.fs.edit / os.fs.patch), or pass overwrite: true if replacing it is really what the user asked for` — as an error result with `details.refused: "input"`. `overwrite: true` (declared in the args schema, so F40 admits it) lets the write through to the approval and the F36 note. The request's own words lift the rule: the basename within six words after rewrite / replace / regenerate / overwrite / recreate / reset (any inflection, same sentence, not negated). Edits and patches are never refused; every other pre-existing file keeps the warn-and-save path. The request reaches the fs tools as `resolveOriginalRequest`, the bootstrap's per-turn record the workers' briefs quote (declared before `registerOsTools`). A worker's record is now its brief, pinned by the worker branch of `executeTurn`; only its ORIGINAL REQUEST block counts (`quotedRequestText`, the brief's markers shared through `request-section.ts`), so a file the task names as an output is not mistaken for one the operator provided. Tests: name matching (whole name, path prefix, case, longer names), the verb window and its negations, the worker-brief block, F40 acceptance of `overwrite`, and the write tool end to end — refusal before any prompt with the file untouched and no copy taken, overwrite landing with the F36 note, the lift on a rewrite request, created / edit / append / new-file exemptions, no request / no store / empty file / unnamed file, the spelled path and the too-large size, and a worker guarded by its brief's request block rather than its task.
… edit in place The refusal in `fs-input-guard.ts` catches the replacement; this is the model told the rule before it reaches for the tool. Both personas — grammar and native-tools — carry `SYSTEM_PERSONA_INPUTS_LINE` right after the bias-toward-action line: "Files that existed before this turn are the user's: edit them in place and write new files beside them; do not regenerate a provided file from memory. If the user asked for a rewrite, say so and pass overwrite: true." One constant, so the two variants cannot drift; a one-time stable-prefix byte change, like every persona edit before it. Tests (`stable-prefix.test.ts`): the exact sentence, its position (line 3 of both personas, between "Bias toward action" and "Terminals"), once in the built prefix on either transport and ahead of `### rules`, and a ceiling on the persona's token cost (~1,310 today, under 1,400) with the line itself under 70.
…ver replaced by a worker The request-name refusal gives a plain session `overwrite: true` as the way past it. A worker is the wrong party to decide that: it sees the request only as context, and the orchestrator read the whole job. So the `fusion.delegate` contract gains `inputs?: string[]` — paths relative to the working directory or absolute, at most 32, trimmed and deduplicated, a bad entry named by index like every other shape problem, an empty list dropped (`contract-inputs.ts`, read by `parseDelegateArgs`, counted against the rendered cap). The shared block every worker reads lists them first: `INPUTS (the operator's own files — read and edit in place, never replace; os.fs.write on one is refused):`. The worker runner resolves them as the worker's tools would (globs guard nothing) and declares them for the worker's session in a new `DeclaredInputsRegistry` (`fs-declared-inputs.ts`, the shape of `FanoutScopeRegistry`, cleared in the same `finally` as the refuse policy). `os.fs.write` on a declared input is refused for that session with no `overwrite` exemption, without a store or a request — the orchestrator declared it, and it can redeclare. The refusal's head is the request rule's; its tail says what a worker can do, because telling a worker to pass a key that is ignored for it would loop: `refused: sales.csv is an input this fan-out declared (2,401 lines → 10); edit it in place (os.fs.edit / os.fs.patch) — a worker cannot replace a declared input; if the task needs it replaced, say so in your reply so the orchestrator can redeclare it`. The registry is wired in the bootstrap next to the request record; the descriptor and the args schema of `fusion.delegate` carry `contract.inputs`. Tests: parsing (carried alone or with the rest, JSON text, every bad entry named, non-array, cap, empty dropped, rendered cap), the INPUTS section first in the block and absent otherwise, resolution without globs or repeats, the brief, the runner's declare-before / clear-after with and without inputs, the tool end to end for every worker, and the write tool: refused whatever `overwrite` says with the file untouched and no prompt, an edit in place fine, another session unbound, the worker unbound after `clear`.
plombeer31
changed the base branch from
harness/16-local-turn-shaping
to
main
September 17, 2026 10:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two fixes for the two failure classes that survived the bench-2 matrix on both local models (findings 29, 34, 35):
fusion.delegatecalls — because the orchestrator gate refuses every write and a local model answers a refusal by reading again. Now the orchestrator turn counts consecutive read-only steps since the last fan-out (a refused write counts as one). Atllm.runMode.fusion.reviewStallSteps(default 6,0disables; halved when the message is a repair request with failing check lines) the model gets one notice: "6 steps of reading and no fan-out. In Fusion you cannot edit; a fix meansfusion.delegatewith the change spelled out, orreplywith what stands." At twice that, the step's tool set isfusion.delegate,reply,finishonly — the grammar for local models, the descriptor list for cloud models, and a dispatch refusal for the text-JSON fallback — so a read cannot be emitted. A fan-out resets the count.step_finishedcarriesreviewStall: {steps, phase}; the TUI step line says "review stalled: delegate or reply" on a cut step. Config 68 → 69.overwrite: true. Five first attempts across both local models failed by regenerating a provided file from memory (projects.json,sales.csv); F36/F43 warned and saved, the write landed. Three layers: (1)os.fs.writeon a file that existed before the turn, was not created by the agent, and whose name appears in the pinned original request is refused —refused: sales.csv is an input the request names (2,402 lines → 10); edit it in place (os.fs.edit / os.fs.patch), or pass overwrite: true if replacing it is really what the user asked for— unless the request's own words asked for a rewrite (rewrite / replace / regenerate / overwrite / recreate / reset within the same sentence, negations respected);os.fs.editandos.fs.patchare never refused by this rule. (2) One persona line: "Files that existed before this turn are the user's: edit them in place and write new files beside them; do not regenerate a provided file from memory. If the user asked for a rewrite, say so and pass overwrite: true." (3) The fusion contract gainsinputs: declared inputs are listed in the worker brief as edit-in-place, and a worker's write that would replace one is refused (the orchestrator redeclares if a replacement is really wanted).Why
These are the last two ways a Fusion or local run in the matrix failed without a defect in the task itself: the planner never handing the fix to a worker, and the builder treating the user's data as scratch. Both fixes act on the runtime's own signals (the gate's refusals, the pinned request, the F36 store), not on prompt hope; the notice comes first, the hard cut second, and every refusal names the one thing to do next.
How it was verified
npm run lintcleannpx vitest run src/agent src/config src/tracing src/llm— 154 files / 2,434 tests green;src/tools src/prompt src/agent src/runtime— 1,305 + 773 tests greensend-message-concurrencybaseline (the whole stack rebased ontomainat v0.6.2, 2026-09-17)0disables; a turn that delegates on step 2 never sees the notice; with the loop wiring stashed the seams tests fail.overwrite: truepasses; worker refusal on a declared input; the persona line pinned; with the write wiring stashed 4 of 11 refusal tests fail.Stacked on #453 (
harness/16-local-turn-shaping); merge in order.