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.
@@ -59,33 +59,70 @@ classifies the whole PR by its title type, not by content.
59
59
 
60
60
  ## PR body
61
61
 
62
- Use this template:
62
+ Use this template. Keep all six sections in this order.
63
+ [`.github/pull_request_template.md`](../.github/pull_request_template.md)
64
+ pre-fills new PRs with the same template; keep the two in sync when either
65
+ changes.
63
66
 
64
67
  ```markdown
65
68
  ## Summary
66
- - 1–3 bullets describing what changed and why.
67
-
68
- ## Test plan
69
- - [ ] make fmt-check
70
- - [ ] make test
71
- - [ ] manual verification step (if relevant)
72
-
73
- ## Globalization
74
- - [ ] No user-facing string changes.
75
- - [ ] User-facing strings are behind `internal/i18n` catalog keys with tests.
76
- - [ ] Non-translated strings are classified as literal/data/debug-only.
69
+ - What changed, in 1–3 bullets.
70
+
71
+ ## Background
72
+ - Why this is needed: the problem, a reproduction, related issues or PRs.
73
+
74
+ ## Changes
75
+ - Behavior and code changes, grouped by area.
76
+ - Breaking: what breaks and how to migrate (if any).
77
+
78
+ ## Scope
79
+ - In scope:
80
+ - Out of scope (and follow-ups):
81
+
82
+ ## Verification
83
+ - [ ] Fast local gates: `make fmt` → `make fix` → `make test`
84
+ - [ ] Long local gates: `make test-integration` → `make test-e2e`
85
+ - [ ] Required CI checks green
86
+ - [ ] Manual steps (if relevant):
87
+ - Globalization (check exactly one):
88
+ - [ ] No user-facing string changes.
89
+ - [ ] User-facing strings are behind `internal/i18n` catalog keys with tests.
90
+ - [ ] Non-translated strings are classified as literal/data/debug-only.
91
+
92
+ ## Measurements
93
+ - (If relevant) before/after numbers, method, environment, run ids.
94
+ Mark each number as observed or inferred.
77
95
  ```
78
96
 
79
- Notes:
80
-
81
- - **Why** matters more than **what**. Diff already shows the what.
82
- - Reference issues with `Closes #<n>` so they auto-close on merge.
83
- - Mention follow-ups explicitly when scope was deliberately deferred.
84
- - For any new or changed user-facing text, check exactly one Globalization
85
- item. Normal UX copy needs a catalog key and test coverage. Commands, paths,
86
- config keys, env vars, provider payloads, locale enum values, product names,
87
- debug logs, and internal diagnostics may stay out of the catalog only when
88
- explicitly classified in the PR body.
97
+ Per-section rules:
98
+
99
+ - **Summary** — the short version a reviewer reads first. The diff already
100
+ shows the code; say what changed in terms of behavior.
101
+ - **Background** — **why** matters more than **what**. State the problem, how
102
+ to reproduce it, and related issues or PRs. Reference issues with
103
+ `Closes #<n>` so they auto-close on merge.
104
+ - **Changes** — group by area rather than by file. Any breaking change must be
105
+ listed here with a migration note, and the title must also carry `!` or the
106
+ body a `BREAKING CHANGE:` footer (see the title rules above).
107
+ - **Scope** — say what is deliberately left out and name the follow-ups
108
+ (issue or PR) instead of leaving deferred work implicit.
109
+ - **Verification** — list the gates in the order
110
+ [AGENTS.md](../AGENTS.md) runs them: fast local gates, then the long local
111
+ gates (which may still be running while CI runs on the published head), then
112
+ the required CI checks, then any manual steps. For any new or changed
113
+ user-facing text, check exactly one Globalization item. Normal UX copy needs
114
+ a catalog key and test coverage. Commands, paths, config keys, env vars,
115
+ provider payloads, locale enum values, product names, debug logs, and
116
+ internal diagnostics may stay out of the catalog only when explicitly
117
+ classified in the PR body.
118
+ - **Measurements** — fill it when the PR claims a change in speed, size, or
119
+ resource use (for example a `perf` PR). Give the method, environment, and
120
+ run ids so the numbers can be reproduced, and mark which values were
121
+ observed and which inferred.
122
+
123
+ For small PRs, **Background** and **Measurements** may be written as `N/A`
124
+ with a one-line reason. Do not delete them, so the section order stays the
125
+ same across PRs.
89
126
 
90
127
  ## Branch protection in effect
91
128
 
@@ -95,6 +132,13 @@ Notes:
95
132
  - Required status checks are the five CI job names `Format`, `Unit Tests`,
96
133
  `NPM Packages`, `Integration Tests`, and `E2E Tests`. The aggregate `Test`
97
134
  job is observed as the project-wide fan-in, but it is not ruleset-required.
135
+ - A required check is a job *name*. Renaming or splitting one of those five
136
+ stops that context from ever being reported, and GitHub holds the PR at
137
+ `expected` forever: every check green, merge blocked. Keep a thin aggregate
138
+ job under the original name with `needs: [<new jobs>]` and `if: always()`.
139
+ Without `if: always()` the job skips on child failure, which is neither green
140
+ nor red. `test/e2e/shard-contract.sh` fails when the `E2E Tests` aggregate is
141
+ missing.
98
142
  - Admin bypass is `pull_request` mode — admin can self-merge without
99
143
  approvals, but the PR itself is mandatory.
100
144
  - Linear history is enforced. The merge methods exposed are
@@ -0,0 +1,97 @@
1
+ # Release
2
+
3
+ This document describes how a projmux version is released. For the npm
4
+ package layout, publish order, and trusted publishing, see
5
+ [npm-distribution.md](npm-distribution.md). For the PR title rules that feed
6
+ the release notes, see [pr-guideline.md](pr-guideline.md).
7
+
8
+ ## Stable releases through release-please
9
+
10
+ `release-please-action` watches `main`, accumulates Conventional Commit
11
+ subjects, and opens or refreshes a "chore(main): release X.Y.Z" PR. That PR
12
+ contains the version bump (`internal/version/version.go` and
13
+ `.release-please-manifest.json`), the `CHANGELOG.md` updates, and the release
14
+ notes.
15
+
16
+ Merging the release PR creates the GitHub Release with auto-generated notes as
17
+ a **draft** (`draft` in `release-please-config.json`).
18
+
19
+ ## Tag creation
20
+
21
+ The same package config sets `force-tag-creation`, so release-please creates
22
+ `refs/tags/vX.Y.Z` during its release pass, before it computes the next release
23
+ PR. That tag creation triggers the tag workflow through the release-please PAT.
24
+
25
+ Do not move tag creation into a post-action workflow step. release-please-action
26
+ creates releases before pull requests, and a delayed tag makes the same run
27
+ treat the just-released commits as unreleased.
28
+
29
+ ## The tag workflow
30
+
31
+ `.github/workflows/release.yml` triggers on the tag push. It runs the shipped
32
+ E2E shard/suite matrix behind the fail-closed `Release E2E Tests` aggregate,
33
+ then builds the linux/darwin × amd64/arm64 matrix and uploads tarballs to the
34
+ drafted release (`gh release upload --clobber`). `Build Release` depends on the
35
+ aggregate rather than on individual shards.
36
+
37
+ Do not add hardcoded notes back to that workflow. release-please owns the
38
+ notes.
39
+
40
+ ## Publish ordering
41
+
42
+ The release becomes visible only in the final `publish-release` job, after
43
+ `publish-npm` succeeds. That ordering is the contract:
44
+
45
+ - Users never see a GitHub release for a version npm cannot install yet.
46
+ - A failed npm publish leaves the release drafted and the workflow red instead
47
+ of shipping a half-published version.
48
+
49
+ See [npm-distribution.md](npm-distribution.md#publish-order) for the npm side
50
+ of that order.
51
+
52
+ ## Release candidates
53
+
54
+ Release candidates are cut **outside** release-please, by the
55
+ `workflow_dispatch`-only `.github/workflows/release-rc.yml`. It takes an
56
+ `X.Y.Z-rc.N` version, drafts a GitHub Release marked `--prerelease` against
57
+ `main` HEAD, then pushes `vX.Y.Z-rc.N` with the release-please PAT, so the same
58
+ `release.yml` builds, uploads, and publishes it.
59
+
60
+ Running it is a deliberate manual act: npm publishes cannot be recalled.
61
+
62
+ ### Do not configure prerelease in release-please
63
+
64
+ Do **not** add `prerelease` or `prerelease-type` to `release-please-config.json`
65
+ to get release candidates. release-please gates the prerelease flag on
66
+ `config.prerelease && (version.preRelease || version.major === 0)`. This
67
+ repository is `0.x`, so the key would stamp *stable* releases as prereleases,
68
+ `releases/latest` would stop resolving, and default-channel updates would go
69
+ silently dead.
70
+
71
+ ### The rc path never touches the manifest
72
+
73
+ The rc path writes exactly one ref: the tag. It never touches
74
+ `.release-please-manifest.json`, so release-please keeps measuring the next
75
+ stable release from the previous *stable* release.
76
+
77
+ release-please picks that boundary by matching the manifest value against a
78
+ tag, with no prerelease filter on that path. An rc holding the manifest slot
79
+ would silently trim the next stable release notes and its `compare/` link.
80
+
81
+ ## Channel branching in release.yml
82
+
83
+ `release.yml` branches on the tag only where the channel is decided:
84
+
85
+ - A prerelease tag publishes npm with `--tag rc` and keeps `--prerelease` when
86
+ the release is undrafted.
87
+ - A stable tag runs exactly as before.
88
+
89
+ Job names, order, and the `publish-npm` → `publish-release` gating are
90
+ identical for both. `test/release_workflow_contract_test.py` executes both step
91
+ scripts against a stable and an rc tag to hold that split.
92
+
93
+ ## Commit subjects
94
+
95
+ Non-Conventional commit subjects on `main` are silently skipped by
96
+ release-please. Keep PR titles strict. Squash merge ensures the PR title is the
97
+ only subject that lands.
@@ -1,7 +1,7 @@
1
1
  # Replacement completion and restorability
2
2
 
3
3
  Three layers of this application hold an execution image, and every consumer of
4
- `make install`, `npm install`, or a provider generation upgrade believes one
4
+ `make install`, `npm install`, or a provider upgrade believes one
5
5
  sentence about all three at once. That sentence is false in a different way on
6
6
  each layer. This document fixes what is guaranteed, what is not, and how a
7
7
  report can be recovered from the tokens the diagnosis prints.
@@ -13,7 +13,7 @@ the `projmux doctor` replacement table:
13
13
  | --- | --- | --- |
14
14
  | `L1` | `installed-executable-image` | the file the installed path publishes |
15
15
  | `L2` | `long-lived-projmux-processes` | every running child of that file |
16
- | `L3` | `provider-sessions-and-generations` | provider sessions and the managed generation pool |
16
+ | `L3` | `provider-sessions-and-generations` | provider sessions, whose app-server lifetime the upstream `codex app-server daemon` owns |
17
17
 
18
18
  `projmux doctor` renders the table on an unfiltered run and under
19
19
  `projmux doctor --section replacement`. It is read-only in the same sense as
@@ -44,22 +44,14 @@ image accepts no new work on any of the three layers*.
44
44
  replace it and is left alone — see *The L2 replacement policy* below. A
45
45
  replacement that has not finished within the drain cutoff is reported as
46
46
  `replacement-cutoff-reached` and is still not ended.
47
- - `L3` cannot enter `draining` without a qualified version pair. Both doors into
48
- a generation switch require a measured receipt for exactly the pair they are
49
- about: managed activation refuses before it writes the journal, and a handover
50
- resume refuses before it drives any effect — the one exception being a
51
- generation nothing is bound to, which the planner has always been allowed to
52
- retire because a receipt proves a thread survives a cross-version resume and
53
- such a generation has no thread to carry. Each refusal names the action that
54
- clears it, and that action is now runnable: `scripts/test-generation-pool-qualification.sh`
55
- measures a declared pair in isolation and
56
- `projmux agent app-server upgrade qualify --receipt <absolute-json>` installs
57
- the receipt it writes where both doors read it. **What is not guaranteed is
58
- the measurement's honesty.** The gate checks that the receipt's coverage
59
- counters can have come from a run — an acceptance claim with no observation
60
- behind it is refused as forged — but a determined forger who writes a coverage
61
- count of one is not contradicted by anything else in the receipt. The gate
62
- makes the receipt state its coverage; it does not attest to it.
47
+ - `L3` is not replaced by an install at all, and this application no longer
48
+ switches an app-server generation: the endpoint lifetime belongs to the
49
+ upstream `codex app-server daemon`, and the private generation pool that once
50
+ owned it — with its upgrade, qualification, and handover commands — is gone.
51
+ What the row still reports is the Registry against provider evidence: a
52
+ `Running` Agent that provider evidence contradicts, and a `Running` Agent that
53
+ nothing can confirm. Absence of both is not evidence of a restore route, so
54
+ restoration stays `unknown`.
63
55
 
64
56
  **Non-Guarantee.** Explicitly outside this contract:
65
57
 
@@ -68,13 +60,14 @@ image accepts no new work on any of the three layers*.
68
60
  `unsupported-platform` with the `unknown` verdict on both axes. This contract
69
61
  does not build a substitute observation for that platform; it states the gap.
70
62
  - The provider's own upstream seamless-upgrade contract. `L3` reasons about the
71
- pool this application manages, never about what the provider promises.
63
+ Registry and the provider evidence it can reach, never about what the provider
64
+ promises.
72
65
  - An externally owned (`unmanaged`) app-server. Its state is not this
73
66
  application's to observe or restore.
74
67
  - Automatic classification of which changes form one invariant. See C-2.
75
68
 
76
69
  **Scope.** In space: one machine's projmux installation, the long-lived
77
- processes it owns, and the generation pool it manages. In time: from the return
70
+ processes it owns, and the provider sessions its Registry records. In time: from the return
78
71
  of one `install` or `update` to the next. **Expiry: a verdict expires when the
79
72
  installed binary's SHA changes.** A table printed before an install describes an
80
73
  installation that no longer exists.
@@ -85,7 +78,7 @@ installation that no longer exists.
85
78
  | --- | --- | --- |
86
79
  | `L1` | this reader's own executable link carries the kernel's `(deleted)` suffix | none for the binary. No copy of the replaced image is retained, and the recovery text the install prints on failure is config convergence, never a rollback. Restorability is `not-restorable` on every supported path. |
87
80
  | `L2` | the whole-fleet vintage census, the newest `install-residue.jsonl` record whose own census observed anything, and the last `install-replacement.json` pass | end and relaunch the residual process through the routes this application already ships. The drain cutoff bounds the wait, and reaching it changes the row rather than the fleet, so a failed replacement leaves every process exactly where a successful one would have found it. |
88
- | `L3` | generation-pool status, its qualification result, and the Running-versus-live-session census | produce a receipt for the pair with `scripts/test-generation-pool-qualification.sh <old> <new> <output-dir>` and install it with `projmux agent app-server upgrade qualify --receipt <absolute-json>`. **The producer is the recovery route**: a pool that can be qualified can be entered, and one that cannot is refused before it enters. The 2026-09-07 measurement — no route back once `draining` was entered without a verdict — held because nothing read a produced receipt; the entry paths now refuse rather than reach that state. |
81
+ | `L3` | the Running-versus-live-session census | there is no projmux-owned generation switch to undo. A `Running` Agent whose provider session is gone is rebound with `projmux agent resume <ref>`, which moves a retired endpoint reference onto the daemon endpoint; a session nothing can confirm has no route this application can establish. |
89
82
 
90
83
  **Enforcement.** `TestDoctorReplacementLayerVerdictsAreFixedByInputCombination`
91
84
  fixes every input combination to its two verdicts and its token.
@@ -93,15 +86,8 @@ fixes every input combination to its two verdicts and its token.
93
86
  to name the evidence that was missing.
94
87
  `TestDoctorReplacementDarwinReportsUnsupportedPlatformForImageAndProcessLayers`
95
88
  holds the platform branch.
96
- `TestDoctorReplacementQualificationMissingMakesGenerationPoolNotRestorable`
97
- holds the `L3` mapping and
98
- `TestDoctorReplacementL3RestorationMovesWhenTheQualificationLaneOpens` holds the
99
- transition across it. For the `L3` guarantee above,
100
- `TestActivateManagedCurrentRefusesEveryUnqualifiedRequestBeforeDraining` and
101
- `TestResumeRefusesAnUnqualifiedPairBeforeAnyHandoverEffect` hold the two doors,
102
- `TestResumeStillRetiresAVacantGenerationWithoutAReceipt` holds the vacancy
103
- exception, and `TestQualificationGateRefusesEvidenceCountersNoObservationBacks`
104
- holds the forgery refusal. For the `L2` guarantee above,
89
+ `TestDoctorReplacementCensusSeparatesRegistryRunningFromLiveProviderSession`
90
+ holds the `L3` census and its mismatch token. For the `L2` guarantee above,
105
91
  `TestBrokerRuntimeDrainsWhenItsOwnImageWasReplaced` holds the vintage entry
106
92
  condition and that a runtime on the installed image is not drained by it,
107
93
  `TestReplacementRolePoliciesMatchTheContractDocument` and
@@ -250,19 +236,14 @@ the token constants in the code equal, in both directions.
250
236
  | `L2` | `install-residue-recorded` | `not-replaced` | `restorable` | no live child was observable, and the newest ledger record whose own census observed anything says that install left residue |
251
237
  | `L2` | `install-residue-clean` | `replaced` | `restorable` | no live child was observable, and the newest ledger record whose own census observed anything says that install left none |
252
238
  | `L2` | `no-observed-processes` | `unknown` | `unknown` | neither the live census nor any ledger record observed anything. Every reading is silent; none says the fleet is clean |
253
- | `L3` | `generation-pool-unobserved` | `unknown` | `unknown` | no pool diagnosis was read |
254
- | `L3` | `registry-running-provider-session-dead` | pool-derived | `not-restorable` | provider evidence contradicts a Registry `Running` Agent |
255
- | `L3` | `qualification-missing` | pool-derived | `not-restorable` | the pool is installed and carries no qualification result. A pool in this state predates the entry gates or had its receipt removed; the row's `pool.action` names the producer that clears it |
256
- | `L3` | `generation-pool-blocked` | pool-derived | `not-restorable` | the pool diagnosis is blocked |
257
- | `L3` | `generation-handover-required` | pool-derived | `restorable` | the pool has a pending operation with a handover route |
258
- | `L3` | `registry-running-session-unobserved` | pool-derived | `unknown` | Running Agents rest on the Registry alone, with no provider handle to check |
259
- | `L3` | `generation-pool-not-installed` | `not-replaced` | `unknown` | no generation journal exists. Absence of a journal is absence of evidence about a restore route, not evidence of one |
260
- | `L3` | `generation-pool-ready` | pool-derived | `restorable` | the pool is installed, qualified, and settled |
261
-
262
- `pool-derived` means the replacement axis follows the pool rather than the
263
- token: `replaced` once a generation is draining, handover-pending, or retired —
264
- because such a generation accepts no new admission, which is exactly the
265
- sentence C-1's Assumption makes — and `not-replaced` otherwise.
239
+ | `L3` | `registry-unobserved` | `unknown` | `unknown` | no Registry was read, so nothing about the provider sessions was observed |
240
+ | `L3` | `registry-running-provider-session-dead` | `not-replaced` | `not-restorable` | provider evidence contradicts a Registry `Running` Agent |
241
+ | `L3` | `registry-running-session-unobserved` | `not-replaced` | `unknown` | Running Agents rest on the Registry alone, with no provider handle to check |
242
+ | `L3` | `provider-sessions-uncontradicted` | `not-replaced` | `unknown` | the Registry was read and no Running Agent is contradicted or unconfirmed. Nothing contradicts the sessions; nothing establishes a restore route for them either |
243
+
244
+ The `L3` replacement axis is `not-replaced` on every reading that reached the
245
+ Registry: an install replaces no provider session, and this application owns no
246
+ app-server generation switch that could replace one.
266
247
 
267
248
  ## Process role vocabulary
268
249
 
@@ -423,15 +404,50 @@ absence is stated by this table's `unsupported-platform` row.
423
404
 
424
405
  `projmux internal install-replace` runs as a step of `make install`, immediately
425
406
  before the residue census so the census measures the fleet the pass left. It
426
- takes the census, splits the residual processes by disposition, dials the
427
- published broker runtime for this state domain, waits a bounded moment, and
407
+ takes the census, splits the residual processes by disposition, discovers the
408
+ published generation-scoped broker endpoints in this state domain, and dials
409
+ those whose PID hints select this executable's residual processes. PID is only
410
+ a selection hint; the existing ownership and credential checks still authorize
411
+ the connection. The legacy default endpoint key locates the directory and is
412
+ not assumed to be the published runtime. The pass waits a bounded moment and
428
413
  writes `install-replacement.json`.
429
414
 
415
+ A welcome from a current-image runtime cannot stand in for a residual target.
416
+ A drain/closing refusal confirms reachability; if the exact socket disappears
417
+ during a failed request, its disappearance proves that target is already gone. The runtime writes `drain-required`
418
+ before an idle drain closes its connection. Completion follows the original
419
+ socket identities and ownership-checked runtime IDs captured for accepted
420
+ targets, so an absent unrelated socket or a successor published at the same
421
+ path cannot distort the drained count. Runtime IDs distinguish successors even
422
+ when the filesystem reuses the original socket's inode. An unreadable or
423
+ untrusted successor record alone does not prove completion.
424
+
430
425
  It **starts nothing** — a replacement pass that launched what it was sent to
431
426
  replace would leave more behind than it found, so it dials and never ensures.
432
- It **signals nothing**: the only request it makes is a socket handshake. And it
433
- never fails an install: it runs after the install has already succeeded, and
434
- everything it could not do is on the record it writes.
427
+ It **signals nothing**: the only request it makes is a socket handshake.
428
+ An unreachable target makes `internal install-replace` exit 1. `make install`
429
+ still runs the residue census, then fails because replacement did not finish.
430
+ The binary publication and config convergence have already completed; this
431
+ failure does not roll them back. Accepted drains that are still carrying work
432
+ remain successful, as do complete, no-target, and unsupported-platform passes.
433
+ Their output is unchanged.
434
+
435
+ On an unreachable result, stderr retains the count and refusal and adds the
436
+ remaining drain targets' role, pid, and mapped executable revision, followed by
437
+ the impact and next action. Targets are rechecked after the refusal; a vanished
438
+ or unreadable target is not invented, and an unavailable build revision is
439
+ `unknown`. These identities are transient terminal output only:
440
+ `install-replacement.json` and the residue ledger retain counts and tokens.
441
+ The replacement record adds `failureStage` on an unsuccessful request:
442
+ `discovery` means target selection, record, or socket validation failed;
443
+ `dial` means the local socket connection failed; `handshake` means the greeting
444
+ or response failed or did not accept a drain. `refusal` keeps its existing broker
445
+ token, when one is available. A current-image welcome has no refusal token.
446
+ Successful, pending, no-target, and unsupported records omit `failureStage`.
447
+ The diagnostic states that later Codex Agents may lack control while the old
448
+ broker remains. Let existing work finish, check that the named processes exit
449
+ naturally, and retry `make install`. If they remain, the operator reviews the
450
+ targets before deciding on termination; the install does not signal them.
435
451
 
436
452
  Its outcome vocabulary is closed, and it reaches the `L2` row as
437
453
  `replacement.outcome`:
@@ -567,13 +583,6 @@ reconstruction.
567
583
  | `replacement.drained` | `L2` | counter |
568
584
  | `replacement.reported` | `L2` | counter |
569
585
  | `registry.observed` | `L3` | `true` / `false` |
570
- | `pool.status` | `L3` | pool status token, or `unobserved` when no pool diagnosis was read |
571
- | `pool.reason` | `L3` | pool reason token |
572
- | `pool.action` | `L3` | pool action token |
573
- | `pool.generations.live` | `L3` | counter |
574
- | `pool.generations.draining` | `L3` | counter |
575
- | `qualification.verdict` | `L3` | qualification verdict token |
576
- | `qualification.reason` | `L3` | qualification reason token |
577
586
  | `sessions.running` | `L3` | counter |
578
587
  | `sessions.live` | `L3` | counter |
579
588
  | `sessions.dead` | `L3` | counter |
@@ -591,6 +600,5 @@ The replacement policy above is the one thing on this page that acts, and it
591
600
  acts through a strictly narrower door. `TestReplacementPathEndsNoProcess` holds
592
601
  the same guard over the policy table and the install pass: they carry no
593
602
  termination, no signalling, and no restart either. **The whole of the action is
594
- a socket handshake, and the runtime decides.** `L1` atomicity and the `L3`
595
- qualification gate are separate work on separate doors, and no change on this
596
- path may alter their behavior.
603
+ a socket handshake, and the runtime decides.** `L1` atomicity is separate work
604
+ on a separate door, and no change on this path may alter its behavior.
@@ -3,8 +3,8 @@
3
3
  Phase 0 provides the read-only attribution contract consumed by the Resource
4
4
  Inspector shipped in Phase 1. `projmux resources`, the client-scoped
5
5
  `resource-inspector` popup, the statusbar range, and `Resources:Open` all keep
6
- the snapshot in memory only for the interactive process lifetime; it remains
7
- outside Session State.
6
+ the snapshot in memory only for the interactive process lifetime; it is never
7
+ persisted.
8
8
 
9
9
  ## Identity and inventory
10
10
 
@@ -1,80 +1,39 @@
1
- # Session Restore
1
+ # Project Startup
2
2
 
3
- Session snapshots are explicit desired-state inputs for one Project. They are
4
- not tmux replay scripts and they are not Registry backups. Snapshot save keeps
5
- the existing v1 schema and storage behavior. In snapshot save's runtime id
6
- duplicate check, a Window or Pane recorded as `MissingRuntime`/`RuntimeUnbound`
7
- does not claim its retained runtime id, so a tmux id reused after a server
8
- restart does not refuse the save; two live Windows or two live Panes sharing an
9
- id are still refused.
3
+ A closed Project starts from its Registry desired state. projmux keeps no other
4
+ saved Project state: the Registry (`registry.json`) is the only input.
10
5
 
11
- ```sh
12
- projmux get snapshots [--session <snapshot-session>]
13
- projmux create snapshot
14
- projmux restore snapshot --session <snapshot-session> [--project <ref> | -p <ref>] --dry-run
15
- projmux restore snapshot --session <snapshot-session> [--project <ref> | -p <ref>] --yes [--client /dev/pts/N]
16
- projmux delete snapshot --session <snapshot-session>
17
- ```
18
-
19
- Restore requires an exact snapshot and an exact, closed target Project. The
20
- dry-run validates both inputs and prints replacement, deletion, preserved-UID,
21
- and lost-conversation-pointer counts with zero Registry, tmux, and snapshot writes.
22
- Snapshot Project/Window/Pane metadata is checked against the exact target owner
23
- chain. A UID held by another root, a cross-kind UID reuse, a Project mismatch,
24
- or conflicting owner metadata refuses before commit. The ordinary Project-open
25
- trust authorization must also approve the exact target root before the Registry
26
- transaction begins.
27
-
28
- After `--yes`, one atomic Registry transaction replaces only the target
29
- Project's descendant Window/Pane/Agent graph and its descendant name
30
- reservations. The Project UID, root, trust metadata, unrelated Projects,
31
- ControlSessions, and source snapshot bytes are preserved. Metadata-bearing
32
- snapshots reuse their exact target-subtree UIDs and preserve a surviving
33
- final-v2 Agent or shell anchor plus a surviving direct default shell.
34
- Metadata-free legacy snapshots reuse target descendants positionally, select
35
- the first valid Window-local Pane as the role-agnostic anchor, select the first
36
- direct shell as the optional default, and mint identities only for missing
37
- items. An Agent-only Window is valid with an empty default. Repeating the same
38
- projection is a Registry zero-diff.
39
-
40
- Resource metadata records the Registry schema that produced it. A v3 snapshot
41
- is projected through the same root-wide same-kind duplicate-group and
42
- destination-closure rule as Registry migration: affected resources receive
43
- their exact UID names, unique names outside the closure are preserved, and no
44
- numeric suffix is minted. A current-v4 snapshot containing a root-wide
45
- same-kind collision is rejected as damaged before trust authorization,
46
- Registry/tmux/provider mutation, or any snapshot write.
47
-
48
- The committed Registry is then converged by the ordinary Project materializer.
49
- For a restored offline Agent-anchor Window, snapshot materialization visibly plans a
50
- lazy default shell, creates the Window from that shell, and stages the Agent on
51
- its retained anchor Pane UID. A successful repeat writes neither Registry nor
52
- topology. Snapshot Agent recipes use the canonical provider launch/resume path,
53
- including their existing fresh-conversation fallback. Stored startup
54
- commands are not directly executed by snapshot restore. A runtime item refusal
55
- does not roll the Registry back: desired state and the source snapshot remain
56
- available for another `Continue project`, and the refusal is reported as an
57
- item notice. If an explicit client is supplied, the final observable step is
58
- `switch-client -c` to the Project's declared session even when the background
59
- continuation has no inherited `TMUX` variable.
60
-
61
- ## Project startup
62
-
63
- A closed Project has exactly two actions:
6
+ A registered closed Project has exactly two actions. A root that is not a
7
+ registered Project is not asked: it opens fresh, which registers it.
64
8
 
65
9
  - `Continue project` opens the current Registry desired state with the ordinary
66
10
  materializer. A retained graph keeps its Project, Window, Pane, and Agent
67
11
  UIDs. A zero-Window Project keeps its Project UID and atomically receives one
68
12
  new canonical Window and shell UID before materialization.
69
- - `Recreate Project` atomically replaces the same-root graph with a new Project
13
+ - `Clear layout and open` clears the Project's saved Window and Agent layout
14
+ and opens it again; the folder, its files, `.projmux/config.toml`, and trust
15
+ stay. It atomically replaces the same-root graph with a new Project
70
16
  UID and one new canonical Window/shell UID chain, after a confirmation naming
71
17
  the exact old Project UID and its Window/Pane/Agent counts. Declining returns
72
18
  to the startup rows and writes nothing. It does not archive or retain the old
73
- generation.
19
+ generation. Its new Window's first Pane follows the saved launch default
20
+ (`tmux-ai-split-mode`), exactly as a Window created from the UI does: the
21
+ choice is made before anything is cleared. The first open of an unregistered
22
+ root, which resolves to the same fresh start, behaves the same.
74
23
 
75
24
  Esc/cancel returns to Projects; it is not an action row. Picker failure falls
76
25
  back to the non-destructive `Continue project` action.
77
26
 
27
+ `Continue project` needs a registered Project. On a root that is not a
28
+ registered Project it refuses with zero Registry writes and points to
29
+ `Clear layout and open` (`continue project unavailable: <root> is not a registered
30
+ Project; choose Clear layout and open`). It never falls back to Fresh on its own.
31
+
32
+ projmux does not save Project state on its own at quit or on a timer.
33
+ `projmux quit` offers only `Quit projmux` and `Cancel`. The hidden `internal tmux autosave-session-state`
34
+ route is kept only so status lines rendered by older installs keep working, and
35
+ it does nothing.
36
+
78
37
  Continue resumes an Agent's exact recorded conversation after interrupted,
79
38
  killed, abnormal, unknown, or unrecorded termination. Intentional and normal
80
39
  termination remain excluded. A recorded receipt must agree on the Agent and
@@ -87,8 +46,8 @@ A missing, blank, malformed, or mismatched conversation ref, a disabled provider
87
46
  a missing workspace, or a resume preparation failure skips the Agent with a
88
47
  reason. Continue never substitutes a new conversation. Shells and other
89
48
  recoverable Agents still converge; an unrecoverable Agent that is itself a
90
- Window's required anchor keeps the existing Window refusal. Explicit snapshot
91
- restore and `agent resume` retain their separate authority.
49
+ Window's required anchor keeps the existing Window refusal. `agent resume`
50
+ retains its separate authority.
92
51
 
93
52
  After Continue commits, its startup summary shows the resumed and skipped Agent
94
53
  totals and `projmux diagnostics log --component topology`. The same counts are
@@ -110,21 +69,28 @@ errors. Dry-run writes no execution event; failed or rolled-back execution
110
69
  records an error with zero committed counts. Journal and display failures are
111
70
  best effort and never change the topology result.
112
71
 
113
- `Recreate Project` never deletes or overwrites autosave or named snapshot files. It
114
- preserves the root, Git/worktrees, trust decision, and all unrelated Registry
72
+ `Clear layout and open` preserves the root, Git/worktrees, trust decision, and all unrelated Registry
115
73
  graphs while changing the Project identity. A rejected commit retains the
116
- exact old Registry preimage. Repeating `Recreate Project` replaces identity again;
74
+ exact old Registry preimage. Repeating `Clear layout and open` replaces identity again;
117
75
  each successful result has exactly one Project claiming the root.
118
76
 
119
- ## Snapshot contents and diagnostics
120
-
121
- Snapshots keep window names, pane cwd/label/title, shell/startup/agent recipes,
122
- AI topic ownership, and provider resume metadata when available. Resume health
123
- in preview is `available`, `stale`, or `unavailable`; confidence derives from
124
- the stored source. Snapshot inspection never reads provider transcript or
125
- conversation database content.
126
-
127
- An approved projection restore records one safe Session State outcome with
128
- aggregate Window/Pane/recipe counts and source `manual`. Paths, commands,
129
- snapshot content, and provider conversation identifiers are never included.
130
- Dry-run remains read-only and records no mutation outcome.
77
+ The saved launch default is used only when the open carries the exact client
78
+ that pressed the row. The order is:
79
+
80
+ 1. Ask. A picker mode (`selective`, the unset default, or `resume`) opens its
81
+ picker on the Pane the row was pressed in, before the old layout is cleared
82
+ or the new Session exists; a provider mode and `shell` are already the
83
+ answer and open nothing.
84
+ 2. Clear the layout and create the new Session with its one shell Pane.
85
+ 3. Fill it: an Agent answer is created in that Window first, and the shell is
86
+ then removed through the canonical Pane delete.
87
+ 4. Move the pressing client onto the finished Session.
88
+
89
+ The client therefore never sees a shell Pane that is about to be replaced.
90
+ Closing the picker without a choice does not stop the open: the Session opens
91
+ with its shell Pane and nothing is said. An open without the exact client -- a
92
+ detached `start project`, a scripted open -- asks nothing, in every mode, and
93
+ keeps the plain shell Pane. A question that cannot be asked, or an answer that
94
+ cannot be filled in, costs one line on that client after the move and keeps the
95
+ shell Pane; the Project stays open either way. `Continue project` and the
96
+ `start project`/`open project` verbs never use the saved launch default.