Skip to content

fix(fusion): waves by contract, stalled hand-back, restore per working directory, one refusal per delegate call (F42–F46) - #439

Merged
plombeer31 merged 6 commits into
mainfrom
harness/14-fusion-loop-tuning
Sep 17, 2026
Merged

plombeer31 merged 6 commits into
mainfrom
harness/14-fusion-loop-tuning

Conversation

@plombeer31

Copy link
Copy Markdown
Collaborator

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.delegate refused a call per field (title missing, an unmatched requires, a provides with no in) — 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; title is optional (defaults to the humanised id), one refusal names every problem (up to 32), an unmatched requires and an unverifiable provides become warnings carried into every brief; a fan-out runs in waves ordered by requiresprovides (Kahn), maxWorkers per wave, a dependent whose provider failed still runs with a warning, cycles fall back to one wave, and the head line says 3 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 lint clean
  • npx vitest run --minWorkers=1 --maxWorkers=3 src/prompt src/tools/fusion src/tools/os — 102 files / 1270 tests green, 3 skipped (baseline failures: none)
  • verified live: fusion-local-orch site task 7/7 in 447 s and report task 6/6 first attempt on this head; the first delegation of each was accepted first time with title defaulted (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.

…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
…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
plombeer31 force-pushed the harness/14-fusion-loop-tuning branch from 5e9c3ad to 309ec92 Compare September 16, 2026 22:03
@plombeer31
plombeer31 changed the base branch from harness/13-guards to main September 17, 2026 10:30
@plombeer31
plombeer31 merged commit be49124 into main Sep 17, 2026
@plombeer31
plombeer31 deleted the harness/14-fusion-loop-tuning branch September 17, 2026 10:31
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.

1 participant