Add filesystem-driven child agent control

Merged
VA
1d ago
+16101-380

Final re-review fix (pushed signed head4b6b867a075611b7ca7ef190696994f384291c10):

  • 78000e885 fixes retained bodies breaking fs ls. Both child/root directory listings use shared TaskVolume::list_files: it selects typed keys from the accepted index without projecting, cloning, parsing or charging file bodies. Cat retains the original body-byte bound; rendered listings retain the original output and file-count bounds.
  • A 32-case shrinking property grows prompt/result bodies independently through8191bytes and compares exact listing output for both selections. The fixed3KiB+3KiB case fits the4KiB listing. The old body-bounded query fails the same success/equality invariant; deleting that assertion lets the control pass. File-count overflow still fails.
  • 4b6b867a0 fixes automated finding4302422637: spawn/wait/archive share one feature-gated cancellation binding, using no token without filesystem support. No native cancellation behavior changed.
  • Focused final validation job1792248207738-45192 passed: agent-file tests3/3 in0.03s (including both new listing checks); child tools10/10 in0.01s; canonical full-feature GNU Windows lib/tests check41.81s; no-default-features library check1m42s. Native build7m04s. Exact script Q:/lanes/gc-agents-listing-gate.sh, all commands through bs.ps1; no local builds. This pure query/feature fix leaves synchronization and parsers unchanged, so the confirmed16models/4096fuzzruns below are reused rather than rebuilt.
  • Coordinator re-review Q:/lanes/rev-669-verdict-r.md confirmed all other findings and all nine defaults. This fixes its sole remaining P2. No new slice or PR; implementation stops after this push per coordinator instruction.

Child lifecycle reads and result folding stay bounded by the selected child. wait(any) retains its selection across recovery, and input delivery processes only the authenticated suffix. Delegation keeps child futures driven across coordinator waits and drops cancelled pending follow reads.

Review fixes:

  • Result eviction uses a BTreeMap range restricted to one child's result keys. A completion cannot scan sibling values. Production fold visits equal 16/32 for 16/32 siblings; the independent old retain loop visits 256/1024 and fails the same assertion. Full folded maps match, a proptest preserves every sibling/newer turn, and deleting the work assertion makes the control pass.
  • /delegate stalled at the parent outcome's coordinator mutex: a child held that gate across IO, its wake made it runnable, but the controller awaited metadata inside a select arm and stopped polling children. The parent outcome now belongs to the driven parent future; all orchestrator metadata awaits share a helper that continues driving parent/sibling futures and queues their completions. The deadline-free regression parks the real coordinator owner after an explicit IO-completion handshake. Its old direct-await variant fails the same progress assertion. The real coordinator mutex now uses the existing crate-local Loom shim.
  • Pending follow reads live inside the cancellable branch future. Only the accepted cursor survives cancellation, so an unfold stream cannot retain a queued publication read permit ahead of its own write. A real Tokio publication-writer handshake catches the old speculative poll and has an assertion-deletion control. The Tokio handshake specifically covers reserved permits; the Loom stream gate is nonreserving.
  • Production child admission returns the task identity without opening a workspace view that its caller immediately dropped. The test-only inspection path retains the view for fork/recovery assertions. Independent differential fixtures compare full answer bytes, exact forged-request errors, and every observer operation against its own exact canonical admission size; the removed view costs 156 items/18 allocations in the old-path control versus exactly zero/zero in the candidate. No allowance or charge changed.
  • Lifecycle selection reads only control/status/delivery metadata and the latest immutable result. Prompts are loaded separately only for scheduled children; inbox and earlier result bodies are not folded into the controller's retained view. The 4 KiB test accepts a 3 KiB prompt plus 3 KiB answer, and a 32-turn test keeps the lifecycle selection at two files. The previous full-history query is the negative control.
  • Wait(any) retains an immutable selection at .tasks//child-waits/.json before returning to the tool journal. It binds the input hash, selected result reference and inline-answer choice. Recovery reloads that immutable answer instead of selecting by argument order. The crash cut selects B, then settles A before reopen; disabling selection retention must change the winner and fail the assertion.
  • agents//delivered.json is a harness-owned monotonic frontier. A rebuilt accepted-order index selects only its suffix, and a pass stops at the input count already acknowledged in status. Sequential N-message/reopen checks assert exactly N+1 staging/delivery calls; removing frontier persistence produces 15 calls for four messages. A send-before-frontier crash retries just its unacknowledged operation, through the existing deduplicated mailbox.
  • Child fs checkout and bash now use the same cached native checkout executor. Volume-only bindings do not advertise child tools without a workspace/controller. Live input acknowledgements precede delivery. A retained ModelStopped result reopens with its original cancellation outcome; parent/child errors drain native sibling capture and result publication before returning.

Parents can run delegated shell work, receive immutable child answers or failures, and stop a running child without dropping its siblings. Child execution uses the shared LocalHarness scheduler, lazy project/private forks, original owner fences and existing native checkout executor.

The default declarative catalog contains exactly bash, fs, read, write, edit, spawn, send, wait, archive. The multiplexed agent tool and its command/action enums are deleted without a shim. Spawn writes a request and returns its ref; send writes an immutable inbox file; wait follows accepted files until any/all selected children settle and returns bounded result refs with fitting answers/failures; archive cancels and drains the subtree before freezing it. Detailed status, list and inspection stay files under .volumes/agents, read through read/fs. Pause/resume/interrupt are client control-file writes. /delegate dispatches the registered spawn tool.

Per-turn results are immutable agents//results/.json files reconstructed from accepted workflow records. The parent-model scenario asserts the answer and immutable ref reach the next model request; multi-wait asserts both results. Known failed outcomes retain their failure diagnostic. The provider transport-error probe separately verifies uncertainty does not manufacture a settled answer. This PR adds no approval modes or gates. Existing Harness approval support remains intact; GraphCoder defaults to always-accept through its configuration.

Active interrupt/cancel/archive retains and drains the original native process, capture/publication future and descendants before acknowledging quiescence. Siblings finish independently; interrupt resumes the same selected child identity. Terminal recovery reads the retained cancelled receive cut without claiming a new execution lease. A cancelled child may advance only to archived; neither terminal state can resume or accept messages. Archived files and branch volumes remain readable and mergeable/discardable through fs.

The preparation/admission/status crash-cut test closes and reopens the real LocalHarness, changes the parent after the captured cut, verifies the same child/workspace and original authored bytes, then runs, archives and merges the child. This found native capture deleting SDK-only .system metadata that checkout publication deliberately excludes; capture now preserves .system and .volumes. The unused submit_child_turn path is deleted; its replay/depth test now publishes real request files in the parent and child volumes.

Full grant selections attenuate capabilities before created-resource proofs. Creation authority is checked against the authenticated parent, independently of rights inherited by the child. Single and batch admission properties cover both allowed and denied parents; a child without the spawn capability sees none of spawn/send/wait/archive. The read-only child retains model:generate and mail:read, which its mailbox-driven turns require.

Child input staging now retains each file immediately through the existing live query, then fences the common filesystem authority log once before admitting preparation. Object commits remain durable individually. Errors fence their accepted prefix too. No model/tool effect runs inside this group, and the original operation identities, work limits and ownership fences remain unchanged.

Latest validation and performance (pushed e5192ddc8, ready for review):

  • Final focused gate job1792233152073-3981190 passed. Lifecycle 12/12 in 5.89s; all four recovery/archive/merge cases in 2.98s; task-file tests including /delegate 5/5 in 1.33s; original native budget regression 0.20s. All 16 scenarios passed in 5.601s. Windows GNU Harness lib/tests proof passed in 1m52s; browser library proof passed in 35.67s.
  • All 16 exhaustive models passed in 4.23s combined: driver0.01, refresh2.04, drain0.08, input0.00, stop1.01, preparation0.35, files0.40, publication0.34. Negative controls and assertion-deletion checks passed; no new exploration bounds.
  • Direct-binary follow-up job1792234034847-3992970 passed all five new-main hook lifecycle tests in 0.27s (pinned settings, recovery, authority and retained workspace edits).
  • Final paired direct-binary comparison against reviewed 7ccf0157c, independent fresh stores, every case retained:
GroupReviewed baselineFinal merged candidate
Lifecycle, all125.09s6.66s
Four crash/reopen/archive/merge cases, serial7.73s7.71s
All16 agent scenarios5.621s5.759s

The paired lifecycle/scenario results do not show a speedup. The final gate's normal recovery suite runs four independent fixtures concurrently (2.98s); the paired measurement forces candidate --test-threads=1, so scheduling savings are not credited to production. No case/assertion was removed. Merged-main changes also contribute to runtime differences. Earlier shared-box runs ranged from 5.537s to14.346s for the scenarios; no elapsed-time oracle, guard or allowance was relaxed.

  • Deterministic differentials establish the removed production work independently of runtime noise: unused workspace156items/18allocations ->0/0; full fork-report payload reads (~4834bytes in final fixture) ->zero; repeated successful follow opens N ->one; stale-cut status publication attempts two ->one; sibling fold16/32visits versus256/1024 in the quadratic control. Full bytes/exact errors/observer traces are checked where applicable.
  • CPU profiles: baseline memmove6.46%, original work admission3.20%, preparation charging3.20%, allocator3.09%; candidate recovery memmove6.88%, work admission5.29%, accounting mutex4.50%, JSON writing2.91%. Truncated unwinds prevent stronger caller attribution.
  • Unchanged child-control parser fuzz passed4,096runs in1s,32checked-in seeds,coverage12,844,RSS137MiB, pinned nightly-2026-07-01, job1792217373692-2732416. Renamed/deleted tool and function callers were searched repository-wide.
  • Main merged through50ff90cc5 only after GitHub reported conflicts. Hook capture now carries the ordinary journal owner and fresh-before/settlement-after publication phases; pinned SessionStart manifests remain retained for child admission. GitHub confirms mergeable=true at this head.

Exact gates (all through Q:/lanes/bs.ps1):

  • Native: cargo test --locked -p acyclic-harness --features terminal,filesystem-local,native-execution,grpc,test-support,model-checking --lib --no-run --message-format=json; run the emitted executable with focused filters. E2E builds only --test e2e with the same closure, then E2E_FILTER=local-scenarios/agents/ scenarios --exact --nocapture.
  • Windows: cargo check --locked -p acyclic-harness --lib --tests --features terminal,filesystem-local,native-execution,grpc,test-support,model-checking --target x86_64-pc-windows-gnu.
  • Browser: cargo check --locked -p acyclic-harness --lib --no-default-features --features browser,test-support --target wasm32-unknown-unknown. Native integration targets selected by a broad --tests are not browser-qualified; their assertions were not removed.
  • Models: the native lib build with RUSTFLAGS='--cfg acyclic_harness_loom --cfg acyclic_loom --cfg acyclic_fs_loom', then emitted binary filters drive::models, follow::models, draining::models, inputs::models, stopping::models, preparation::models, task_volume::files::models and task_volume::publication::models. Complete script: Q:/lanes/gc-agents-final-main-gate.sh. Paired measurement (no builds): $gate=Get-Content -Raw Q:/lanes/gc-agents-final-paired-measurement.sh; Q:/lanes/bs.ps1 bash -lc $gate. It runs independent reviewed-baseline and candidate stores, with candidate recovery explicitly serial.

Budget regression:
runtime::native_context::tests::shared_native_content_uses_original_owner_and_keeps_real_output_custody passes with the original 65,536-item fork allowance. It failed on #669 at 65,537/65,536 while main passed. Original-ledger probes located repeated JSON descendant scans during fork preparation; the fix uses main #686's single-pass structural decoder. All probes were removed; no ledger was replaced or reset, and no allowance or charge was weakened.

Original ledger / counterMain 8d902b852Before 9c9f38432Fixed
Fixture / items examined197907197
Fixture / bytes copied4,2324,1844,232
Fixture / allocation operations118110118
Fixture / peak allocation bytes33,40033,40833,408
Fork preparation / items examined18,54165,536; next charge 65,537 rejected18,541
Fork preparation / authority reads17495 before failure174
Fork preparation / authority appends8576 before failure85
Fork preparation / bytes hashed184,395111,321 before failure184,395
Fork preparation / bytes copied3,696,8833,572,245 before failure3,696,883
Fork preparation / bytes encoded169,656122,251 before failure169,656
Fork preparation / allocation operations6,0123,630 before failure6,012
Fork preparation / peak allocation bytes16,777,21616,777,21616,777,216
Native content / items after context binding58,427not reached58,427
Native content / items after staging78,819not reached78,819

The historical budget-repair receipt matches main 8d902b852 across all 24 counters, including storage I/O and returned items; so do its native context-binding and staging receipts. The current gate separately reruns the unchanged-allowance regression test. Before-fix fork values are a failed prefix, not a complete-run delta. Fixture item work drops by exactly 710; its eight-byte peak difference from main is unchanged by the fix. All original allowances are unchanged.

Decisions:

  • Spawn.grants is a complete capability subset; omission inherits. Parent authority prepares and admits the child, while execution receives only selected grants plus its authenticated created-resource bindings.
  • Wait defaults to all. Spawn and archive return compact refs; detailed execution status stays in files rather than being repeated in tool results. This preserves the original 32,768/4,096 context/output capacities and exact scripted work.
  • Cancelled may only advance to archived. No terminal transition accepts new inbox work or revives execution.
  • An ancestor stop cancels descendants, including idle descendants, before acknowledging the ancestor. Interrupt retains the selected identity; it does not resume stopped descendants.
  • Recovery uses an authenticated retained checkpoint only after cancellation and reservation release. Ordinary wake queries continue to hide terminal tasks.
  • One native feature closure: terminal,filesystem-local,native-execution,grpc,test-support,model-checking. Dedicated Loom adds --cfg acyclic_harness_loom --cfg acyclic_loom --cfg acyclic_fs_loom only in the model command.
  • The spawn admission barrier remains: releasing it before owned preparation/admission finishes would release the parent's live lease too soon.
  • Budget repair reuses main #686's one-pass structural decoder. No allowance, charge, original ledger or ownership invariant was weakened.
  • All control state and results remain files followed through existing live queries. Existing Harness approvals remain intact; GraphCoder defaults to always-accept through configuration.
  • Only immutable child input staging defers authority durability. Accepted files still push immediately; preparation admission waits for one common fence, including failure prefixes. The model checks that completion boundary; provider durability internals remain covered by their own models.
  • The accepted-order child-input index is derived from the existing task-volume stream, not another authority store. Frontiers and wait selections are ordinary authenticated task files; callers cannot write harness-owned records. No new limits or preemption bounds.
  • Input acknowledgement is split at the accepted publication cut: one model drives the real controller publisher and subsequent immutable result; a separate model races the real prepared acknowledgement/control commits. Ordered preparations cover either write starting after the other accept. This partitions all outcomes without bounding schedules or repeating the entire setup inside the concurrent prefix.
  • The drain model separates the external effect wakeup from the ready-sibling completeness check. A ready sibling touches no external synchronization; both models still use production finish/cancellation paths and require the parent completion fence. Each retains a missing-drain negative control and assertion-deletion check.
  • The effect wakeup adapter models external I/O with one Loom leaf mutex. It does not substitute any production lock: private cancellation and completion gates still use the real acyclic-fs shim. This avoids re-exploring an unrelated cancellation tree for a generic effect future.

Live steps:

  • Runtime measurements below are Linux/bb1. Windows GNU compile proof passes; macOS/Windows runtime performance qualification has not been measured in this lane.
  • Fuzz-crate Windows cross-check needs x86_64-w64-mingw32-g++, absent on bb1. Harness Windows proof covers the changed tool types and Windows paths.
  • Share authenticated ownership/recovery primitives with #515 after it lands, carrying native command-session identity through the shared admission path. No gc-service specifics are present in Q:/lanes/followups.md. This remains follow-up work, not a blocker for this PR.
  • Existing Harness approvals remain intact; GraphCoder always-accept is configuration. Approval-mode implementation is outside this PR and belongs to gc-approvals.