Admit goal continuations in LocalHarness

Merged
VA
3d ago
+1033-105

#495 is merged. This branch merges main at 4774d27a82; conflicts retain main's LocalHarness, shared synchronization shim and relocated harness-fuzz crate, plus the approved goal-loop delta.

LocalHarness now evaluates private GOAL.md after a settled stock turn and admits the next ordinary stock task itself. Active goals continue across unchanged files; done/blocked goals stop. The existing finite policy writes a blocked GOAL.md with its reason at the cap. Request publication and task admission use the existing filesystem and StreamProvider paths; continuation tasks produce ordinary SchedulerEventEnvelope frames.

Replay first recovers staged immutable model input by its ordinary publication identity before reading the current GOAL.md. Resuming any continuation recovers its actual loop root from that turn's retained request-head publication, including after successors replace the head. No extra store or API is added.

Decisions

  • LocalHarnessConfig.goal_continuations is declarative data: Some(GoalContinuationPolicy::default()) enables the finite default of 32; None disables admission. Setting max_continuations selects another finite allowance.
  • inputs/current.json retains the ordinary receive/model request, root operation and automatic-turn count. Reopen/replay uses staged input and the original request publication; goal edits neither change an in-flight request nor reset the cap. Each turn recovers its root from its immutable head-write generation.
  • User submit and settle share one admission guard. A newer user turn supersedes the stale settle; if a continuation already occupied the slot, another submit receives Admission::Rejected while that turn remains active. A new explicit user turn starts a new allowance.
  • Lock order is admission guard, then provider locks. Model/tool execution runs outside the admission guard. The owned async shim is shared with checkout commands; instrumentation uses only cfg(acyclic_harness_loom), selected by the model command's RUSTFLAGS. The model-checking feature selects the optional dependency only.
  • Goal status and objective remain in GOAL.md, edited by the ordinary write tool or cap writer. Status rewrites use an exact filesystem generation condition.

API for gc-e2e / #600

Drive LocalHarness::open(LocalHarnessConfig::new(directory, (model, scripted_provider)).volume(project)), then submit(operation, prompt) and resume(task_id), or run(prompt). The runtime owns every continuation; the runner must not call on_settle or submit continuation prompts.

For the negative control, set config.goal_continuations = None before open. For cap cases, set Some(GoalContinuationPolicy { max_continuations: cap }).

With test-support + filesystem-local, test_support::local_task_stream(&harness) returns the actual provider. Follow harness/v3/coordinator/events and decode wire::SchedulerEventEnvelope; each continuation has ordinary Declared/Admitted/Completed records. Follow uses server push. Release the follower/provider handles before reopening LocalHarness.

Use filesystem(), scratch(), filesystem::workspace_ref, and FilesystemHost::read for final private GOAL.md bytes. The crate-local fixture in src/local/tests/goals.rs demonstrates frame assertions and ordinary agent writes; goals.json contains unchanged-active, agent-done/blocked, disabled, finite/zero-cap and absent-file cases.

Main merge preserves all shared fuzz targets and context-manifest binding. Tool-registry extension retains the mutable call sites from #495 while using main's immutable shared storage; renamed read/write tools are used by the merged tests.

Validation

Windows MSVC at signed head 65097e95559dbd34497132efb9816947d65507e9:

cargo check -p acyclic-harness --locked --features filesystem-local,native-execution,test-support --lib --tests

Passed locally in 3m21s, including unit and integration-test compilation. Conflict resolution only; all main changes are retained. The goal request corpus and target now live in rust/crates/harness-fuzz. Earlier runtime verification follows.

On bb1 through bs.ps1:

  • The remote gate at intermediate merge head 21172c6dc was stopped when the lane was paused; no pass is claimed for that run. Final conflict resolution was verified by the Windows library/test compilation above.
  • At head 4b35c6af13f0fae09cfe1170d49ad721fb9ae963: focused local::tests::goals suite passed all 4 tests in 1.94 s. Includes interruption after input staging followed by an active goal edit, and resuming a completed continuation with its successor pending, both across reopen and with broken-path negative controls. Command: cargo test -p acyclic-harness --features filesystem-local --lib local::tests::goals -- --nocapture.
  • Earlier focused local:: regression suite: 15 passed in 9.06 s, including runtime scenarios, reopen/replay with future automatic admission disabled, and the request-head round-trip property. Command: cargo test -p acyclic-harness --features filesystem-local --lib local:: -- --nocapture.
  • Settle/user-submit Loom model through the production serialization boundary and predicate, with an unserialized negative control: passed in 0.01 s. Command sets RUSTFLAGS=--cfg acyclic_harness_loom and enables filesystem-local,model-checking; filter local::admission::model.
  • Existing records cargo-fuzz target extended with the production request-head parser and a checked-in seed: 256 sanitizer executions passed in under 1 s on nightly-2026-07-01. Command: cargo +nightly-2026-07-01 fuzz run --fuzz-dir rust/crates/harness/fuzz records -- -runs=256 -max_total_time=5 -max_len=4096 -timeout=1. The record codec is unchanged by the recovery fixes.

Each suite ran below 10 seconds, excluding compilation and queue time. Source formatting and git diff --check pass.


Summary by cubic

LocalHarness now evaluates private GOAL.md after a settled stock turn and admits the next ordinary stock task itself, so active goals continue across unchanged files while done or blocked goals stop. Setting goal_continuations to None disables admission; the default finite policy writes a blocked GOAL.md with its reason at the cap.

The runtime owns every continuation, so runners only call submit(operation, prompt) or resume(task_id); a newer user turn supersedes a stale settle and starts a fresh allowance. Reopen and replay recover staged goal requests and continuation roots from the pinned request head without resetting the cap on goal edits. Disabling admission does not drop continuations already admitted; their replay still works.

Supporting changes

  • Moves the plugin's managed git executor into the harness as shared tools::git types, with bash, fs, read, write, and edit under the new shared tools::defaults() registry and a mutable ToolRegistry::extend.
  • Adds parser and record fuzz targets under one workspace package, plus Loom ownership models for the admission gate and checkout commands with negative controls.
  • Splits request token accounting between fixed framing and message content, fits automatic compaction budgets to provider-counted tool overhead, and adds an optional range input to the read tool.
  • Renames read_file/write_file/edit_file to read/write/edit; the old names are gone.