Choose the private summary volume when opening a task runtime
Problem
native_media_boundary::live_original_options_and_actual_native_parts_reach_adapter_without_conversion, in the Harness integration target main, has failed on main since #711:
left: Failed { message: "unauthorized: task has no owner-bound content writer" }
Bisect on bb1 with the canonical native features: 108fa5e52 (#711) fails (0 passed, 1 failed). Its parent bfbaaf2b3 (#773) passes (1 passed).
Root cause
#711 moved native compaction summaries out of the host journal. They are now published through the task context into summaries/<operation>/<step>.txt, via TaskContext::stage_private_file_once. That needs a private writer, which FilesystemTaskRuntime got only from the optional builder with_private_volume.
#711 updated LocalHarness, which calls the builder, and its own canonical test. The retained native-media scenario opens a FilesystemTaskRuntime whose stock turn compacts ("Replay, compaction summary, parent response and child response"). It never called the builder, so compaction failed mid-turn. An earlier fixture-only attempt (e90f054cc, on old #641) bound the outer bundle's writer, which is the wrong runtime, and still failed.
Fix
FilesystemTaskRuntime::open(andfrom_host) takesprivate: Option<&VolumeRef>. When it isSome, open binds it with the same owner, grant and native-custody checks.with_private_volumeis deleted. Its body is now the privatebind_private_volume, called only byfrom_host.LocalHarnesspasses its scratch volume.- The browser runtime, the retained native-media runtime and the canonical compaction test pass their agent-private volume (the browser since review round 1).
- Every other caller passes
Noneexplicitly, each with an unchanged binding:- The Pi runtime: Pi tasks publish into their created scratch volume, and its authority forbids task grants on journal storage.
- The example and the existing fixtures that never compact.
Class: an optional builder step that a compacting runtime could forget, failing only mid-turn. The private destination is now a required argument at the runtime's single constructor, so no caller can omit the decision, and the builder that allowed it is gone. LocalHarness no longer depends on remembering a chained call. Review round 1 closes the remaining gap: with None, compaction is off by default, and arming it explicitly is rejected at turn entry. Binding a native runtime to its journal volume by default isn't possible, because Pi forbids task grants on journal storage.
Deleted: the public with_private_volume builder and its two chained calls.
Review round 1: compaction needs a validated private writer
The review found that browser model commands also bind a TaskContext. With threshold compaction, enough history and no output volume, the browser still reached stage_private_file_once mid-turn and failed with "task has no owner-bound content writer".
- Capability.
TaskContext::private_summariesreturnsPrivateSummaries, a crate-private type built only after validation. It accepts, in order: the task's created private content, the runtime's private volume, or an output binding. The writer must be owner-bound and write-authorized, and must be anAgentPrivatevolume owned by an agent.stage_private_file_oncetakes the capability as an argument, so summary staging cannot run without one. - Entry gate.
verify_task_context_bindingruns at every turn, model-step, tool, prefix and native entry, and fromwith_task_context. It now requires that capability whenever the policy isThreshold. Arming compaction without a private writer fails before any model dispatch, instead of when a summary comes due. - Default.
stock_executionkeeps the defaultThresholdonly where the capability resolves, and usesDisabledotherwise. A runtime opened withNonetherefore runs without compaction unless the caller explicitly arms it. Arming it there is rejected at entry. - Browser. The WASM runtime now passes its authorized private journal volume,
Some(&options.volume), which also holds stock-turn summaries. - Why there is no full compile-time typestate.
Executor::execute_with_contextbinds arbitrary contexts at run time, and task-created private content exists only per task. "Has a private writer" therefore isn't known when the runtime is constructed. The single run-time choke point plus the capability-typed staging call covers every path that can stage a summary.
Fallout fixed in this round:
- Three executor unit tests bind contexts that have no writer. They don't exercise compaction, so they now say
Disabledexplicitly. - The direct-executor
task-workflowcase and the providersrun_stock_turnfixture armThreshold, so they now open with their private volume.
Browser fixtures repaired:
browser-task-fixture.mjsandbrowser-task.htmlcalledissueScopeForAgent, which #710 removed, so every browser task page failed before running. They now callissueScope(id, caps, agent).browser-task.htmlpasses again.- The existing
browser-task-model.htmlstill fails on main for other stale reasons: it lacks the stock turn'sreceiveinput and the context manifest required since #694. That is out of scope here.
Verification (bb1, canonical native features filesystem-local,native-execution,terminal,grpc,test-support,search-http,tui)
- Browser regression (
browser-task-compaction.html, registered inbrowser-qualify.mjs; headless Chrome 131, harness wasm dev build):- It runs a stock turn of two tool steps whose results cross the threshold. A summary request and the response follow, with no output volume selected.
- It asserts the final text and exactly 4 model requests. It also asserts that the big result is gone from the final request, which starts with a
summaries/…file on the agent-owned private volume. - PASS.
- Negative control: the same page, against a build whose browser runtime passes
None, fails at turn entry with 0 model requests.
- Native negative control:
canonical_compactionopens without the private volume and armsThreshold. It getsUnauthorizedbefore any new model dispatch (request count stays 2). It passes; the positive canonical scenario passes too. - Real-path control:
--test mainpassed 44 of 44 in 30.7 s (build 605 s). The native-media test passes. Main fails it with the exact message above (43 passed, 1 failed on 2ecd4f21a, and the same in #882's CI run). - Open-time negative control:
local::tests::root_output_requires_task_grants_and_exclusive_configurationnow asserts that opening without grants on the private volume isUnauthorized. Output denial is still checked separately, against the ungranted journal volume. The test passes. - Owner matrix: the existing
private_content_owner_matrixproperty and its broken-policy control are unchanged and pass. - Other targets:
--test task-workflowpassed 23 of 23 in 0.47 s.acyclic-harness-providers --lib stockpassed 5 of 5 in 0.60 s.cargo check -p acyclic-plugin --lib --testspassed.- WASM
browser --libwith-Dwarningspassed. - The Windows GNU canonical
--lib --testscheck passed. It includes theapproved-native-processexample, whose missed call site the first Windows run caught.
- Pre-existing failures, unchanged:
--libfiltersfilesystem::task_runtime:: durable_host::tests:: local::tests::give 133 passed and 8 failed, both with this PR and on baseline main 2ecd4f21a. The failures are future-size bounds (execution_and_worker_futures_stay_bounded,native_stock_model_original_lost_ack…) and local-harness tests sharing the box's~/.local/share/acyclicstate ("core state namespace is in use", "cannot close an unfinished local turn"). One failure differs between runs:goalson base,command_fileshere, both from that shared state. These need their own follow-up and are not touched here. - Round 1 gates:
acyclic-harness-providers --libpassed 224 of 224.--test task-workflowpassed 23 of 23.--test mainpassed 44 of 44.--lib executor:: canonical_compactionpassed 84 of 84.- WASM
browser --libwith-Dwarningspassed. The Windows GNU canonical--lib --testscheck passed. - A wider
--libsweep coversexecutor:: compaction filesystem::task_runtime:: filesystem::memory:: local::inheritance:: local::admission::. It fails the same 11 tests at #898's previous head daf32cd2e as with this change (future sizes, shared local state, memory-summary and poll-sweep coverage).local-recursive-fork deep::(items examined) fails on both too.
Review round 2: direct summary admission
StockExecutor::summarize could dispatch a summary request before rejecting a writerless task: its internal summary session disables recursive compaction, so the policy-dependent entry gate was skipped.
The shared summary path now resolves PrivateSummaries before admission, independently of compaction policy, and passes the validated capability together with its task context into publication. Publication no longer resolves the writer after model dispatch. This closes the class across direct summaries, in-turn summaries, summary-file turns and automatic compaction.
A shrinking property varies summary history and text, checking both public summary entry points under Disabled and Threshold after binding a writerless context with Disabled. Every case must return Unauthorized with zero model requests and no journal writes. A negative control recreates the former ordering through the real internal summary session and model-step path: exactly one request is dispatched before private-writer resolution returns Unauthorized.
Validation on bb1 at signed commit 00156319d, using canonical native features filesystem-local,native-execution,terminal,grpc,test-support,search-http,tui:
- Focused library binary (
executor:: canonical_compaction private_content_owner_matrix): 87 passed, 0 failed, 4.10 s (4.116 s wall). - Task-workflow binary: 23 passed, 0 failed, 1.74 s (1.766 s wall).
- Combined focused Linux test build: 394.165 s.
- Windows GNU
cargo check --locked -p acyclic-harness -p acyclic-harness-providers -p acyclic-plugin --lib --tests --features filesystem-local,native-execution,terminal,grpc,test-support,search-http,tui --target x86_64-pc-windows-gnu: passed, 298.118 s.
Main conflict resolution (2026-10-18)
Updated to main ddc5cf325 in signed merge 870963d22. The LocalHarness conflict preserves main's erased task-file binding and concrete projection-file binding. The private-volume choice remains in the runtime constructor, replacing the removed builder call.
Focused validation through Q:/lanes/bs.ps1, canonical native features filesystem-local,native-execution,terminal,grpc,test-support,search-http,tui:
- Executor/compaction/owner matrix: 101 passed, 4.76 s (4.777 s wall), including the summary admission regression and negative control.
- Task-workflow: 23 passed, 1.40 s (1.412 s wall).
- Retained native-media regression: 1 passed, 0.37 s.
- Local task-file write/follow/reopen: 1 passed, 0.26 s.
- Windows GNU
--lib --testscheck for Harness, providers and plugin: passed, 413.602 s. - Combined focused Linux test build: 1076.001 s. Built test binaries ran directly; no CI wait.
Decisions
- Preserve main's two task-file bindings when resolving the LocalHarness conflict. Update the pushed branch with a signed merge as required by the lane's history rules.
- Resolve the private summary capability in the shared summary entry regardless of compaction policy, and carry it into publication. This keeps existing public APIs and prevents a nested Disabled summary session from deferring authorization until after dispatch.
Optionrather than a required&VolumeRef, because Pi deliberately has no runtime-level private volume.- Fixtures that never compact pass
None, keeping their open-time grant requirements unchanged. Only the two compacting fixtures bind.
🤖 Generated with Claude Code