projmux 0.15.2 → 0.16.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.
@@ -1,623 +0,0 @@
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
- `projmux doctor --section integrations` reports the saved version pairs and
221
- their verdicts even without a generation journal. Text, JSON, and support
222
- `doctor.json` distinguish an empty store from damaged or misfiled receipts;
223
- see [Stored Codex version-pair qualification](codex-stored-qualification.md).
224
-
225
- The same `receipt.json` also goes verbatim into the `qualification` field of an
226
- `agent app-server upgrade plan|apply --request <absolute>.json` document. The
227
- upgrade request requires that receipt's version pair to match the exact current
228
- and target generation versions.
229
-
230
- The `Generation Pool Qualification` workflow is the scheduled-lane counterpart.
231
- It is `workflow_dispatch`-only and takes the pair as inputs: the qualification
232
- needs a credentialed Codex state domain, which a GitHub-hosted runner does not
233
- have, so a nightly run could only ever report `unsupported`. The lane installs
234
- both declared versions, runs the same script, uploads the typed record, and is
235
- green only on a measured `pass`.
236
-
237
- Payload-free fresh create now has a stronger product boundary than the
238
- generation model: canonical CLI, shortcut, and default AI intent choose the
239
- plain-interactive lane before consulting `Current` or `Resolve`. They therefore
240
- create no generation obligation, provider thread, turn, or resume barrier. The
241
- generation-pool qualification below may still exercise a payload-free thread
242
- directly as a provider capability probe; that lower-layer negative evidence is
243
- not functional `projmux create codex` success.
244
-
245
- Payload-free executable qualification is also distinct from pool health. The
246
- `internal/integrations/agents/codexgeneration` record keys RoleTUI and
247
- RoleAppServer digests, protocol, bound private socket route, state domain, and
248
- platform/arch, then reduces stored zero-turn resume separately from remote-new
249
- first-real-input identity. A healthy Current generation, successful
250
- `thread/read`, or living TUI does not promote either predicate. Doctor and the
251
- create planner share that record projection, but Phase 1 keeps every projected
252
- create route on the Phase-0 plain fallback. Generation admission, drain,
253
- handover, lease lifecycle, and first-turn production binding are unchanged.
254
-
255
- ## Phase 1 lifecycle projection
256
-
257
- `codexgeneration.ProjectLifecycle` is the only interaction-plus-generation
258
- mapper. A dash below means that the exact tmux option is absent. Draining,
259
- handover-pending, recovering, and blocked rows require an operation ref whose
260
- endpoint exactly matches the durable generation input; without that marker the
261
- tuple is empty rather than inferred from a process exit or version change.
262
-
263
- | Effective interaction | Preparing | Current | Draining | Handover pending | Retired | Recovering | Blocked |
264
- | --- | --- | --- | --- | --- | --- | --- | --- |
265
- | unknown | `-/-/-` | `-/-/-` | `draining/draining/-` | `draining/handover_pending/-` | `-/-/-` | `recovering/recovering/-` | `blocked/blocked/-` |
266
- | idle | `-/-/-` | `idle/-/-` | `draining/draining/-` | `draining/handover_pending/-` | `-/-/-` | `recovering/recovering/-` | `blocked/blocked/-` |
267
- | in progress | `-/-/-` | `thinking/in_progress/busy` | `draining/in_progress/busy` | `draining/handover_pending/-` | `-/-/-` | `recovering/recovering/-` | `blocked/blocked/-` |
268
- | approval required | `-/-/-` | `waiting/approval_required/reply` | `draining/approval_required/reply` | `draining/handover_pending/-` | `-/-/-` | `recovering/recovering/-` | `blocked/blocked/-` |
269
- | input required | `-/-/-` | `waiting/input_required/reply` | `draining/input_required/reply` | `draining/handover_pending/-` | `-/-/-` | `recovering/recovering/-` | `blocked/blocked/-` |
270
- | response complete | `-/-/-` | `waiting/response_complete/reply` | `draining/response_complete/reply` | `draining/handover_pending/-` | `-/-/-` | `recovering/recovering/-` | `blocked/blocked/-` |
271
-
272
- Provider-neutral callers use the same interaction tuples as `Current` without
273
- claiming generation authority. Explicit generation events additionally require
274
- the exact durable Agent endpoint and the live activation authority tuple:
275
-
276
- ```text
277
- stateDomainID + endpointGenerationID + brokerRuntimeID
278
- + connectionEpoch + bindingEpoch
279
- ```
280
-
281
- The event endpoint, durable endpoint, stored durable state/operation, stored
282
- activation authority, presented authority, and exact target runtime are
283
- compared before either bounded writer runs. A provider event cannot authorize a
284
- syntactically valid operation that differs from the stored operation. Local
285
- epoch numbers are never compared outside the endpoint and broker-runtime
286
- namespace.
287
-
288
- | Owner | Fence | Target | Effect |
289
- | --- | --- | --- | --- |
290
- | owner | current | target | semantic effect |
291
- | owner | current | sibling | zero write |
292
- | owner | stale | target | zero write |
293
- | owner | stale | sibling | zero write |
294
- | foreign | current | target | zero write |
295
- | foreign | current | sibling | zero write |
296
- | foreign | stale | target | zero write |
297
- | foreign | stale | sibling | zero write |
298
-
299
- Every native semantic `Apply` owns the existing exact-Pane authority fence for
300
- its complete Registry/queue/tmux write set, including the provider-neutral
301
- lane. `SetAuthority` therefore cannot invalidate between an older Apply's
302
- Registry and tmux halves. Generation-aware Apply additionally repeats its
303
- composite comparison inside the Registry transaction and after the Registry
304
- commit before presentation writes. The production resource
305
- reconciler consumes the same durable state/operation and exact stored
306
- activation fence; an unavailable fence is zero-write rather than a reason to
307
- overwrite the planned tuple with legacy interaction state. Reconciliation
308
- compares the desired tuple with the exact live options, so its second full pass
309
- emits no writes.
310
-
311
- Contract enforcement is split by invariant rather than duplicated by adapter:
312
-
313
- - C-1 Generation-pinned routing: `TestRuntimeMutationEquivalenceTableIsClosed`,
314
- `TestRuntimeMutationClassesMatchDecisionKernel`,
315
- `TestRuntimeMutationCompositeFenceAndSiblingRecorder`, and
316
- `TestGenerationLifecycleSinkCompositeAuthorityHasZeroCrossWrites`, plus
317
- `TestGenerationLifecycleProductionReconcileRejectsForeignOrSiblingAuthorityWithZeroWrites`.
318
- - C-5 Exact lifecycle projection and actionability:
319
- `TestGenerationLifecycleProjectionClosedTable`,
320
- `FuzzGenerationLifecycleProjectionMatchesClosedTable`,
321
- `TestPlannedGenerationProjectionRequiresExactDurableOperationRef`,
322
- `TestMarkerlessCrashAndVersionDriftRemainOrdinaryFailure`,
323
- `TestMarkerlessCrashAndVersionDriftRemainOrdinaryFailureThroughProductionReconcile`,
324
- `TestGenerationLifecycleProjectionReconcileWritesOnceThenZero`, and
325
- `TestGenerationLifecycleProjectionUsesIsolatedRealTmuxAndExactCleanup`.
326
-
327
- ### Mapping and authority test migration ledger
328
-
329
- No canonical test symbol was deleted. Phase 0 endpoint-schema/authority tests,
330
- the pure lifecycle reducer property, and the broker reconnect/C01 canaries each
331
- retain a unique boundary. The one duplicate assertion family was merged in
332
- place:
333
-
334
- | Previous assertion or owner | Phase 1 action | Old mutant still detected | New mutant receipt / canonical owner |
335
- | --- | --- | --- | --- |
336
- | `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` |
337
- | `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 |
338
- | 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 |
339
- | `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` |
340
- | 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 |
341
- | `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 |
342
-
343
- The deletion count is zero test symbols, one obsolete test-only authorization
344
- helper, and one merged duplicate mapping-expectation family. This is
345
- intentional: removing any retained row
346
- would delete a distinct schema, adapter, provider-ordering, or reducer mutant.
347
- The new runtime inventory keeps `agent.presentation` as the sole typed mutation
348
- surface and records all five live authority dimensions plus durable owner and
349
- target runtime. Process pool/host, create routing, consumer notification/sidebar/
350
- statusbar/reply behavior, badge rendering, reducer transitions, and Phase 2+
351
- remain outside Phase 1.
352
-
353
- ## Phase 2 endpoint pool and private host
354
-
355
- `codexbroker.GenerationPool` keys every managed endpoint by the canonical
356
- `stateDomainID + endpointGenerationID` pair and enforces the Phase 0 two-slot
357
- bound again at runtime construction. Each generation owns an independent
358
- Broker, random broker-runtime ID, connection epoch, binding epoch, and
359
- initialize/snapshot/reconnect/binding ledger. A sibling reconnect cannot touch
360
- another generation's opener, fence, binding, or provider wire. A broker
361
- restart closes a generation-local restart fence, restores the exact sorted
362
- binding ledger, and issues a new broker-runtime ID; authority from the old
363
- runtime writes zero even when its local epoch numbers repeat. The restart fence
364
- also refuses a bind or second restart that raced the restore snapshot.
365
-
366
- Thread routing is the exact endpoint plus exact thread ID. Presenting the same
367
- thread ID under another state domain or generation returns the typed
368
- `route-mismatch` refusal before the endpoint wire. `Preparing` readiness is a
369
- separate fact from admission: Phase 2's `AdmitCreate` always returns
370
- `admission-closed`, including after a snapshot proves the endpoint ready.
371
-
372
- `codexgenerationhost` launches only from the Phase 0-qualified immutable
373
- bundle layout. The required `codex`, `codex-code-mode-host`, bundled `rg`, and
374
- bundled `bwrap` paths are a closed package-owned set, not caller-overridable
375
- configuration. The content-addressed lease is re-opened and every manifest,
376
- role, mode, size, and hash is revalidated before publication and again before
377
- any lifecycle signal. The versioned socket must be directly below an existing
378
- owner-private `0700` root and must use the exact endpoint-generation name;
379
- ambient/default parents, symlinks, permissive roots, and occupied paths are
380
- never repaired or changed.
381
-
382
- Lifecycle authority is the full PID plus `Setsid` process-group ID, socket
383
- device/inode/change-time, executable device/inode/change-time/mode/size/hash,
384
- bundle ID, endpoint identity, and random endpoint-runtime ID proof. Change time
385
- keeps replacement fail-closed even when a filesystem immediately reuses a
386
- socket inode. That private app-server host proof feeds only an exact opener;
387
- `codexbroker.GenerationPool` separately owns the broker-runtime ID and composite
388
- connection/binding fence. The existing OS broker Host and hidden CLI remain
389
- default-only and are not claimed as generation-enabled. Drift in any one axis,
390
- an exited/reused PID, or bundle-helper drift
391
- keeps stop/restart/kill effects at zero. Cleanup signals only the revalidated
392
- private process group and waits for both the repeatable leader-exit channel and
393
- EOF on an inherited session-lifetime token, including leader-first and
394
- token-first exit orders, before removing the exact socket or letting the
395
- installed smoke remove its caller-owned roots.
396
- Readiness is driven by private-root filesystem events followed by an initialized
397
- app-server handshake; neither readiness nor cleanup uses a fixed sleep. The
398
- launch argv always names the leased executable and versioned private socket, so
399
- mutable current/source removal cannot redirect it. Phase 2 exposes no successful
400
- lease-release path: process exit and caller claims remain refused, and the later
401
- journaled handover owner must establish terminal retirement. Phase 2 does not
402
- delete bundle bytes.
403
-
404
- The opt-in installed smoke uses only exact private `0.152.0` and `0.152.1`
405
- leases and a unique empty root:
406
-
407
- ```sh
408
- smoke_root="$(mktemp -d "${TMPDIR:-/tmp}/projmux-codex-host-XXXXXX")"
409
- bundle_root="$(mktemp -d /var/tmp/projmux-codex-host-bundles-XXXXXX)"
410
- env -u TMUX -u TMUX_PANE \
411
- PROJMUX_CODEX_GENERATION_HOST_SMOKE_ROOT="$smoke_root" \
412
- PROJMUX_CODEX_GENERATION_BUNDLE_SMOKE_ROOT="$bundle_root" \
413
- PROJMUX_CODEX_GENERATION_OLD=/absolute/0.152.0/bin/codex \
414
- PROJMUX_CODEX_GENERATION_NEW=/absolute/0.152.1/bin/codex \
415
- PROJMUX_CODEX_GENERATION_SOURCE_HOME=/absolute/private/source-home \
416
- go test ./internal/testutil/codexinstalled \
417
- -run '^TestInstalledPrivateGenerationHostDualListenerSmoke$' -count=1 -v
418
- ```
419
-
420
- It requires inherited tmux routing to be absent, observes two initialized
421
- private listeners and complete held leases, compares ambient/default socket
422
- and PID-record identity before/after, waits for both exact private process
423
- groups and their token-bearing descendants to exit, and removes only the exact
424
- two private sockets, leases, and smoke root.
425
-
426
- ### Phase 2 migration and deletion ledger
427
-
428
- No canonical test symbol was deleted or renamed. Existing default-endpoint
429
- broker reconnect, binding, foreign-event, approval, runtime-loss, Phase 0
430
- bundle/qualification, and Phase 1 composite-fence/projection tests retain their
431
- unique owners. Phase 2 adds generation-local mutants instead of replacing
432
- those canaries. Current-pointer/drain, Agent create/resume, foreign adoption,
433
- Settings/background UX, derived consumers, and unrelated cleanup remain
434
- excluded for their later phases.
435
-
436
- ## Phase 4 rolling admission and drain
437
-
438
- `projmux agent app-server upgrade plan|apply --request <absolute-json>` and
439
- `resume|abort --operation <ref>` are explicit-only operations over one
440
- owner-private journal. The journal contains exact state-domain/generation and
441
- operation identities, private routing material, monotonic receipts, and a
442
- content-free Agent obligation ledger. It never persists prompt, response,
443
- approval, transcript, credential, or tmux title content. Plan is read-only and
444
- reports all Registry, provider, tmux, process, and current-pointer mutation
445
- counts as zero.
446
-
447
- Plan re-opens both complete leases read-only and binds the request to the Phase
448
- 0 tuple before any candidate start. The current and target bundle IDs, manifest
449
- versions, server/TUI/helper inventory, and qualification old/new version pair
450
- must agree exactly. Both sockets must be distinct direct children of distinct
451
- owner-private roots; those roots must be physically non-overlapping with each
452
- other, the shared state-domain path, and either immutable lease root. Current
453
- and target must name the same canonical state-domain path. Any mismatch is a
454
- mutation-zero refusal.
455
-
456
- Apply durably records launch intent before starting the exact leased successor.
457
- The operation journal lock spans the final authorization check and physical
458
- supervisor start, so an abort fence either wins before launch or observes the
459
- exact operation-owned durable intent. A coordinator crash after supervisor
460
- launch but before launch-proof publication resumes the existing candidate; it
461
- cannot start a duplicate. The supervisor captures the exact socket identity,
462
- keeps a semantic guard for the child lifetime, and cleans only that identity.
463
- An unknown or rebound socket is refused and never unlinked. CandidateReady and
464
- in-flight pre-admission abort both clean the exact new candidate while retaining
465
- the immutable bundle lease. If a crash lands between intent unlink and guard
466
- unlink, a retry may remove only the exact regular mode-0600 guard after proving
467
- that it is unlocked and that no socket exists; a locked guard, rebound path, or
468
- present socket is left untouched and refused.
469
-
470
- Before candidate-ready publication and again while the Registry admission
471
- barrier is held, the complete server/TUI/helper lease is re-opened and verified.
472
- The recorded TUI executable must equal the lease's single `RoleTUI` artifact;
473
- an arbitrary absolute path or post-proof TUI drift leaves admission-current
474
- unchanged. The current pointer and the old/new Draining/Current route transition
475
- are one atomic journal commit relative to Agent create transactions. A create
476
- therefore uses either the old Current route before the barrier or the new
477
- Current route after it; it can never create on Draining. Retired receipt rows do
478
- not consume a live slot, while Current+Draining closes a third upgrade with all
479
- mutation surfaces at zero.
480
-
481
- Existing live Agents remain pinned to the old generation for turn, reply,
482
- approval, observation, and TUI routing. An Offline resume on Draining or
483
- HandoverPending never takes the ordinary rebind path: it creates or reuses one
484
- generation-wide HandoverPending operation ref and then refuses with
485
- `handover-required`. An unconfigured requester fails closed before provider,
486
- Registry, or tmux writes. The drain ledger classifies exact Agent UID plus old
487
- generation as `active`, `approval-pending`, `no-turn`, `unknown`,
488
- `completed-persisted`, or `closed`; the first four remain durable blockers.
489
-
490
- Every Phase 4 receipt carries seven explicit zero counters:
491
- `oldEndpointStop`, `successorResume`, `endpointRefCAS`, `paneRelaunch`,
492
- `retirement`, `leaseRelease`, and `foreignAdoption`. Those effects, same-thread
493
- successor resume, endpoint-ref CAS, Pane relaunch, retirement, and foreign
494
- adoption remain Phase 5 work. The default installed Codex endpoint remains
495
- unmanaged attach-only; an absent rolling journal preserves that read-only route
496
- and no Phase 4 lifecycle path can stop, restart, kill, or adopt it.
497
-
498
- The installed observation is opt-in and must use one unique empty temp root,
499
- which it partitions into separate state-domain, generation, bundle, and XDG
500
- roots, plus the existing old/new installed bundle inputs:
501
-
502
- ```sh
503
- PROJMUX_CODEX_PHASE4_SMOKE_ROOT=/tmp/<unique-empty-root> \
504
- PROJMUX_CODEX_PHASE4_PROJMUX=/absolute/installed/projmux \
505
- PROJMUX_CODEX_GENERATION_OLD=/absolute/0.152.0/bin/codex \
506
- PROJMUX_CODEX_GENERATION_NEW=/absolute/0.152.1/bin/codex \
507
- PROJMUX_CODEX_GENERATION_SOURCE_HOME=/absolute/private/source-home \
508
- go test ./internal/testutil/codexinstalled \
509
- -run '^TestInstalledPrivateRollingAdmissionReceipt$' -count=1 -v
510
- ```
511
-
512
- It observes the ambient socket/PID record read-only, starts only fixture-owned
513
- private process groups, checks the rendered seven-zero receipt and durable
514
- candidate/admission/drain counters, re-observes the old Draining proof and
515
- reads its exact content-free thread, then exercises catalog listing without
516
- requiring that no-turn thread to appear and creates/reads one payload-free thread
517
- through the journal-selected new Current. It finally uses the exact operation
518
- proof for semantic teardown. It never signals or adopts the default endpoint.
519
-
520
- ### Phase 4 migration and deletion ledger
521
-
522
- No canonical invariant test symbol is deleted or renamed. The canonical graph
523
- baseline expands only for the four explicit upgrade leaves, and generated CLI
524
- reference coverage remains owned by its existing exact-tree tests. Phase 0
525
- qualification, Phase 1 lifecycle fencing, Phase 2 host/pool, and Phase 3
526
- generation-pinned routing tests retain their unique boundaries. Phase 4 adds
527
- model/fuzz, an exhaustive ten-coordinator-failpoint plus two-journal-hook restart
528
- table whose four durable effects each converge exactly once, launch-intent
529
- recovery, admission/abort race, Phase-0 tuple/root topology refusal, orphan-guard
530
- recovery, TUI drift, two-slot, Draining/HandoverPending resume, direct old-route
531
- continuity, and content-free receipt mutants rather than merging or deleting
532
- those existing suites.
533
-
534
- ## Phase 5 journaled generation handover
535
-
536
- `projmux agent app-server handover plan|apply --request <absolute-json>` and
537
- `resume|abort --operation <ref>` operate on a Phase-5 record linked to the
538
- unchanged Phase-4 rolling receipt under the same owner-private journal lock.
539
- Plan is repeatable and read-only. It pins exact Agent UID, Pane UID, live Pane
540
- runtime ID, Pane activation generation, thread ID, old/successor endpoint tuple,
541
- owner class, and explicit no-turn decisions. Active, pending-approval, unknown,
542
- unresolved no-turn, incomplete owner proof, tuple drift, or a successor thread
543
- already loaded before stop leaves old stop, Pane exit, provider resume, and
544
- endpoint-ref write at zero.
545
-
546
- Apply prewrites an intent for every effect. For Projmux-private ownership it
547
- revalidates the complete old launch authority and every Registry/live-Pane
548
- tuple immediately before the exact owner stop. Official-managed, unmanaged,
549
- and unknown ownership run the reversible fences and successor-absence checks,
550
- then stop at `AwaitingOwnerStop` with lifecycle argv zero until an exact
551
- user-owned stop receipt is supplied. Explicit no-turn close/replacement is
552
- required; no automatic selection exists, and an applied close is a forward-only
553
- boundary.
554
-
555
- The forward-only order is old stop, one operation-qualified successor resume
556
- per target, completed persisted snapshot for every target, endpoint-ref CAS,
557
- same-Pane relaunch, terminal retirement, then lease release. The durable resume
558
- receipt is content-free and survives a successor restart, so coordinator retry
559
- does not send a second resume wire; a persisted completed not-loaded snapshot
560
- can still satisfy the semantic barrier before the CAS. CAS keeps the same Agent,
561
- Pane, and thread identities while issuing a new Pane activation generation.
562
- Retirement must observe no remaining old endpoint ref, and the old immutable
563
- lease remains held until the terminal receipt.
564
-
565
- If the exact old private process disappeared, a read-only lease/intent probe
566
- prefers a journaled same-generation durable-host restart. If that exact bundle
567
- is unavailable but the qualified pair and exact stop authority remain, the
568
- handover takes the qualified fallback and the normal stop Ensure proves owned
569
- absence before successor work. No fixed sleep is used; host guard release,
570
- app-server lifecycle snapshots, Registry CAS, tmux identity mirrors, and Pane
571
- relaunch markers are the bounded semantic barriers.
572
-
573
- ### Retirement vacancy
574
-
575
- The version-pair receipt proves that a thread survives a cross-version resume.
576
- A draining generation that holds no thread has no subject for that proof, so
577
- requiring the receipt there is a gate with nothing behind it. That case is real
578
- and ordinary: `ActivateManagedCurrent` starts a rolling activation on its own
579
- whenever the running Codex version differs from the managed one, and deleting
580
- the Agents pinned to the old generation afterwards leaves a generation nobody
581
- can retire -- the only production caller of the rolling handover request is
582
- `agent resume` on a live pinned Agent.
583
-
584
- `codexgeneration.EvaluateRetirementVacancy` names that one case and nothing
585
- wider. Its evidence is a content-free census of the exact retiring endpoint,
586
- and every field must be zero *and* both censuses must have actually run:
587
-
588
- - freshly projected non-closed obligations, taken from a Registry snapshot read
589
- for this decision. The durable journal ledger is never the input: an
590
- obligation whose Agent has since been deleted is frozen at its last
591
- projection, carries no ThreadID, and can neither be recomputed nor traced;
592
- - Agents whose session ref still names the endpoint, including ones carrying no
593
- ThreadID. An obligation requires a ThreadID, so this covers that hole on the
594
- Registry side;
595
- - Pane activations whose native authority names the endpoint. A Pane outlives
596
- its Agent record, so the obligation census cannot see this axis at all;
597
- - threads the shared state domain actually holds that a Registry record still
598
- binds to the endpoint. The census reads rollout *file names* under
599
- `<state domain>/sessions`; no rollout body is opened, and a state domain that
600
- cannot be read yields no verdict of vacancy.
601
-
602
- A vacant plan reports `request-handover`: `Apply` fires the exact rolling
603
- handover request itself, through a declared requester seam, then re-plans and
604
- authorizes the destructive operation from that second census. Everything else
605
- is unchanged. The `multiple-draining` cap and the two-slot arithmetic are
606
- untouched -- the freed slot comes from a retirement receipt, not a changed
607
- topology rule -- retirement still only rewrites route state and obligation
608
- rows, and no thread file is ever removed. One live obligation, one endpoint-
609
- bound Registry record, or one unreadable state domain keeps both original
610
- refusals exactly as they were.
611
-
612
- ### Phase 5 enforcement and deletion ledger
613
-
614
- No canonical test symbol is deleted or renamed, and the Phase-4 rolling
615
- operation plus its seven-zero effect invariant remain unchanged. The command
616
- graph adds only the four explicit handover leaves. Phase 5 adds a separate
617
- model/fuzz state machine, coordinator and journal failpoint restart coverage,
618
- exact ownership/blocker negatives, Registry CAS and durable resume-receipt
619
- tests, production effect tuple-drift tests, and private installed handover/no-turn
620
- observations. Prompt/turn/approval resend, provider-content replay or inspection,
621
- double same-thread ownership, duplicate Agent/Pane identity, foreign lifecycle
622
- mutation, automatic no-turn migration, live-old same-thread resume, and Phase-6
623
- consumer/Doctor changes remain enforced at zero.