fix(fusion): waves by contract, stalled hand-back, restore per working directory, one refusal per delegate call (F42–F46) - #439
Merged
Conversation
…ps without a write, never mid-generation Live, Gemma 4 31B at ~6 tok/s, 2026-09-15: F19's time half fired at 1,350 s while the worker's FIRST completion was still streaming — 7,293 tokens of the file it was about to write. Zero completed steps, 22 minutes of generation discarded, the orchestrator re-briefed from scratch. The early hand-back for a task with declared files now fires only when (a) at least HAND_BACK_MIN_COMPLETED_STEPS = 2 steps have completed (step_finished, not step_started) with no successful os.fs.write/edit/patch, AND (b) nothing is in flight: the rule runs when a step finishes, never on a timer. Past that floor it fires at max(2, workerMaxSteps/2) completed steps or, at a step boundary, once half the time limit has elapsed. A hand-back that trips on the step that closed the turn by itself leaves the reply to the ground-truth check. The worker's wall timeout (F19's throughput-derived limit) is unchanged as the hard bound, now armed on a plain setTimeout with the same TimeoutError so a test clock can drive it. The needs_orchestrator status and the forced summary stay; the note carries how many steps had completed, and the row's stepCount matches it. Tests: the time-based hand-back during step 1 now expects the worker to continue; added the two-completed-steps floor on the step half, one completed step plus a long in-flight generation, the time half at a step boundary (not at one completed step), and the wall timeout ending a worker stuck in one endless step under fake timers.
…eplaced input is a status-table fact
Live, fusion, Gemma worker, 2026-09-15: the worker overwrote the user's
2,401-row `sales.csv` with a 9-row sample. F36 saved the original under
`<stateDir>/restore/<workerSessionId>/1-sales.csv` and warned in the
write result. Two gaps: each fusion worker is its own ephemeral session,
so a later worker's `os.fs.restore {"path":"sales.csv"}` found nothing;
and the orchestrator saw the warning only inside the worker's prose
block of the delegate summary and merged anyway.
- fs-restore-store / fs-restore-manifest: the copies are keyed by
WORKING DIRECTORY — `<stateDir>/restore/<key>/` with `<n>-<basename>`
and a `manifest.json` (v2: the copy index plus the directory's path),
`key` = first 32 hex chars of sha256(absolute working dir)
(`restoreKey`). The created-by-the-agent set stays per session at
`<stateDir>/restore/sessions/<id>.json`. Any session on the same
working directory — the orchestrator, a worker of any fan-out, a
resumed session — restores a path. Cap of 20 copies per working dir
and the 5 MB limit kept. Nothing migrated: F36's per-session dirs are
no longer consulted. Concurrent writers are the normal case now (one
fan-out's workers share a manifest): read-modify-writes are serialised
in-process per index, and a copy file is created with `wx`, its
number bumped past anything another process left. Each copy records
the session that took it (`savedBy` in the restore result).
- fs-replace-guard: takes `workingDir` beside `sessionId`; the result's
`details.replaced` carries `display` (the path as the call spelled
it). os.fs.restore looks up by working directory; its description and
the prompt descriptor say "in this working directory, by this session
or another".
- fusion/replaced-inputs: every replace-guard hit in a worker's tool
results is recorded per task as `replacedInputs` (path, tool, lines
before/after, header changed, saved). worker-result renders it first
on the task's status-table row — `- [t1] ok — Sales — replaced the
user's file sales.csv (2,401 → 9 lines)` — ahead of the error and the
notes, counts it on the head line (`2 tasks: 2 ok — 1 replaced
input`), puts it first in the task block's diagnosis, and
`details.tasks[].replacedInputs` carries it. The status stands.
- fusion-guidance: "A task that replaced a pre-existing file is not done
until the file is restored (`os.fs.restore` in a worker) or the
replacement was asked for." Paid for within the 1,400-char budget by
shortening the other lines; every pinned phrase kept.
Tests: cross-session restore on one working directory (worker A
replaces, worker B / the orchestrator / a fresh store restores);
different working directories isolated; the key is the absolute path
however spelled; five sessions replacing concurrently get five distinct
copies; the created set stays per session; the collector records hits
in call order and ignores unknown shapes; head line, row, block and
details with a replaced input, plural and unsaved variants; the row
keeps it through the declared-file check whether it stays `ok` or turns
`failed`; end to end through fusion.delegate; the guidance sentence and
its budget.
…lts task titles, and warns instead of refusing an unmatched requires
…equires → provides
…per working directory, one refusal per call (F42–F46) The orchestrator section now states the rules the live fusion runs settled: a fan-out with a contract runs in waves ordered by requires → provides (F45); a worker with declared files is handed back only at a step boundary after two completed steps without a write, and the step half additionally needs a stall — same result three times or six read-only steps (F42, F46); the restore store is keyed by working directory so any worker or the orchestrator can restore a replaced input, which is rendered first on the task's status row and counted on the head line (F43); fusion.delegate refuses only what cannot run, in one message naming every problem, with title optional and an unmatched requires or an unverifiable provides downgraded to a warning (F44).
plombeer31
force-pushed
the
harness/14-fusion-loop-tuning
branch
from
September 16, 2026 22:03
5e9c3ad to
309ec92
Compare
plombeer31
force-pushed
the
harness/13-guards
branch
from
September 16, 2026 22:03
ca5c35d to
2d708ea
Compare
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
Before: the early hand-back fired at half the time budget while a 6 tok/s worker's first completion was still streaming (22 minutes of generation discarded); a busy cloud worker taking 25 cheap steps was handed back as if idle; restore copies were keyed by session, so a later worker could not restore what an earlier worker replaced, and the orchestrator saw the replacement only inside the worker's prose;
fusion.delegaterefused a call per field (titlemissing, an unmatchedrequires, aprovideswith noin) — four consecutive refusals cost ~20 minutes of local generation; a pipeline declared in the contract ran as one parallel fan-out and two workers spent their budget waiting for a file that did not exist yet.After: a hand-back fires only at a step boundary after two completed steps without a write, never mid-generation, and the step half additionally needs a stall (same result 3× or 6 read-only steps), with the reason in the note; restore copies are keyed by working directory so the orchestrator and every worker share them, and a replaced input is rendered first on the task's status row and counted on the head line (
1 replaced input), with one guidance sentence;titleis optional (defaults to the humanised id), one refusal names every problem (up to 32), an unmatchedrequiresand an unverifiableprovidesbecome warnings carried into every brief; a fan-out runs in waves ordered byrequires→provides(Kahn),maxWorkersper wave, a dependent whose provider failed still runs with a warning, cycles fall back to one wave, and the head line says3 tasks in 2 waves (analyze → organize, index). AGENTS.md notes for all five.Why
Time alone is not evidence of idleness on a slow model; a gate in front of a slow model should refuse only what cannot run at all, warn about everything else, and say all of it at once. A restore that only the session that saved it can see is useless in a fan-out where every worker is its own session.
How it was verified
npm run lintcleannpx vitest run --minWorkers=1 --maxWorkers=3 src/prompt src/tools/fusion src/tools/os— 102 files / 1270 tests green, 3 skipped (baseline failures: none)titledefaulted (F44); a worker's replaced input was rendered on its status row and counted on the head line (F43)Stacked on #438; merge in order.