projmux 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,618 @@
1
+ # Codex app-server generation pool — Phase 0–5 contract
2
+
3
+ Phase 0 adds the identity, validation, qualification, immutable bundle, and
4
+ read-only planning contracts needed by a future bounded app-server pool. It
5
+ does not start or stop a product endpoint, change a current pointer, dial more
6
+ than the existing broker endpoint, or change Agent create/resume behavior.
7
+
8
+ Phase 2 adds a dark, bounded endpoint runtime pool and a private generation
9
+ host. It still does not change a current pointer or Agent create/resume
10
+ routing: even an initialized, ready `Preparing` endpoint refuses fresh create
11
+ admission before a provider, Registry, tmux, or lifecycle write.
12
+
13
+ Phase 4 adds the explicit rolling operation that prepares one private
14
+ successor, commits admission-current at most once, and marks the old generation
15
+ Draining without moving its live Agents. It does not stop the old endpoint,
16
+ resume a successor thread, rewrite an Agent endpoint ref, relaunch a Pane,
17
+ retire a generation, release a bundle lease, or adopt a foreign runtime.
18
+
19
+ Phase 5 adds a distinct linked handover journal. It pins the generation-wide
20
+ Agent/Pane/thread target set, fences admission and live bindings, stops only an
21
+ exact Projmux-owned old generation, resumes completed persisted threads on the
22
+ qualified successor, observes every snapshot before any cutover, then performs
23
+ endpoint CAS, same-Pane relaunch, terminal retirement, and old lease release.
24
+ It never replays prompt, turn, approval, or content and never kills or adopts a
25
+ foreign/default endpoint.
26
+
27
+ ## Identity and schema
28
+
29
+ A native Codex Agent has two independent generation axes:
30
+
31
+ - `Pane.status.activation.generation` identifies one Pane child
32
+ materialization. Existing lifecycle and termination guards keep this meaning.
33
+ - `Agent.status.sessionRef.codex.endpoint` identifies the Codex state domain and
34
+ app-server generation that durably owns the thread.
35
+
36
+ The additive endpoint shape is:
37
+
38
+ ```json
39
+ {
40
+ "stateDomainID": "opaque-state-domain",
41
+ "endpointGenerationID": "opaque-endpoint-generation"
42
+ }
43
+ ```
44
+
45
+ The optional live activation authority is:
46
+
47
+ ```json
48
+ {
49
+ "stateDomainID": "opaque-state-domain",
50
+ "endpointGenerationID": "opaque-endpoint-generation",
51
+ "brokerRuntimeID": "opaque-broker-runtime",
52
+ "connectionEpoch": 1,
53
+ "bindingEpoch": 1
54
+ }
55
+ ```
56
+
57
+ Phase 1 adds an optional durable projection input beside that endpoint. Planned
58
+ states carry the exact operation that authorized them; ordinary states carry
59
+ no operation:
60
+
61
+ ```json
62
+ {
63
+ "state": "recovering",
64
+ "operation": {
65
+ "id": "opaque-operation",
66
+ "endpoint": {
67
+ "stateDomainID": "opaque-state-domain",
68
+ "endpointGenerationID": "opaque-endpoint-generation"
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ All five live authority dimensions must match. Connection and binding epochs
75
+ remain broker-local counters; equal numbers from another endpoint generation or
76
+ restarted broker are not authority. A registry written before these optional
77
+ fields existed decodes and re-encodes with them absent. Absence is
78
+ `legacy-generation-unavailable`, never an inference to the current endpoint.
79
+
80
+ ## Bounded model and read-only plan
81
+
82
+ The v1 model permits at most two non-retired slots: one `current` and one
83
+ draining obligation (`draining` or `handover-pending`). Preparing, recovering,
84
+ and blocked candidates also consume a live slot. It rejects, among other
85
+ invalid states:
86
+
87
+ - two current generations;
88
+ - two draining/handover-pending generations;
89
+ - more than two non-retired generations;
90
+ - a live Agent obligation without an admission-current generation;
91
+ - obligations naming a missing or retired generation.
92
+
93
+ `codexgeneration.PlanUpgrade` is a pure value reduction. It has no Registry
94
+ writer, provider, tmux, process, or filesystem/current-pointer adapter. JSON and
95
+ text output include the exact Agent UID and endpoint-generation ID for every
96
+ blocker and an explicit zero counter for each mutation class. An exact-current
97
+ unmanaged endpoint is attach-only: the plan can report it, but cannot stop,
98
+ restart, kill, or adopt it.
99
+
100
+ ## Shared-state qualification gate
101
+
102
+ The pool lane remains closed unless one exact old/new version pair passes all
103
+ of the following in a single isolated state domain:
104
+
105
+ 1. different private sockets concurrently create and turn different threads;
106
+ 2. both endpoints read and list the exact threads without duplicates;
107
+ 3. both endpoints survive an observed crash/exit and restart barrier;
108
+ 4. a successor resume is not invoked while the old generation owns the same
109
+ thread;
110
+ 5. after the exact old process stops, only the completed persisted thread is
111
+ resumed and its exact completed-turn snapshot is observed;
112
+ 6. cross-thread writes, store corruption, and ambient mutations remain zero;
113
+ 7. copied `auth.json` and `config.toml` are shared only inside the private state
114
+ domain at mode `0600` and no values enter the receipt.
115
+
116
+ The strict `codexgeneration.QualificationResult` persists only versions,
117
+ booleans, counters, a closed verdict, and a closed reason. If distinct-thread
118
+ concurrency or same-thread ownership is `no`—or any other required fact is
119
+ missing—`GateQualification` keeps Phase 2+ closed and selects
120
+ `single-endpoint-journaled-handover`. It never stores prompts, responses,
121
+ credentials, paths, sockets, or rollout content.
122
+
123
+ ## Release bundle lease
124
+
125
+ `codexbundle` retains the complete executable set a generation needs:
126
+
127
+ - the `codex` binary in both server and TUI roles;
128
+ - `codex-code-mode-host`, bundled `rg`, and bundled `bwrap` as helpers.
129
+
130
+ The lease ID hashes the canonical manifest, which covers version, protocol
131
+ range, role set, relative path, file mode, size, and SHA-256 for every artifact.
132
+ Creation copies into a private staging directory, verifies it, and only then
133
+ atomically commits the content-addressed directory. It never writes a current
134
+ symlink. Missing roles, manifest/hash/size/mode drift, or an unsupported
135
+ protocol range refuse before a usable lease is returned. `Open` repeats the
136
+ verification before launch, so removing the mutable upstream release directory
137
+ does not affect a valid lease.
138
+
139
+ ## Validation workflow
140
+
141
+ The deterministic suite is part of `make test`. The real-Codex test is opt-in
142
+ and qualifies whichever version pair the operator declares. It needs the two
143
+ standalone executables of that pair plus a source Codex home containing
144
+ `auth.json` and `config.toml`. `scripts/test-generation-pool-qualification.sh`
145
+ owns the isolated roots and always leaves one terminal typed record behind:
146
+
147
+ ```sh
148
+ PROJMUX_CODEX_GENERATION_OLD="/absolute/path/to/old/bin/codex" \
149
+ PROJMUX_CODEX_GENERATION_NEW="/absolute/path/to/new/bin/codex" \
150
+ PROJMUX_CODEX_GENERATION_SOURCE_HOME="/absolute/path/to/source-codex-home" \
151
+ scripts/test-generation-pool-qualification.sh <old-version> <new-version> artifacts/pair
152
+ ```
153
+
154
+ The runner writes `artifacts/pair/outcome.json` on every exit — `pass`, `fail`,
155
+ `unsupported` when a declared binary or the credentialed state is unavailable,
156
+ or `infra-error` — and `artifacts/pair/receipt.json` whenever the harness
157
+ reached a verdict. Set `PROJMUX_CODEX_GENERATION_BUNDLE_TMPDIR` to place the
158
+ ~700 MiB bundle root somewhere other than the temporary directory; the smoke
159
+ root itself must stay there so the private sockets fit `sun_path`.
160
+
161
+ The test can also be driven directly. Each declared version must match what its
162
+ binary reports, and the receipt path must be absolute:
163
+
164
+ ```sh
165
+ smoke_root="$(mktemp -d /tmp/projmux-codex-generation-XXXXXX)"
166
+ bundle_root="$(mktemp -d /path/with/at-least-700MiB/projmux-codex-bundles-XXXXXX)"
167
+ env -u TMUX -u TMUX_PANE \
168
+ PROJMUX_CODEX_GENERATION_SMOKE_ROOT="$smoke_root" \
169
+ PROJMUX_CODEX_GENERATION_BUNDLE_SMOKE_ROOT="$bundle_root" \
170
+ PROJMUX_CODEX_GENERATION_OLD="/absolute/path/to/old/bin/codex" \
171
+ PROJMUX_CODEX_GENERATION_NEW="/absolute/path/to/new/bin/codex" \
172
+ PROJMUX_CODEX_GENERATION_OLD_VERSION="<old-version>" \
173
+ PROJMUX_CODEX_GENERATION_NEW_VERSION="<new-version>" \
174
+ PROJMUX_CODEX_GENERATION_SOURCE_HOME="/absolute/path/to/source-codex-home" \
175
+ PROJMUX_CODEX_GENERATION_RECEIPT="/absolute/path/to/receipt.json" \
176
+ go test ./internal/testutil/codexinstalled \
177
+ -run '^TestInstalledIsolatedGenerationPoolQualification$' -count=1 -v
178
+ ```
179
+
180
+ The root must be an empty child of the system temporary directory. The test
181
+ uses two root-contained sockets, performs semantic readiness/completion/exit
182
+ barriers rather than fixed sleeps, removes the leased source directories, and
183
+ deletes only the exact owned root after both children and sockets are gone.
184
+
185
+ The declared pair is the only operator input. Every evidence boolean and
186
+ counter in the receipt stays measured by the harness, and `Validate` recomputes
187
+ the verdict from that evidence, so editing either a counter or the verdict in
188
+ the file makes it stop decoding rather than pass a stronger claim.
189
+
190
+ Recomputing the verdict is not, by itself, a check on the evidence. A YES
191
+ verdict is fully determined by the acceptance booleans and the violation
192
+ counters — every boolean true, every violation zero — so a receipt written by
193
+ hand to say YES is self-consistent and decodes cleanly. The **coverage
194
+ counters** are what separates it from a measured one: `observedThreadTurns`,
195
+ `observedThreadReads`, `observedCrashRestarts`, and `observedBundleLaunches`
196
+ tally what the harness actually did, and an acceptance claim with none of them
197
+ behind it is refused at the gate as unbacked. This does not make forgery
198
+ impossible; it makes the receipt state its coverage, so the evidence set that
199
+ costs nothing to write no longer passes. Receipts predating the coverage
200
+ counters carry `schemaVersion` 1 and are refused as incomplete rather than read
201
+ forward — there is no coverage in them to check.
202
+
203
+ ### Installing a receipt
204
+
205
+ The emitted `receipt.json` reaches the generation entry paths through one route:
206
+
207
+ ```sh
208
+ projmux agent app-server upgrade qualify --receipt /absolute/path/to/receipt.json
209
+ ```
210
+
211
+ `qualify` re-reads the file through the same decoder and the same gate the entry
212
+ paths use, refuses it on exactly their terms, and stores it under the version
213
+ pair it names. Only an accepted receipt is stored, so the presence of a stored
214
+ receipt is itself the claim the gate honors. Both doors into a generation switch
215
+ read that store: managed activation refuses before it writes the journal, and a
216
+ handover resume refuses before it drives any effect. A receipt qualifies the one
217
+ pair it names and no other, and the refusal for a pair with no stored receipt
218
+ names the two commands that produce and install one.
219
+
220
+ The same `receipt.json` also goes verbatim into the `qualification` field of an
221
+ `agent app-server upgrade plan|apply --request <absolute>.json` document. The
222
+ upgrade request requires that receipt's version pair to match the exact current
223
+ and target generation versions.
224
+
225
+ The `Generation Pool Qualification` workflow is the scheduled-lane counterpart.
226
+ It is `workflow_dispatch`-only and takes the pair as inputs: the qualification
227
+ needs a credentialed Codex state domain, which a GitHub-hosted runner does not
228
+ have, so a nightly run could only ever report `unsupported`. The lane installs
229
+ both declared versions, runs the same script, uploads the typed record, and is
230
+ green only on a measured `pass`.
231
+
232
+ Payload-free fresh create now has a stronger product boundary than the
233
+ generation model: canonical CLI, shortcut, and default AI intent choose the
234
+ plain-interactive lane before consulting `Current` or `Resolve`. They therefore
235
+ create no generation obligation, provider thread, turn, or resume barrier. The
236
+ generation-pool qualification below may still exercise a payload-free thread
237
+ directly as a provider capability probe; that lower-layer negative evidence is
238
+ not functional `projmux create codex` success.
239
+
240
+ Payload-free executable qualification is also distinct from pool health. The
241
+ `internal/integrations/agents/codexgeneration` record keys RoleTUI and
242
+ RoleAppServer digests, protocol, bound private socket route, state domain, and
243
+ platform/arch, then reduces stored zero-turn resume separately from remote-new
244
+ first-real-input identity. A healthy Current generation, successful
245
+ `thread/read`, or living TUI does not promote either predicate. Doctor and the
246
+ create planner share that record projection, but Phase 1 keeps every projected
247
+ create route on the Phase-0 plain fallback. Generation admission, drain,
248
+ handover, lease lifecycle, and first-turn production binding are unchanged.
249
+
250
+ ## Phase 1 lifecycle projection
251
+
252
+ `codexgeneration.ProjectLifecycle` is the only interaction-plus-generation
253
+ mapper. A dash below means that the exact tmux option is absent. Draining,
254
+ handover-pending, recovering, and blocked rows require an operation ref whose
255
+ endpoint exactly matches the durable generation input; without that marker the
256
+ tuple is empty rather than inferred from a process exit or version change.
257
+
258
+ | Effective interaction | Preparing | Current | Draining | Handover pending | Retired | Recovering | Blocked |
259
+ | --- | --- | --- | --- | --- | --- | --- | --- |
260
+ | unknown | `-/-/-` | `-/-/-` | `draining/draining/-` | `draining/handover_pending/-` | `-/-/-` | `recovering/recovering/-` | `blocked/blocked/-` |
261
+ | idle | `-/-/-` | `idle/-/-` | `draining/draining/-` | `draining/handover_pending/-` | `-/-/-` | `recovering/recovering/-` | `blocked/blocked/-` |
262
+ | in progress | `-/-/-` | `thinking/in_progress/busy` | `draining/in_progress/busy` | `draining/handover_pending/-` | `-/-/-` | `recovering/recovering/-` | `blocked/blocked/-` |
263
+ | approval required | `-/-/-` | `waiting/approval_required/reply` | `draining/approval_required/reply` | `draining/handover_pending/-` | `-/-/-` | `recovering/recovering/-` | `blocked/blocked/-` |
264
+ | input required | `-/-/-` | `waiting/input_required/reply` | `draining/input_required/reply` | `draining/handover_pending/-` | `-/-/-` | `recovering/recovering/-` | `blocked/blocked/-` |
265
+ | response complete | `-/-/-` | `waiting/response_complete/reply` | `draining/response_complete/reply` | `draining/handover_pending/-` | `-/-/-` | `recovering/recovering/-` | `blocked/blocked/-` |
266
+
267
+ Provider-neutral callers use the same interaction tuples as `Current` without
268
+ claiming generation authority. Explicit generation events additionally require
269
+ the exact durable Agent endpoint and the live activation authority tuple:
270
+
271
+ ```text
272
+ stateDomainID + endpointGenerationID + brokerRuntimeID
273
+ + connectionEpoch + bindingEpoch
274
+ ```
275
+
276
+ The event endpoint, durable endpoint, stored durable state/operation, stored
277
+ activation authority, presented authority, and exact target runtime are
278
+ compared before either bounded writer runs. A provider event cannot authorize a
279
+ syntactically valid operation that differs from the stored operation. Local
280
+ epoch numbers are never compared outside the endpoint and broker-runtime
281
+ namespace.
282
+
283
+ | Owner | Fence | Target | Effect |
284
+ | --- | --- | --- | --- |
285
+ | owner | current | target | semantic effect |
286
+ | owner | current | sibling | zero write |
287
+ | owner | stale | target | zero write |
288
+ | owner | stale | sibling | zero write |
289
+ | foreign | current | target | zero write |
290
+ | foreign | current | sibling | zero write |
291
+ | foreign | stale | target | zero write |
292
+ | foreign | stale | sibling | zero write |
293
+
294
+ Every native semantic `Apply` owns the existing exact-Pane authority fence for
295
+ its complete Registry/queue/tmux write set, including the provider-neutral
296
+ lane. `SetAuthority` therefore cannot invalidate between an older Apply's
297
+ Registry and tmux halves. Generation-aware Apply additionally repeats its
298
+ composite comparison inside the Registry transaction and after the Registry
299
+ commit before presentation writes. The production resource
300
+ reconciler consumes the same durable state/operation and exact stored
301
+ activation fence; an unavailable fence is zero-write rather than a reason to
302
+ overwrite the planned tuple with legacy interaction state. Reconciliation
303
+ compares the desired tuple with the exact live options, so its second full pass
304
+ emits no writes.
305
+
306
+ Contract enforcement is split by invariant rather than duplicated by adapter:
307
+
308
+ - C-1 Generation-pinned routing: `TestRuntimeMutationEquivalenceTableIsClosed`,
309
+ `TestRuntimeMutationClassesMatchDecisionKernel`,
310
+ `TestRuntimeMutationCompositeFenceAndSiblingRecorder`, and
311
+ `TestGenerationLifecycleSinkCompositeAuthorityHasZeroCrossWrites`, plus
312
+ `TestGenerationLifecycleProductionReconcileRejectsForeignOrSiblingAuthorityWithZeroWrites`.
313
+ - C-5 Exact lifecycle projection and actionability:
314
+ `TestGenerationLifecycleProjectionClosedTable`,
315
+ `FuzzGenerationLifecycleProjectionMatchesClosedTable`,
316
+ `TestPlannedGenerationProjectionRequiresExactDurableOperationRef`,
317
+ `TestMarkerlessCrashAndVersionDriftRemainOrdinaryFailure`,
318
+ `TestMarkerlessCrashAndVersionDriftRemainOrdinaryFailureThroughProductionReconcile`,
319
+ `TestGenerationLifecycleProjectionReconcileWritesOnceThenZero`, and
320
+ `TestGenerationLifecycleProjectionUsesIsolatedRealTmuxAndExactCleanup`.
321
+
322
+ ### Mapping and authority test migration ledger
323
+
324
+ No canonical test symbol was deleted. Phase 0 endpoint-schema/authority tests,
325
+ the pure lifecycle reducer property, and the broker reconnect/C01 canaries each
326
+ retain a unique boundary. The one duplicate assertion family was merged in
327
+ place:
328
+
329
+ | Previous assertion or owner | Phase 1 action | Old mutant still detected | New mutant receipt / canonical owner |
330
+ | --- | --- | --- | --- |
331
+ | `agentTmuxProjection` interaction switch plus hard-coded `waiting`/badge expectations in `TestCodexSemanticDeliveryMatrix` | production switch replaced by the core mapper; policy test now consumes its tuple and owns only Notify/State only/Quiet overlay | quiet or state-only incorrectly retains actionability | wrong interaction or generation tuple fails `TestGenerationLifecycleProjectionClosedTable` and `FuzzGenerationLifecycleProjectionMatchesClosedTable` |
332
+ | `TestResourceReconcileProjectsAllAgentFieldsFromRegistryAuthority` | retained at the unique Registry effective-interaction/topic/offline adapter boundary | stale/offline options survive or a repeat writes | planned-state fixed point is owned by `TestGenerationLifecycleProjectionReconcileWritesOnceThenZero` and the isolated real-tmux test |
333
+ | test-only `planAuthorizedGenerationLifecycleProjection` | deleted; fake and real tests now execute `planResourceAgentProjections` with durable Registry lifecycle plus exact activation authority | a generation-aware sink writes a planned tuple, then ordinary production reconcile erases it through the legacy-only input | `TestGenerationLifecycleProjectionReconcileWritesOnceThenZero` fails both the production-input deletion mutant and a second-pass rewrite; the isolated real-tmux test owns the same boundary on an explicit returned physical socket |
334
+ | `TestCompositeAuthorityRejectsSameNumberCrossGenerationAndLegacyWithZeroWrites` | retained as the Phase 0 pure five-part schema fence | missing endpoint namespace authorizes a legacy ref | owner/foreign × current/stale × target/sibling closure is owned by `TestRuntimeMutationEquivalenceTableIsClosed`, `TestRuntimeMutationClassesMatchDecisionKernel`, and `TestRuntimeMutationCompositeFenceAndSiblingRecorder` |
335
+ | native hook authority and reconnect tests, including the C01 sentinel | retained at the provider-hook/control-plane ordering boundary; no generation expectation copied into them | hook writes during pending/invalidating, an older native Apply restores stale Pane semantics after disconnect, or a retired broker epoch writes | `TestNativeCodexHookAuthorityChangeAfterGuardCommitsZero` and `TestNativeSemanticApplyAndInvalidationShareExactPaneFence` close both hook and native-Apply split-write races; `TestGenerationLifecycleSinkCompositeAuthorityHasZeroCrossWrites` rejects reused epochs across generations and broker restarts before Registry or tmux writes |
336
+ | `FuzzCodexLifecycleReferenceModel` and its readable reducer regressions | retained unchanged as the pure provider-event reducer owner | duplicate, stale, foreign, or reordered provider operations mutate reducer state | generation presentation is a downstream mapper and is independently exhausted by the 6×7 table; no reducer transition was reimplemented |
337
+
338
+ The deletion count is zero test symbols, one obsolete test-only authorization
339
+ helper, and one merged duplicate mapping-expectation family. This is
340
+ intentional: removing any retained row
341
+ would delete a distinct schema, adapter, provider-ordering, or reducer mutant.
342
+ The new runtime inventory keeps `agent.presentation` as the sole typed mutation
343
+ surface and records all five live authority dimensions plus durable owner and
344
+ target runtime. Process pool/host, create routing, consumer notification/sidebar/
345
+ statusbar/reply behavior, badge rendering, reducer transitions, and Phase 2+
346
+ remain outside Phase 1.
347
+
348
+ ## Phase 2 endpoint pool and private host
349
+
350
+ `codexbroker.GenerationPool` keys every managed endpoint by the canonical
351
+ `stateDomainID + endpointGenerationID` pair and enforces the Phase 0 two-slot
352
+ bound again at runtime construction. Each generation owns an independent
353
+ Broker, random broker-runtime ID, connection epoch, binding epoch, and
354
+ initialize/snapshot/reconnect/binding ledger. A sibling reconnect cannot touch
355
+ another generation's opener, fence, binding, or provider wire. A broker
356
+ restart closes a generation-local restart fence, restores the exact sorted
357
+ binding ledger, and issues a new broker-runtime ID; authority from the old
358
+ runtime writes zero even when its local epoch numbers repeat. The restart fence
359
+ also refuses a bind or second restart that raced the restore snapshot.
360
+
361
+ Thread routing is the exact endpoint plus exact thread ID. Presenting the same
362
+ thread ID under another state domain or generation returns the typed
363
+ `route-mismatch` refusal before the endpoint wire. `Preparing` readiness is a
364
+ separate fact from admission: Phase 2's `AdmitCreate` always returns
365
+ `admission-closed`, including after a snapshot proves the endpoint ready.
366
+
367
+ `codexgenerationhost` launches only from the Phase 0-qualified immutable
368
+ bundle layout. The required `codex`, `codex-code-mode-host`, bundled `rg`, and
369
+ bundled `bwrap` paths are a closed package-owned set, not caller-overridable
370
+ configuration. The content-addressed lease is re-opened and every manifest,
371
+ role, mode, size, and hash is revalidated before publication and again before
372
+ any lifecycle signal. The versioned socket must be directly below an existing
373
+ owner-private `0700` root and must use the exact endpoint-generation name;
374
+ ambient/default parents, symlinks, permissive roots, and occupied paths are
375
+ never repaired or changed.
376
+
377
+ Lifecycle authority is the full PID plus `Setsid` process-group ID, socket
378
+ device/inode/change-time, executable device/inode/change-time/mode/size/hash,
379
+ bundle ID, endpoint identity, and random endpoint-runtime ID proof. Change time
380
+ keeps replacement fail-closed even when a filesystem immediately reuses a
381
+ socket inode. That private app-server host proof feeds only an exact opener;
382
+ `codexbroker.GenerationPool` separately owns the broker-runtime ID and composite
383
+ connection/binding fence. The existing OS broker Host and hidden CLI remain
384
+ default-only and are not claimed as generation-enabled. Drift in any one axis,
385
+ an exited/reused PID, or bundle-helper drift
386
+ keeps stop/restart/kill effects at zero. Cleanup signals only the revalidated
387
+ private process group and waits for both the repeatable leader-exit channel and
388
+ EOF on an inherited session-lifetime token, including leader-first and
389
+ token-first exit orders, before removing the exact socket or letting the
390
+ installed smoke remove its caller-owned roots.
391
+ Readiness is driven by private-root filesystem events followed by an initialized
392
+ app-server handshake; neither readiness nor cleanup uses a fixed sleep. The
393
+ launch argv always names the leased executable and versioned private socket, so
394
+ mutable current/source removal cannot redirect it. Phase 2 exposes no successful
395
+ lease-release path: process exit and caller claims remain refused, and the later
396
+ journaled handover owner must establish terminal retirement. Phase 2 does not
397
+ delete bundle bytes.
398
+
399
+ The opt-in installed smoke uses only exact private `0.152.0` and `0.152.1`
400
+ leases and a unique empty root:
401
+
402
+ ```sh
403
+ smoke_root="$(mktemp -d "${TMPDIR:-/tmp}/projmux-codex-host-XXXXXX")"
404
+ bundle_root="$(mktemp -d /var/tmp/projmux-codex-host-bundles-XXXXXX)"
405
+ env -u TMUX -u TMUX_PANE \
406
+ PROJMUX_CODEX_GENERATION_HOST_SMOKE_ROOT="$smoke_root" \
407
+ PROJMUX_CODEX_GENERATION_BUNDLE_SMOKE_ROOT="$bundle_root" \
408
+ PROJMUX_CODEX_GENERATION_OLD=/absolute/0.152.0/bin/codex \
409
+ PROJMUX_CODEX_GENERATION_NEW=/absolute/0.152.1/bin/codex \
410
+ PROJMUX_CODEX_GENERATION_SOURCE_HOME=/absolute/private/source-home \
411
+ go test ./internal/testutil/codexinstalled \
412
+ -run '^TestInstalledPrivateGenerationHostDualListenerSmoke$' -count=1 -v
413
+ ```
414
+
415
+ It requires inherited tmux routing to be absent, observes two initialized
416
+ private listeners and complete held leases, compares ambient/default socket
417
+ and PID-record identity before/after, waits for both exact private process
418
+ groups and their token-bearing descendants to exit, and removes only the exact
419
+ two private sockets, leases, and smoke root.
420
+
421
+ ### Phase 2 migration and deletion ledger
422
+
423
+ No canonical test symbol was deleted or renamed. Existing default-endpoint
424
+ broker reconnect, binding, foreign-event, approval, runtime-loss, Phase 0
425
+ bundle/qualification, and Phase 1 composite-fence/projection tests retain their
426
+ unique owners. Phase 2 adds generation-local mutants instead of replacing
427
+ those canaries. Current-pointer/drain, Agent create/resume, foreign adoption,
428
+ Settings/background UX, derived consumers, and unrelated cleanup remain
429
+ excluded for their later phases.
430
+
431
+ ## Phase 4 rolling admission and drain
432
+
433
+ `projmux agent app-server upgrade plan|apply --request <absolute-json>` and
434
+ `resume|abort --operation <ref>` are explicit-only operations over one
435
+ owner-private journal. The journal contains exact state-domain/generation and
436
+ operation identities, private routing material, monotonic receipts, and a
437
+ content-free Agent obligation ledger. It never persists prompt, response,
438
+ approval, transcript, credential, or tmux title content. Plan is read-only and
439
+ reports all Registry, provider, tmux, process, and current-pointer mutation
440
+ counts as zero.
441
+
442
+ Plan re-opens both complete leases read-only and binds the request to the Phase
443
+ 0 tuple before any candidate start. The current and target bundle IDs, manifest
444
+ versions, server/TUI/helper inventory, and qualification old/new version pair
445
+ must agree exactly. Both sockets must be distinct direct children of distinct
446
+ owner-private roots; those roots must be physically non-overlapping with each
447
+ other, the shared state-domain path, and either immutable lease root. Current
448
+ and target must name the same canonical state-domain path. Any mismatch is a
449
+ mutation-zero refusal.
450
+
451
+ Apply durably records launch intent before starting the exact leased successor.
452
+ The operation journal lock spans the final authorization check and physical
453
+ supervisor start, so an abort fence either wins before launch or observes the
454
+ exact operation-owned durable intent. A coordinator crash after supervisor
455
+ launch but before launch-proof publication resumes the existing candidate; it
456
+ cannot start a duplicate. The supervisor captures the exact socket identity,
457
+ keeps a semantic guard for the child lifetime, and cleans only that identity.
458
+ An unknown or rebound socket is refused and never unlinked. CandidateReady and
459
+ in-flight pre-admission abort both clean the exact new candidate while retaining
460
+ the immutable bundle lease. If a crash lands between intent unlink and guard
461
+ unlink, a retry may remove only the exact regular mode-0600 guard after proving
462
+ that it is unlocked and that no socket exists; a locked guard, rebound path, or
463
+ present socket is left untouched and refused.
464
+
465
+ Before candidate-ready publication and again while the Registry admission
466
+ barrier is held, the complete server/TUI/helper lease is re-opened and verified.
467
+ The recorded TUI executable must equal the lease's single `RoleTUI` artifact;
468
+ an arbitrary absolute path or post-proof TUI drift leaves admission-current
469
+ unchanged. The current pointer and the old/new Draining/Current route transition
470
+ are one atomic journal commit relative to Agent create transactions. A create
471
+ therefore uses either the old Current route before the barrier or the new
472
+ Current route after it; it can never create on Draining. Retired receipt rows do
473
+ not consume a live slot, while Current+Draining closes a third upgrade with all
474
+ mutation surfaces at zero.
475
+
476
+ Existing live Agents remain pinned to the old generation for turn, reply,
477
+ approval, observation, and TUI routing. An Offline resume on Draining or
478
+ HandoverPending never takes the ordinary rebind path: it creates or reuses one
479
+ generation-wide HandoverPending operation ref and then refuses with
480
+ `handover-required`. An unconfigured requester fails closed before provider,
481
+ Registry, or tmux writes. The drain ledger classifies exact Agent UID plus old
482
+ generation as `active`, `approval-pending`, `no-turn`, `unknown`,
483
+ `completed-persisted`, or `closed`; the first four remain durable blockers.
484
+
485
+ Every Phase 4 receipt carries seven explicit zero counters:
486
+ `oldEndpointStop`, `successorResume`, `endpointRefCAS`, `paneRelaunch`,
487
+ `retirement`, `leaseRelease`, and `foreignAdoption`. Those effects, same-thread
488
+ successor resume, endpoint-ref CAS, Pane relaunch, retirement, and foreign
489
+ adoption remain Phase 5 work. The default installed Codex endpoint remains
490
+ unmanaged attach-only; an absent rolling journal preserves that read-only route
491
+ and no Phase 4 lifecycle path can stop, restart, kill, or adopt it.
492
+
493
+ The installed observation is opt-in and must use one unique empty temp root,
494
+ which it partitions into separate state-domain, generation, bundle, and XDG
495
+ roots, plus the existing old/new installed bundle inputs:
496
+
497
+ ```sh
498
+ PROJMUX_CODEX_PHASE4_SMOKE_ROOT=/tmp/<unique-empty-root> \
499
+ PROJMUX_CODEX_PHASE4_PROJMUX=/absolute/installed/projmux \
500
+ PROJMUX_CODEX_GENERATION_OLD=/absolute/0.152.0/bin/codex \
501
+ PROJMUX_CODEX_GENERATION_NEW=/absolute/0.152.1/bin/codex \
502
+ PROJMUX_CODEX_GENERATION_SOURCE_HOME=/absolute/private/source-home \
503
+ go test ./internal/testutil/codexinstalled \
504
+ -run '^TestInstalledPrivateRollingAdmissionReceipt$' -count=1 -v
505
+ ```
506
+
507
+ It observes the ambient socket/PID record read-only, starts only fixture-owned
508
+ private process groups, checks the rendered seven-zero receipt and durable
509
+ candidate/admission/drain counters, re-observes the old Draining proof and
510
+ reads its exact content-free thread, then exercises catalog listing without
511
+ requiring that no-turn thread to appear and creates/reads one payload-free thread
512
+ through the journal-selected new Current. It finally uses the exact operation
513
+ proof for semantic teardown. It never signals or adopts the default endpoint.
514
+
515
+ ### Phase 4 migration and deletion ledger
516
+
517
+ No canonical invariant test symbol is deleted or renamed. The canonical graph
518
+ baseline expands only for the four explicit upgrade leaves, and generated CLI
519
+ reference coverage remains owned by its existing exact-tree tests. Phase 0
520
+ qualification, Phase 1 lifecycle fencing, Phase 2 host/pool, and Phase 3
521
+ generation-pinned routing tests retain their unique boundaries. Phase 4 adds
522
+ model/fuzz, an exhaustive ten-coordinator-failpoint plus two-journal-hook restart
523
+ table whose four durable effects each converge exactly once, launch-intent
524
+ recovery, admission/abort race, Phase-0 tuple/root topology refusal, orphan-guard
525
+ recovery, TUI drift, two-slot, Draining/HandoverPending resume, direct old-route
526
+ continuity, and content-free receipt mutants rather than merging or deleting
527
+ those existing suites.
528
+
529
+ ## Phase 5 journaled generation handover
530
+
531
+ `projmux agent app-server handover plan|apply --request <absolute-json>` and
532
+ `resume|abort --operation <ref>` operate on a Phase-5 record linked to the
533
+ unchanged Phase-4 rolling receipt under the same owner-private journal lock.
534
+ Plan is repeatable and read-only. It pins exact Agent UID, Pane UID, live Pane
535
+ runtime ID, Pane activation generation, thread ID, old/successor endpoint tuple,
536
+ owner class, and explicit no-turn decisions. Active, pending-approval, unknown,
537
+ unresolved no-turn, incomplete owner proof, tuple drift, or a successor thread
538
+ already loaded before stop leaves old stop, Pane exit, provider resume, and
539
+ endpoint-ref write at zero.
540
+
541
+ Apply prewrites an intent for every effect. For Projmux-private ownership it
542
+ revalidates the complete old launch authority and every Registry/live-Pane
543
+ tuple immediately before the exact owner stop. Official-managed, unmanaged,
544
+ and unknown ownership run the reversible fences and successor-absence checks,
545
+ then stop at `AwaitingOwnerStop` with lifecycle argv zero until an exact
546
+ user-owned stop receipt is supplied. Explicit no-turn close/replacement is
547
+ required; no automatic selection exists, and an applied close is a forward-only
548
+ boundary.
549
+
550
+ The forward-only order is old stop, one operation-qualified successor resume
551
+ per target, completed persisted snapshot for every target, endpoint-ref CAS,
552
+ same-Pane relaunch, terminal retirement, then lease release. The durable resume
553
+ receipt is content-free and survives a successor restart, so coordinator retry
554
+ does not send a second resume wire; a persisted completed not-loaded snapshot
555
+ can still satisfy the semantic barrier before the CAS. CAS keeps the same Agent,
556
+ Pane, and thread identities while issuing a new Pane activation generation.
557
+ Retirement must observe no remaining old endpoint ref, and the old immutable
558
+ lease remains held until the terminal receipt.
559
+
560
+ If the exact old private process disappeared, a read-only lease/intent probe
561
+ prefers a journaled same-generation durable-host restart. If that exact bundle
562
+ is unavailable but the qualified pair and exact stop authority remain, the
563
+ handover takes the qualified fallback and the normal stop Ensure proves owned
564
+ absence before successor work. No fixed sleep is used; host guard release,
565
+ app-server lifecycle snapshots, Registry CAS, tmux identity mirrors, and Pane
566
+ relaunch markers are the bounded semantic barriers.
567
+
568
+ ### Retirement vacancy
569
+
570
+ The version-pair receipt proves that a thread survives a cross-version resume.
571
+ A draining generation that holds no thread has no subject for that proof, so
572
+ requiring the receipt there is a gate with nothing behind it. That case is real
573
+ and ordinary: `ActivateManagedCurrent` starts a rolling activation on its own
574
+ whenever the running Codex version differs from the managed one, and deleting
575
+ the Agents pinned to the old generation afterwards leaves a generation nobody
576
+ can retire -- the only production caller of the rolling handover request is
577
+ `agent resume` on a live pinned Agent.
578
+
579
+ `codexgeneration.EvaluateRetirementVacancy` names that one case and nothing
580
+ wider. Its evidence is a content-free census of the exact retiring endpoint,
581
+ and every field must be zero *and* both censuses must have actually run:
582
+
583
+ - freshly projected non-closed obligations, taken from a Registry snapshot read
584
+ for this decision. The durable journal ledger is never the input: an
585
+ obligation whose Agent has since been deleted is frozen at its last
586
+ projection, carries no ThreadID, and can neither be recomputed nor traced;
587
+ - Agents whose session ref still names the endpoint, including ones carrying no
588
+ ThreadID. An obligation requires a ThreadID, so this covers that hole on the
589
+ Registry side;
590
+ - Pane activations whose native authority names the endpoint. A Pane outlives
591
+ its Agent record, so the obligation census cannot see this axis at all;
592
+ - threads the shared state domain actually holds that a Registry record still
593
+ binds to the endpoint. The census reads rollout *file names* under
594
+ `<state domain>/sessions`; no rollout body is opened, and a state domain that
595
+ cannot be read yields no verdict of vacancy.
596
+
597
+ A vacant plan reports `request-handover`: `Apply` fires the exact rolling
598
+ handover request itself, through a declared requester seam, then re-plans and
599
+ authorizes the destructive operation from that second census. Everything else
600
+ is unchanged. The `multiple-draining` cap and the two-slot arithmetic are
601
+ untouched -- the freed slot comes from a retirement receipt, not a changed
602
+ topology rule -- retirement still only rewrites route state and obligation
603
+ rows, and no thread file is ever removed. One live obligation, one endpoint-
604
+ bound Registry record, or one unreadable state domain keeps both original
605
+ refusals exactly as they were.
606
+
607
+ ### Phase 5 enforcement and deletion ledger
608
+
609
+ No canonical test symbol is deleted or renamed, and the Phase-4 rolling
610
+ operation plus its seven-zero effect invariant remain unchanged. The command
611
+ graph adds only the four explicit handover leaves. Phase 5 adds a separate
612
+ model/fuzz state machine, coordinator and journal failpoint restart coverage,
613
+ exact ownership/blocker negatives, Registry CAS and durable resume-receipt
614
+ tests, production effect tuple-drift tests, and private installed handover/no-turn
615
+ observations. Prompt/turn/approval resend, provider-content replay or inspection,
616
+ double same-thread ownership, duplicate Agent/Pane identity, foreign lifecycle
617
+ mutation, automatic no-turn migration, live-old same-thread resume, and Phase-6
618
+ consumer/Doctor changes remain enforced at zero.