Repository navigation
meta: v2 baseline release + zccache patch removal + #782 Phase A + #497 follow-ups #500
Description
⚠ Not a
/clud-fixtarget. Coordination tracker for the next batch of v2 broker work. Each row is/clud-pr-driveable onmain. This meta exists to anchor a single/goalon a focused next-milestone chunk: drop zccache's[patch.crates-io]git+rev pin (the most visible cleanup) + finish #782 Phase A + close out the #497 trait-shape follow-ups.
Background
The broker-v2 baseline is live on running-process/main — slices 1–9 from #488 shipped. zccache consumes the new surface via a [patch.crates-io] running-process = { git+rev } pin while the migration runs (4 zccache PRs merged: #778, #779, #780, #781, #789, #790, #791, #795). The #782 meta drives the consumer-side burn-down (13 slices remaining). #497 added the BrokeredBackend trait scaffold and explicitly deferred Option B + trybuild + launcher-budget test.
This meta wraps a single coherent milestone — small enough to drive to completion in a focused goal, large enough to deliver visible cleanup ("zccache stops carrying a git+rev pin").
Done When
All sub-items checked AND:
- running-process has a published crates.io release exposing the full v2 surface:
protocol_v2,client_v2,names_v2,brokered_backend,http_endpoint_registry,get_http_endpoint_dispatch,broker_http_port,broker_http_server, plus therunning-process-broker-v2binary. - zccache's
[patch.crates-io] running-process = { git+rev }is removed; zccache compiles + tests against the published version like any other downstream consumer. - zccache#782 Phase A (slices 15–17) is fully ticked.
- feat(broker-v2): formalize daemon startup contract — declare fast-bind vs lazy-init, broker-enforced budget #497's three deferred follow-ups have either shipped or been explicitly punted via a follow-up issue with a clear reason.
Queue
Each row is its own /clud-pr slice. Order is dependency-led — every row below depends on the rows above.
Track 1 — Ship the v2 surface (running-process)
- Slice 30 —
running-processrelease on crates.io. (PR chore(release): bump 4.4.0 -> 4.5.0 (slice 30 of #500) #501; Auto Release fires post-merge) Bump workspace version (4.4.0→4.5.0or whatever the version-check script wants), exerciseuv run --module ci.version_checklocally, then push a tagged release through the Auto Release workflow (.github/workflows/auto-release.yml). Manually confirm the release on crates.io exposes the v2 modules listed above.
Track 2 — Drop the pin (zccache)
- Slice 31 —
zccacheCargo.toml: replace[patch.crates-io]git+rev with regularrunning-process = "4.x". Bump the workspace dep to the new published version. Runcargo update -p running-process. Verify./test(zccache's full suite) passes. This is feat(test-watchdog): prefer the cooperative probe dump over a debugger #782 slices 26 + 27 collapsed.
Track 3 — Finish #782 Phase A
- Slice 15 —
classify_adopt_errorricher mapping. (feat(ipc): per-ErrorCode mapping in from_brokerv2_error (slice 15 of #500) forge-ci/zccache#806) Wire the per-ErrorCodemapping for theBrokerV2Error::Refused→BrokerRefusalconversion that slice 12 stubbed coarse (feat(window-icon): add the X11 _NET_WM_ICON backend (#577) #790). Tests cover each ErrorCode variant. ~3 refs incrates/zccache/src/ipc/broker.rs. - Slice 16 —
shared_broker_pipe/user_sid_hashmigration. (feat(ipc): default_broker_v2_pipe_name helper (slice 16 of #500) forge-ci/zccache#807) Replace v1 pipe-name derivation inipc/broker.rswithnames_v2::v2_program_pipe+lifecycle::sid::user_sid_hash. ~2 refs. - Slice 17 —
Frame/ENVELOPE_VERSION/MAX_FRAME_BYTES/ErrorCodeenvelope helpers. (docs(ipc): document v1 envelope retention in probe.rs (slice 17 of #500) forge-ci/zccache#808) Decision per-touch: keep the cross-version-stable constants (feat(broker): v2 — broker-as-proxy with shmem broker↔daemon, streaming, and dev/release gating #470 coexistence) or migrate to v2 equivalents. Document the per-symbol decision in the PR body. ~6 refs.
Track 4 — #497 follow-ups
- Slice 32 — Option B (broker-owned bind via inherited handles). Strictly stronger than the Option A trait scaffold from feat(broker-v2): BrokeredBackend trait scaffold (#497 Option A) #499. New launcher path that
bind()s in the broker process and passes the handle viaSCM_RIGHTS(Unix) /DuplicateHandle(Windows). Documents the broker's process model assumption (spawn → fd-pass → exec). Conformance test:broker_owned_bind_endpoint_is_probe_able_before_daemon_main_runsper feat(broker-v2): formalize daemon startup contract — declare fast-bind vs lazy-init, broker-enforced budget #497's acceptance criteria. - Slice 33 — trybuild UI harness. (feat(broker-v2): trybuild UI harness for BrokeredBackend (slice 33 of #500) #502) Properly assert "can't smuggle state into
bind" via a trybuild test (tests/ui/brokered_backend_state_in_bind.rs) instead of relying on the type-system shape check that PR feat(broker-v2): BrokeredBackend trait scaffold (#497 Option A) #499 used as a substitute. Each rejected misuse pattern gets its own*.stderrsnapshot. - Slice 34 —
launch_fails_if_endpoint_not_probe_able_within_budget. Integration test gated on the v2 broker launcher actually callingbrokered_backend::bootstrap. Wiring step itself is one PR; the test follows. VerifiesDEFAULT_ENDPOINT_PROBE_TIMEOUTis honored as the hang-detection budget per feat(broker-v2): formalize daemon startup contract — declare fast-bind vs lazy-init, broker-enforced budget #497.
How to drive this
- Per slice:
/clud-pr <this meta>with a scope sentence referencing the specific slice number above. - Per merge:
/clud-pr-merge <PR url>or the explicit "merge it - we can fix issues later" admin-merge cadence from the established pattern. - Coordination updates: tick the checkbox + add the PR number after each merge so the queue stays current.
- Docker precheck convention: every slice that touches zccache runs
MSYS_NO_PATHCONV=1 docker run --rm -v "<repo>:/work" -w /work -v zccache-slice11-cargo-cache:/usr/local/cargo/registry -v zccache-slice11-target:/work/target rust:1.94.1 bash -c "cargo test -p zccache --lib ipc::broker_v2"before pushing. running-process slices runuv run ci/dev_docker.py cargo test --features client --lib <module>.
Related
- zackees/zccache#782 — full zccache migration burn-down (this meta picks off the first batch: Phase A + the unblock-the-pin path).
- zackees/zccache#777 — long-form integration tracker (consumer-side acceptance criteria).
- zackees/running-process#488 — upstream broker-v2 baseline (slices 1–9 shipped; stays open as historical context).
- zackees/running-process#499 —
BrokeredBackendtrait scaffold (feat(broker-v2): formalize daemon startup contract — declare fast-bind vs lazy-init, broker-enforced budget #497) that this meta's Track 4 finishes out. - zackees/running-process#470 — broker-v2 source-of-truth design (closed).
Update 2026-06-20: vendoring shipped
- Vendor ConPTY sidecars via LFS + drift-check + lint workflows (chore(release): vendor ConPTY sidecars via LFS for hermetic builds (#500) #505). Replaces slice 30c smart-extraction (chore(release): smart ConPTY extraction + bump to 4.5.2 (slice 30c of #500) #504) with hermetic vendored bytes under
vendor/win32/conpty/<arch>/. Release builds no longer fetch from NuGet at build time;conpty-drift-check.ymlweekly cron opens an issue on drift but never auto-updates;conpty-vendor-lint.ymlverifies PE arch on every PR touchingvendor/win32/**.
Activity
feat(ipc): per-ErrorCode mapping in from_brokerv2_error (slice 15 of #…
feat(ipc): default_broker_v2_pipe_name helper (slice 16 of #500) (#807)
docs(ipc): document v1 envelope retention in probe.rs (slice 17 of #500…
chore(release): bump 4.4.0 -> 4.5.0 (slice 30 of #500)
chore(release): bump 4.4.0 -> 4.5.0 (slice 30 of #500) (#501)
feat(broker-v2): trybuild UI harness for BrokeredBackend (slice 33 of #…
zackees commentedon Jun 20, 2026
Found a release-pipeline regression while running Phase 0 of this meta (the 'check existing builder failures' step in the goal directive).
Auto Release workflow run 27858611833 on the 4.5.0 bump (#501) failed at the
Build ConPTY sidecars and manifeststep:Root cause:
WINDOWS_CONPTY_VERSION.txtpins Microsoft.Windows.Console.ConPTY 1.24.260402001, which NuGet no longer serves (404). Latest stable version available is 1.24.260512001.Publish crates.ioandPublish PyPIwere SKIPPED so 4.5.0 never published — the next release attempt has to bump the patch level (4.5.1) since the 4.5.0 git tag exists.Fixing as Slice 30b (Phase 0 follow-up):
WINDOWS_CONPTY_VERSION.txtto 1.24.260512001.running-process = "4.5.1"and removes the[patch.crates-io]git+rev pin.chore(release): fix conpty NuGet pin + bump to 4.5.1 (slice 30b of #500…
zackees commentedon Jun 20, 2026
Slice 30b update — release still failing, different cause:
PR #503 (NuGet pin 1.24.260512001 + bump to 4.5.1) merged. Second Auto Release run 27859121907 now fails at a different step:
The newer NuGet package (1.24.260512001) has a different on-disk layout than the workflow expects. Specifically the
OpenConsole.exebinary is not at the path the workflow'star -C "$src" -cf - conpty.dll OpenConsole.exe ...invocation expects.This is now a release-workflow debugging exercise on the ConPTY sidecar contract (#447), not v2-broker work. Recommended next steps:
curl -fsSL "https://www.nuget.org/api/v2/package/Microsoft.Windows.Console.ConPTY/1.24.260512001" -o /tmp/conpty.nupkg && unzip -l /tmp/conpty.nupkg | grep -i opencons.Filing as Slice 30c — release workflow ConPTY sidecar packaging update — to track separately from the v2 broker work in this meta. Track 2 (zccache patch removal) stays blocked until 4.5.2 publishes.
The other Tracks (3 + 4) are unaffected — slices 15, 16, 17, 33 all merged this session; only slice 32 (Option B broker-owned bind) and slice 34 (launcher-budget integration test) remain in Track 4, both substantial.
chore(release): smart ConPTY extraction + bump to 4.5.2 (slice 30c of #…
chore(release): vendor ConPTY sidecars via LFS for hermetic builds (#500
chore(ci): consolidate ConPTY lint + drift into Dependency Check (#500)…
chore(release): bump 4.5.2 -> 4.5.3 — first release on vendored sidec…
fix(lint): bootstrap divergence + redundant Read import (#500) (#509)
feat(ci): macOS preflight workflow — master build + cached sub-workers (
14 remaining items
feat(broker-v2): launcher can bind the endpoint itself, opt-in (#500) (…
zackees commentedon Aug 1, 2026
Slice 32 is functionally complete. #824 merged as
99a2e6a(27/27) — the launcher half, opt-in and off by default.broker_owned_bindprimitive (bind / prepare / recover)SO_TYPE,SO_ACCEPTCONNwhere the platform supports itdisown_endpoint)bootstrapRUNNING_PROCESS_BROKER_OWNED_BIND=1turns it on. Unset — the default — every byte of the old allocate → spawn → probe path runs.The default-off is load-bearing, not caution
Until it is settled who unlinks the socket at backend teardown, a broker-owned endpoint is never unlinked: enabling this by default would leak a socket file per launch. That is the concrete cost of the open question, and it is why the flag exists rather than a straight switch-over.
The test pins the default rather than describing it — flipping
opted_into default-on failsthe_launcher_opt_in_is_off_by_default_and_respects_an_explicit_zero. Someone changing that default without noticing the teardown question is exactly the regression the shape guards against.What is now decidable with evidence rather than in the abstract
Both questions can be answered by trying it:
support()reports the Windows gap with a reason. The question is whether "the endpoint is reachable before the daemon runs" becomes a cross-platform guarantee or stays a documented Unix optimisation.Answering (2) is what would let the flag flip to on by default. Answering (1) decides whether it eventually disappears in favour of one path everywhere.
Remaining on this meta issue
Slice 34 (
launch_fails_if_endpoint_not_probe_able_within_budget) and the rest of the v2 baseline. Slice 32 itself needs no more code — only the teardown decision before it can become the default.test(broker-v2): the launcher gives up on a daemon that never binds (#…
zackees commentedon Aug 1, 2026
Slice 34 landed — #825 merged as
d657f78(15/15).launch_fails_if_endpoint_not_probe_able_within_budgetpins the property everything else in the broker assumes: a backend whose daemon starts but never reachesbindfails the launch rather than hanging the caller.testbin-sleeperis the fixture because it is a well-behaved process that is useless as a daemon — a live child that never serves is the case where "wait a bit longer" always looks reasonable.Worth recording that the first revision of that test passed while proving nothing: it asserted only
is_err(), and the launcher rejected the fixture in 351µs withBinaryOutsideAllowRootbecauseper_version_binary_dirwas"."while the binary lives undertarget/. Nothing was spawned. The assertion now requiresBackendLaunchError::BackendHandlespecifically, so a pre-spawn validation error can no longer masquerade as a probe timeout.Slices 32 and 34 are both complete
broker_owned_bindprimitiveSO_TYPE/SO_ACCEPTCONNWhat is left on this meta issue, and what it needs
Nothing in slices 32/34 needs more code. Two decisions gate what happens next, and both have been open across this whole run:
RUNNING_PROCESS_BROKER_OWNED_BINDopt-in — until it is answered, a broker-owned endpoint is never unlinked and default-on would leak a socket file per launch. The broker looks like the right owner: it already owns backend lifecycle, and a daemon that dies unexpectedly cannot clean up after itself.Both are now answerable by experiment rather than argument — the flag turns the path on and the conformance tests observe it.
The rest of the meta issue (v2 baseline release, zccache patch removal, #782 Phase A) is consumer-side and outside what this repo can close.
fix(broker-v2): a failed launch must not orphan the socket (#500) (#826)
zackees commentedon Aug 1, 2026
Orphaned-socket defect fixed — #826 merged as
41c13a0(27/27).I introduced this in #824 and found it re-reading my own merged code.
disown_endpointfired immediately afterspawn, so a failed probe killed the child and dropped the broker's listener with its reclaim guard already released: dead daemon, socket file left behind, once per failed launch.The conceptual error is worth naming because it is easy to repeat: a child existing is not the same as a child owning the endpoint. The probe is what decides that, which is exactly what #814 said the reclaim guard was for. Disowning now happens only on the success path.
Why this matters for the teardown question
It sharpens question (1) below rather than answering it. There are two distinct lifetimes here, and only one is still open:
RUNNING_PROCESS_BROKER_OWNED_BINDopt-in.So the leak surface is now strictly the second case. That is a smaller and better-defined decision than "who owns teardown" in general.
Slice 32 / 34 state
All merged: #812 (primitive), #814 (endpoint outlives the listener), #820 (cross-process conformance), #823 (daemon adopts), #824 (launcher, opt-in), #825 (launch gives up on a non-binding daemon), #826 (no orphan on failure).
Still gated on decisions, not code:
fix(broker-v2): the broker removes the socket it tore down (#828)
zackees commentedon Aug 1, 2026
Teardown cleanup narrowed — #828 merged as
cfd3f93(27/27).BackendHandle::shutdownnow removes the socket after it has watched the daemon exit. Worth noting this leaked in the pre-existing spawn-then-probe path too: a signalled daemon never runs its own reclaim, so the endpoint outlived it regardless of who bound it. Not a broker-owned-bind defect.force_killdeliberately does not clean up — it signals and returns without confirming exit, so removing the name there could unlink the socket of a daemon still serving. An unreachable-but-live backend is worse than a stale file.What this does and does not settle for the opt-in
The teardown question splits into three cases. Two are now closed:
shutdown)The third is what still keeps
RUNNING_PROCESS_BROKER_OWNED_BINDopt-in, and it is now the only thing. Under broker-owned bind the daemon's adopted listener has no reclaim guard (From<OwnedFd>givesReclaimGuard::default()), so a self-exiting daemon leaves its endpoint behind.Options, roughly:
(3) is more defensible than it first sounds given (1) cannot cover crashes, but it is a decision about what the runtime directory is allowed to look like, not a code gap. That is the call I have not made.
Everything else in slices 32/34 is merged and tested.
zackees commentedon Aug 1, 2026
Option 1 from my previous comment is not implementable. Ruling it out on evidence rather than taste, so the remaining decision is between two options instead of three.
I proposed "the daemon reclaims its socket on clean exit". Two things in the current design prevent it:
BrokeredBackend::servereturnsNever. A brokered daemon does not return throughbootstrap— it serves until something terminates it. There is no post-serve point at which generic cleanup could run, and adding one would mean changingserve's signature, which exists specifically to prove divergence at the type level.An adopted listener's reclaim guard cannot be armed.
interprocessexposesListenerOptions::reclaim_name(bool)at bind time anddo_not_reclaim_name_on_drop()to disarm, but nothing to arm a guard after the fact. The daemon's listener comes fromFrom<OwnedFd>, which isReclaimGuard::default()— no guard, no way to add one.An individual
BrokeredBackendimplementation could still unlink before callingprocess::exiton its own idle path, but that is per-daemon discipline, not something the framework can guarantee — and it still misses crashes andSIGKILL.So the self-exit case comes down to
Option 2 — the broker sweeps. On the next launch for a service, remove endpoints belonging to daemons that are gone. Catches every case eventually, including crashes. Costs the broker some knowledge of endpoint naming and a liveness check per stale candidate.
Option 3 — accept it, and say so. The allocator generates a fresh random path per launch, so a stale socket is never reused; it accumulates. On Linux
$XDG_RUNTIME_DIRis tmpfs cleared at logout; on macOS$TMPDIRis per-user and periodically swept. The cost is untidiness in a directory that is already ephemeral, not a correctness problem.I lean to (3) for now and (2) only if the accumulation is ever observed to matter — a sweep introduces its own failure mode, deciding a socket is dead while a daemon still holds it, which is the same hazard that made
force_killskip cleanup in #828.But this is a call about what the runtime directory is allowed to look like, and it is the last thing gating
RUNNING_PROCESS_BROKER_OWNED_BINDfrom becoming the default. Everything else in slices 32/34 is merged and tested.chore(release): 4.6.4 -> 4.7.0 (#829)
feat(broker-v2): broker-owned bind on by default (#500 slice 32) (#835)
zackees commentedon Aug 1, 2026
Slice 32 is done. #835 merged as
1e5121a(27/27) — broker-owned bind is on by default, and the last open question is answered.The teardown question, settled
Accepted because sweeping costs more than the untidiness. The allocator issues a fresh random path per launch, so a stale entry is never reused — it accumulates in a directory that is already ephemeral. A sweep would have to decide a socket is dead while a daemon might still hold it, which is the hazard that made
force_killskip cleanup in #828.The third option I originally floated — "the daemon reclaims on clean exit" — is not implementable:
BrokeredBackend::servereturnsNever, so there is no post-serve point, and an adopted listener's reclaim guard cannot be armed after the fact. Ruling that out on evidence is what reduced the choice to two.RUNNING_PROCESS_BROKER_OWNED_BIND=0remains the escape hatch, and a test pins it specifically — if it stops working the fallback to spawn-then-probe becomes unreachable, which is what an operator would need in a hurry.Slice 32 / 34, complete
broker_owned_bindprimitiveSO_TYPE/SO_ACCEPTCONNStill open on this meta issue — and not closable from here
Only the Windows contract question remains as a design item: whether Windows eventually gets a pre-created first pipe instance, or stays on spawn-then-probe permanently. Nothing is blocked on it —
support()reports the gap with a reason and the fallback applies — but it decides whether "reachable before the daemon runs" is a cross-platform guarantee or a documented Unix optimisation.The rest of #500 (v2 baseline release, zccache patch removal, #782 Phase A) is consumer-side and cannot be closed from this repository.
zackees commentedon Sep 30, 2026
Planning pass (automated, Opus):
Row-by-row verification (running-process main @ 2ee2081, plus zccache main)
[patch.crates-io]pinCargo.tomlhas norunning-processdependency and no patch section. Its comment reads "no local path or patch override remains". It now gets systems code throughkernal-api =0.1.23.RUNNING_PROCESS_BROKER_OWNED_BINDis on by defaultAll four Done-When criteria are met. None of them covered Windows.
What is still open
One design question from the last comment: should Windows get a pre-created first pipe instance, or stay on spawn-then-probe permanently? Nothing depends on the answer.
broker_owned_bind::support()already reports the Windows gap, and the spawn-then-probe fallback applies there.Decision
Close this meta. It is the smallest safe option: every row and every Done-When criterion is met. Keeping a tracker that is marked "not a fix target" open for one unscheduled design question just invites more re-audits like the ones already recorded here.
Record the Windows question as its own design issue. Recommended default: spawn-then-probe is the permanent Windows contract, and broker-owned bind is a documented Unix-only optimisation. The reason is structural (see the earlier comment): a Windows named-pipe listener is a single pipe instance, not a bound object. A pre-created instance would serve only the first client, and it would need a new handle-inheritance path through
spawn_daemon, all for little gain.Plan if the default is taken
No code PR. One docs-only PR that states, in the Broker section of
docs/and CLAUDE.md, that reachability before the daemon runs is Unix-only by design../lintQuestion for the maintainer
Close #500 now, and treat spawn-then-probe as the permanent Windows contract? Recommended default: yes to both. Open a dedicated issue only if Windows launch latency is ever measured to be a problem.
zackees commentedon Sep 30, 2026
Closing per the planning comment above: every row and every 'done when' criterion is met (zccache no longer pins running-process, zccache#782 is closed, the v2 baseline shipped). Windows stays on spawn-then-probe permanently; the broker-owned bind is documented as a Unix-only optimisation. New work has its own issues (#1258, #974, #975, #1101, #850).