instar 1.3.1320 → 1.3.1321

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.
Files changed (91) hide show
  1. package/dist/commands/machine.d.ts +20 -0
  2. package/dist/commands/machine.d.ts.map +1 -1
  3. package/dist/commands/machine.js +135 -21
  4. package/dist/commands/machine.js.map +1 -1
  5. package/dist/commands/server.d.ts.map +1 -1
  6. package/dist/commands/server.js +64 -5
  7. package/dist/commands/server.js.map +1 -1
  8. package/dist/commands/setup.d.ts +6 -0
  9. package/dist/commands/setup.d.ts.map +1 -1
  10. package/dist/commands/setup.js +8 -0
  11. package/dist/commands/setup.js.map +1 -1
  12. package/dist/core/Config.d.ts +12 -0
  13. package/dist/core/Config.d.ts.map +1 -1
  14. package/dist/core/Config.js +38 -1
  15. package/dist/core/Config.js.map +1 -1
  16. package/dist/core/MachineIdentity.d.ts +10 -0
  17. package/dist/core/MachineIdentity.d.ts.map +1 -1
  18. package/dist/core/MachineIdentity.js +15 -4
  19. package/dist/core/MachineIdentity.js.map +1 -1
  20. package/dist/core/PostUpdateMigrator.d.ts +8 -0
  21. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  22. package/dist/core/PostUpdateMigrator.js +129 -2
  23. package/dist/core/PostUpdateMigrator.js.map +1 -1
  24. package/dist/core/SubscriptionPool.d.ts +30 -0
  25. package/dist/core/SubscriptionPool.d.ts.map +1 -1
  26. package/dist/core/SubscriptionPool.js +52 -1
  27. package/dist/core/SubscriptionPool.js.map +1 -1
  28. package/dist/core/SubscriptionPoolAuthority.d.ts +33 -0
  29. package/dist/core/SubscriptionPoolAuthority.d.ts.map +1 -1
  30. package/dist/core/SubscriptionPoolAuthority.js +150 -0
  31. package/dist/core/SubscriptionPoolAuthority.js.map +1 -1
  32. package/dist/core/shutdownDeadline.d.ts +26 -0
  33. package/dist/core/shutdownDeadline.d.ts.map +1 -0
  34. package/dist/core/shutdownDeadline.js +25 -0
  35. package/dist/core/shutdownDeadline.js.map +1 -0
  36. package/dist/data/standards-guard-index.json +1 -1
  37. package/dist/data/standards-guard-index.meta.json +2 -2
  38. package/dist/data/standards-registry.meta.json +1 -1
  39. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  40. package/dist/messaging/TelegramAdapter.js +12 -1
  41. package/dist/messaging/TelegramAdapter.js.map +1 -1
  42. package/dist/monitoring/CodexRolloutScan.worker.d.ts +16 -0
  43. package/dist/monitoring/CodexRolloutScan.worker.d.ts.map +1 -0
  44. package/dist/monitoring/CodexRolloutScan.worker.js +49 -0
  45. package/dist/monitoring/CodexRolloutScan.worker.js.map +1 -0
  46. package/dist/monitoring/QuotaCollector.d.ts +8 -0
  47. package/dist/monitoring/QuotaCollector.d.ts.map +1 -1
  48. package/dist/monitoring/QuotaCollector.js +73 -32
  49. package/dist/monitoring/QuotaCollector.js.map +1 -1
  50. package/dist/monitoring/TokenLedger.d.ts +11 -1
  51. package/dist/monitoring/TokenLedger.d.ts.map +1 -1
  52. package/dist/monitoring/TokenLedger.js +79 -37
  53. package/dist/monitoring/TokenLedger.js.map +1 -1
  54. package/dist/scaffold/templates.d.ts.map +1 -1
  55. package/dist/scaffold/templates.js +7 -4
  56. package/dist/scaffold/templates.js.map +1 -1
  57. package/dist/server/machineRoutes.d.ts.map +1 -1
  58. package/dist/server/machineRoutes.js +16 -0
  59. package/dist/server/machineRoutes.js.map +1 -1
  60. package/dist/server/routes.d.ts.map +1 -1
  61. package/dist/server/routes.js +4 -3
  62. package/dist/server/routes.js.map +1 -1
  63. package/dist/threadline/ThreadlineBootstrap.d.ts +9 -0
  64. package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
  65. package/dist/threadline/ThreadlineBootstrap.js +5 -1
  66. package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
  67. package/dist/threadline/client/ThreadlineClient.d.ts +14 -0
  68. package/dist/threadline/client/ThreadlineClient.d.ts.map +1 -1
  69. package/dist/threadline/client/ThreadlineClient.js +18 -0
  70. package/dist/threadline/client/ThreadlineClient.js.map +1 -1
  71. package/package.json +1 -1
  72. package/src/data/builtin-manifest.json +64 -64
  73. package/src/data/standards-guard-index.json +1 -1
  74. package/src/data/standards-guard-index.meta.json +2 -2
  75. package/src/data/standards-registry.meta.json +1 -1
  76. package/src/scaffold/templates.ts +7 -4
  77. package/upgrades/1.3.1321.md +153 -0
  78. package/upgrades/luna-stack-ci-ratchets.eli16.md +9 -0
  79. package/upgrades/side-effects/agent-health-reopen-duplicate.md +58 -0
  80. package/upgrades/side-effects/auth-read-after-secret-migration.md +56 -0
  81. package/upgrades/side-effects/codex-rollout-scan-yield.md +58 -0
  82. package/upgrades/side-effects/join-autostart-lifeline.md +59 -0
  83. package/upgrades/side-effects/join-refuses-copied-identity.md +62 -0
  84. package/upgrades/side-effects/luna-stack-ci-ratchets.md +24 -0
  85. package/upgrades/side-effects/quota-jsonl-off-event-loop.md +54 -0
  86. package/upgrades/side-effects/second-machine-copied-config.md +62 -0
  87. package/upgrades/side-effects/shutdown-hard-deadline.md +62 -0
  88. package/upgrades/side-effects/subscription-pool-legacy-migrate.md +68 -0
  89. package/upgrades/side-effects/threadline-relay-standby.md +54 -0
  90. package/upgrades/side-effects/threadline-wire-thread-id.md +60 -0
  91. package/upgrades/side-effects/wakeup-targets-awake-machine.md +58 -0
@@ -0,0 +1,153 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ `TelegramAdapter.createOrReopenAgentHealthAttentionItem` rethrew a `duplicate-content` hold on its "returned after recovery" post, so callers (the Threadline spawn drain's give-up handler) treated an alert the operator already had as a failed Attention write and retried every 30 s for the whole dedup window (ACT-1304 fault 2, Luna/sagemind log 2026-10-04 12:05-12:18Z). That one hold reason now counts as delivered; every other hold still throws.
9
+
10
+ After `instar pair` externalizes secrets, `.instar/config.json` holds `{ "secret": true }` for `authToken`. Instar's hooks and scripts already read `$INSTAR_AUTH_TOKEN` first, but the CLAUDE.md "API Authentication" instruction still read the config directly and turned the placeholder into a rejected Bearer value (ACT-1303, reported by Luna/sagemind).
11
+
12
+ - `generateClaudeMd` (both auth sections): `$INSTAR_AUTH_TOKEN` → `node .instar/scripts/secret-get.mjs authToken` → config value only when it is a string.
13
+ - `PostUpdateMigrator.migrateClaudeMd` rewrites the old line in existing agents' CLAUDE.md (content-sniffed, idempotent).
14
+
15
+ Codex rollout discovery, file reads, and full parsing now run in a worker thread. The worker relocates the prior scan without changing its selection or results: `listAllRollouts` still selects 500 files by default in descending modification-time order, every selected file is read on every poll, the optional age and project-directory filters are unchanged, and duplicate session IDs retain final-upsert-wins behavior in discovery order. The poller keeps its existing 30-day age window.
16
+
17
+ Worker timeout, error, unexpected-exit, shutdown termination, and overlapping-poll guards prevent a stuck or duplicate scan. The main thread applies compact parsed results with the legacy per-file SQLite upserts.
18
+
19
+ `instar join` installs a standby's LaunchAgent with `server start` (its scaffolded config has no Telegram, and the lifeline needs one), and nothing revisited that once Telegram was configured; join's closing text also told the operator to `instar server start` a second, unsupervised server (instar#2122, Luna/sagemind).
20
+
21
+ - Server boot's auto-start self-heal (macOS) now regenerates the LaunchAgent with the lifeline when Telegram is configured but the plist starts the bare server (`autoStartNeedsLifeline` in `src/commands/setup.ts`).
22
+ - `instar join` no longer suggests starting a server when auto-start was installed.
23
+
24
+ `instar join` used any existing `.instar/machine/identity.json`, so a second machine set up from a copied agent home claimed the first machine's id (ACT-1302, Luna/sagemind's Studio booted as the laptop).
25
+
26
+ - `POST /api/pair` refuses `409 joiner-identity-is-inviter` when the joiner presents the inviter's own machine id, before the pairing code is checked (no attempt spent, no state change).
27
+ - `instar join`: an identity whose private signing key is not on this machine is first offered to boot identity recovery (keychain-authenticated key rebuild keeps the id). If recovery does not restore it, the identity and key files (current and legacy names) are renamed `*.copied-<timestamp>` and a fresh standby identity is minted. On the inviter's 409 the joiner does the same and retries once.
28
+
29
+ Fixes instar#2120. When the quota read from Anthropic fails, `QuotaCollector` estimates usage from the last 7 days of Claude JSONL transcripts. It read each file whole and parsed every line synchronously on the event loop; on ~1.8 GB of transcripts the server stopped answering for 3-20 s every 1-2.5 minutes.
30
+
31
+ - `JsonlParser.parseFileAsync` streams each file line by line; `collectFromJsonl` is async and yields between files.
32
+ - `JsonlParser.accumulateLine` holds the per-line rule for both the sync and streamed paths, so they count identically.
33
+ - Lines without `assistant` are skipped before `JSON.parse`; every counted entry contains it, so totals are unchanged.
34
+
35
+ Fixes the blocker in instar#2122, found while bringing Luna up on a second machine.
36
+
37
+ - **Copied Claude path** (`src/core/Config.ts` → `resolveConfiguredClaudePath`). `sessions.claudePath` that provably does not exist on this machine is replaced by the detected Claude binary, with a warning naming both paths; with none detected the configured value is kept. Previously it was used verbatim, every Claude session died at spawn, and each death revoked the session's Telegram sending credential (`invalid-origin-token`). An existing path, a bare command name, or an unprobeable path is kept as configured — the rule `sessions.frameworkBinaryPaths` already follows.
38
+ - **Half-revoked machine entries** (`src/core/MachineIdentity.ts` → `isRegistryEntryActive`). A registry entry is active only with `status: 'active'` and no `revokedAt`. Used by the active-machine list, awake lookup, nickname lookup, `instar machine` views, the passkey cell-state check and the origin pool audit. A removed identity whose endpoints were this machine's own no longer receives this machine's cross-machine calls (`wrong-recipient`).
39
+
40
+ The server's SIGTERM/SIGINT teardown awaited ~10 subsystem stops with no overall bound, so one hang kept the process alive until SIGKILL (instar#2122: Luna's Studio standby ignored SIGTERM).
41
+
42
+ - `src/core/shutdownDeadline.ts`: `armShutdownDeadline` arms an unref'd timer at teardown start (default 20 s; `INSTAR_SHUTDOWN_DEADLINE_MS` override, floor 2 s); on expiry it logs the step in flight, closes SQLite handles, releases the single-instance lock and exits 1.
43
+ - A second signal during teardown exits at once with the same cleanup.
44
+ - The awaited steps are labelled in the log (`telegram-origin`, `notification-flush`, `tunnel`, `threadline`, `telegram`, `http-server`, `sqlite`).
45
+
46
+ Builds the legacy single-file migration named in the approved spec `subscription-pool-authority-foundation.md` (operation `legacy-migrate`), prompted by instar#2122 (a joined machine inherited the first machine's login locations through the git-shared `.instar/subscription-pool.json`).
47
+
48
+ - `SubscriptionPoolAuthorityStore.migrateLegacy` + `recoverLegacyMigrate`: witness bound to the untouched source's SHA-256+size; staging, finalize, rebuild-from-matching-source, fail-closed recovery per the spec table.
49
+ - `SubscriptionPool.migrateLegacyToMachineLocal`: drops rows whose login home is not on this machine; `persist()` writes to the authority once one is published, even with a legacy file still present.
50
+ - `PostUpdateMigrator.migrateSubscriptionPoolToMachineLocal`: `.gitignore` entry, one-time publication, legacy file removed only when untracked (a tracked copy stays — deleting it would propagate to peers). `GITIGNORE_ENTRIES` gains `.instar/subscription-pool.json`.
51
+
52
+ The relay admits one connection per agent identity, so two machines of one agent displaced each other (instar#2122). `bootstrapThreadline` gains `relayStandby`; `server.ts` passes `!shouldOwnTelegramPoll(config)` — the per-machine `multiMachine.telegramPolling: false` flag the lifeline already uses. A standby logs `relay connection SUPPRESSED (standby: …)` and keeps local Threadline. CLAUDE.md template + migration gain one awareness bullet.
53
+
54
+ `POST /threadline/relay-send` (relay path) recorded `threadId ?? messageId` when the caller passed no thread, while `ThreadlineClient` put its own affinity/minted `thread-…` id on the wire. The outbox, thread log, bridge mirror, delivery tracker, reply waiter and the origin-topic binding were all keyed on the `msg-…` id, so the peer's reply on `thread-…` matched nothing and cold-spawned — refused at a full session cap and held (ACT-1304 fault 3, reported by Luna/sagemind).
55
+
56
+ - New `ThreadlineClient.sendAutoWithThread()` returns `{ messageId, threadId }` with the id that went on the wire.
57
+ - The relay-send route uses it for every record and for its response; `threadline_send` now returns the real conversation id.
58
+
59
+ `wakeup()` named the awake machine from the registry's lagging role field and posted the handoff challenge to its OWN server, which verified the signature with itself as receiver and answered `Invalid challenge signature` (instar#2122).
60
+
61
+ - `resolveAwakeMachine`: live lease holder from the local server's `/health` (`multiMachine.syncStatus.leaseHolder`, active non-revoked entry), else the registry role.
62
+ - `resolveAwakeServerUrl`: probes the awake machine's advertised endpoints + last-known URL with `/health`; never localhost.
63
+
64
+ ## What to Tell Your User
65
+
66
+ If something I warned you about in the Agent Health topic comes back, I no longer keep retrying the same heads-up in the background every 30 seconds.
67
+
68
+ If you've connected a second machine, I no longer get locked out of my own controls when I follow my setup notes. The notes now find my access key wherever it's kept.
69
+
70
+ If you use Codex a lot, I no longer freeze for several seconds every minute or so while counting its usage. The numbers I report are the same.
71
+
72
+ If you add a second machine, once Telegram is set up there it starts at login the same way as the first machine, with my watchdog keeping the server running. The join steps no longer tell you to start a second copy of the server.
73
+
74
+ If you set up a second machine by copying my folder from the first, it now creates its own identity instead of pretending to be the first machine. A machine that has only lost its keys still recovers its own identity.
75
+
76
+ When I can't get my usage numbers straight from Anthropic and have to estimate them from my own logs, I no longer freeze for several seconds while doing it. The estimate itself is the same.
77
+
78
+ If you add a second machine by copying settings from the first, I no longer go silent on the new machine when my settings name a Claude program that only exists on the old one — I find the right one and note the mismatch. A machine you removed also stays removed, even if an old copy of the machine list still calls it active.
79
+
80
+ When my server is asked to stop, it now always stops within about 20 seconds, even if one part of it is stuck, and the log says which part. Updates and restarts no longer have to force-kill it.
81
+
82
+ My list of which Claude or Codex logins live on which machine is now kept separately on each machine. A second machine no longer inherits the first one's login locations, and a login that only exists on the other machine is left off this machine's list. Nothing is deleted on any other machine.
83
+
84
+ When I run on two machines, only the one in charge connects to the agent network; the other stays quiet instead of knocking it off the relay every few seconds.
85
+
86
+ When I message another agent and it answers, the answer now comes back to the conversation where I asked, instead of sometimes getting stuck waiting for a free session.
87
+
88
+ Moving me to another machine with "instar wakeup" now talks to the machine that is really in charge, so the hand-over no longer fails with a signature error.
89
+
90
+ ## Summary of New Capabilities
91
+
92
+ None — a retry loop is removed.
93
+
94
+ None — a broken instruction is corrected for new and existing agents.
95
+
96
+ None — the same token totals, gathered without freezing the server.
97
+
98
+ None — an auto-start repair and corrected join instructions.
99
+
100
+ - `/api/pair` names a copied identity (`joiner-identity-is-inviter`) instead of accepting it.
101
+
102
+ None — the same estimate, without the freeze.
103
+
104
+ - A copied `sessions.claudePath` that is missing on this machine falls back to the detected Claude program, with a boot warning.
105
+ - A machine-list entry carrying a removal date is always treated as removed.
106
+
107
+ - `INSTAR_SHUTDOWN_DEADLINE_MS` sets the teardown bound.
108
+
109
+ None user-visible — the per-machine store the spec already promised is now populated automatically on update.
110
+
111
+ None — a connection gate on an existing flag.
112
+
113
+ - `threadline_send` returns the real conversation id on relay sends, so `threadline_history` on it works.
114
+
115
+ None — a CLI fix.
116
+
117
+ ## Evidence
118
+
119
+ `tests/unit/attention-single-topic-routing.test.ts`: a `duplicate-content` hold on reopen resolves with the item OPEN; an `origin-display-authority-unavailable` hold still rejects. The new test fails on main and passes with the fix; all 21 tests pass.
120
+
121
+ - `tests/unit/auth-read-after-secret-migration.test.ts`: the generated instruction resolves a plain string token, never emits the `{ secret: true }` placeholder (the old line does), and the migration rewrites an existing CLAUDE.md once.
122
+ - Live on this machine with no session env: a vault-externalized agent and a plain-config agent both get `200` from `/jobs` using the new lines; the old read gave `400`.
123
+ - 40 template/migration test files pass; the secret-externalization lint stays green.
124
+
125
+ `TokenLedgerPoller-codex.test.ts` covers responsiveness, repeated full reads, preserved-mtime rewrites, legacy duplicate-session ordering, the 500-file default, worker timeout/error/exit/close, and overlapping polls. The focused Vitest suites and TypeScript build validate the change.
126
+
127
+ - `tests/unit/autostart-needs-lifeline.test.ts`: server plist + Telegram configured → switch; lifeline plist → leave; server plist without Telegram → leave.
128
+ - The predicate matches the real LaunchAgent format on this machine (`<string>lifeline</string>` in the ProgramArguments of Echo's and Luna's plists).
129
+
130
+ - `tests/e2e/multi-machine-http.test.ts`: on the real server path, the inviter's own id is refused with 409 and the code is not spent.
131
+ - `tests/integration/pool-noninteractive-pairing.test.ts`: the inviter's own id is refused with 409, the code is not spent, the registry is unchanged; all 8 pass.
132
+ - `tests/unit/join-copied-identity.test.ts`: a copied identity is set aside (renamed, kept) including legacy key names, and a fresh identity with a different id is minted.
133
+ - Independent second-pass review: two concerns raised and fixed (see `upgrades/side-effects/join-refuses-copied-identity.md`).
134
+
135
+ - `tests/unit/quota-collector.test.ts`: streamed and sync parsing return identical totals on mixed input (CRLF, malformed, out-of-window, non-assistant, missing timestamp); a 20 MB file lets a 1 ms timer fire during the read; all 56 tests pass.
136
+ - Original CPU profiles (in instar#2120) attribute the stalls to `collectFromJsonl` → `parseFile` → `readFileSync`.
137
+
138
+ - Luna's Mac Studio log: 34 sessions "died during startup" while `sessions.claudePath` was `/opt/homebrew/bin/claude` (absent on the Studio); none after it was corrected by hand. Continuous `[mesh-rpc] rejected telegram-origin from <self>: wrong-recipient` from a registry entry with `revokedAt` set and `status: 'active'`.
139
+ - `tests/unit/second-machine-copied-config.test.ts` (8 tests), `tests/unit/Config.test.ts`, `tests/unit/machine-identity.test.ts` and 165 related unit files pass.
140
+
141
+ `tests/unit/shutdown-deadline.test.ts`: the deadline fires with the in-flight step name at exactly the bound, a cancelled timer never fires and is unref'd, and the env override/floor/default resolve as documented.
142
+
143
+ - `tests/unit/subscription-pool-legacy-migrate.test.ts` (7): publish + witness binding + legacy removal; tracked copy kept; refusals; recovery (finalize, rebuild from matching source, fail closed on a mutated source); pool-level drop/keep and persist-to-authority.
144
+ - `tests/unit/PostUpdateMigrator-subscription-pool-machine-local.test.ts` (3): tracked / untracked / no-repo; idempotent.
145
+ - `tests/e2e/subscription-pool-lifecycle.test.ts`: production update path + server boot serve only this machine's logins.
146
+ - 166 related unit files pass.
147
+
148
+ `tests/unit/threadline/ThreadlineBootstrap.test.ts`: with the relay enabled and `relayStandby: true`, no relay client is created, the suppression line is logged, and local components still bootstrap. 148 template/migrator test files pass.
149
+
150
+ - `tests/unit/ThreadlineClient-wire-thread.test.ts`: plaintext and encrypted paths return the wire thread (not the message id); affinity reuse and explicit ids preserved.
151
+ - Relay-send integration suites (nickname, priority, canonical remote agent, negotiator gate, local roundtrip) pass; 141 Threadline unit files (2,335 tests) pass.
152
+
153
+ `tests/unit/wakeup-targets-awake-machine.test.ts` (5): live lease wins over the registry; registry fallback; a revoked/unknown lease holder is ignored; endpoint probing picks the first rope that answers and never localhost; null when none answer.
@@ -0,0 +1,9 @@
1
+ # Combined Luna fixes: two CI checks made green (ELI16)
2
+
3
+ The combined submission of Luna's multi-machine fixes failed two automatic checks on GitHub.
4
+
5
+ 1. **The "no silent fallbacks" counter.** The repository counts every place where an error is caught and quietly ignored, and refuses any change that raises the count. Three new error handlers in the login-list migration ignore errors on purpose: a machine that has no identity yet, a migration record that does not exist, and a legacy file that cannot be read. Each of these is a normal, expected state, and each already falls back in the safe direction. They now carry the required explanatory note inside the handler, so the counter is back at its baseline. No behavior changes.
6
+
7
+ 2. **Documentation coverage.** The repository requires a minimum share of its code classes to be mentioned in the public docs. A new background worker that scans Codex usage files had no mention, which pushed the share just below the floor. The token-ledger section of the observability docs page now names the Codex rollout parser and the worker and says what they do.
8
+
9
+ Nothing about how the agent runs changes; these are comments and documentation only.
@@ -0,0 +1,58 @@
1
+ # Side-Effects Review — a reopened agent-health notice held as a duplicate counts as delivered
2
+
3
+ **Version / slug:** `agent-health-reopen-duplicate`
4
+ **Date:** `2026-10-05`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (narrow error classification on one notice path)`
7
+
8
+ ## Summary of the change
9
+
10
+ ACT-1304 fault 2 (reported by Luna/sagemind). Luna's laptop log, 2026-10-04 12:05-12:18Z: `Threadline.SpawnDrainAttention: Attention write failed: Telegram message held: duplicate-content`, every 30 s for 13+ minutes. The spawn drain's give-up handler calls `TelegramAdapter.createOrReopenAgentHealthAttentionItem`; once the item exists, each call reposts a fixed "returned after recovery" line to the Agent Health topic. The outbound dedup held each repost as a duplicate of the copy already accepted in that topic, the adapter rethrew the hold, and the spawn manager treated it as a failed Attention write and retried. Now a `TelegramOriginHoldError` with reason `duplicate-content` on that reopen post is treated as delivered; every other hold still throws so the caller retries as before.
11
+
12
+ ## Decision-point inventory
13
+
14
+ None added. The dedup gate's decision is unchanged; this changes how one caller interprets "already delivered".
15
+
16
+ ## 1. Over-block
17
+
18
+ Nothing new is refused.
19
+
20
+ ## 2. Under-block
21
+
22
+ `duplicate-content` is returned only when the same text to the same topic was platform-accepted within the dedup window (`OriginContentDedup` returns `content-reservation-pending` for an in-flight copy, which still throws), so the operator has the notice. Other holds (for example `origin-display-authority-unavailable`) still fail and retry.
23
+
24
+ ## 3. Level-of-abstraction fit
25
+
26
+ The adapter owns this send and knows the hold reason; the spawn manager stays generic.
27
+
28
+ ## 4. Signal vs authority compliance
29
+
30
+ No authority added; the dedup gate keeps its authority.
31
+
32
+ ## 4b. Judgment-point check
33
+
34
+ Not a decision point.
35
+
36
+ ## 5. Interactions
37
+
38
+ Ends the 30 s retry loop and its degradation-log noise. The first-creation path already swallowed send errors. Other `sendToTopic` callers are untouched.
39
+
40
+ ## 6. External surfaces
41
+
42
+ No new message is sent; a duplicate stays suppressed.
43
+
44
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
45
+
46
+ Machine-local; the Agent Health lane and the dedup are per machine.
47
+
48
+ ## 8. Rollback cost
49
+
50
+ Revert; no state change.
51
+
52
+ ## Conclusion
53
+
54
+ A one-branch error classification that stops a retry loop on a notice the operator already has.
55
+
56
+ ## Class-Closure Declaration (display-only mirror)
57
+
58
+ `{defectClass: "unbounded-self-action", closure: "n/a", reason: "Adds no self-triggered action: it classifies a duplicate-content hold on one notice as already delivered, which REMOVES the trigger for the callers 30s retry loop. Other holds still throw, bounded by the spawn drain existing give-up latch."}`
@@ -0,0 +1,56 @@
1
+ # Side-Effects Review — the CLAUDE.md API-auth instruction survives secret externalization
2
+
3
+ **Version / slug:** `auth-read-after-secret-migration`
4
+ **Date:** `2026-10-05`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (instruction text + idempotent CLAUDE.md migration)`
7
+
8
+ ## Summary of the change
9
+
10
+ ACT-1303 (reported by Luna/sagemind on Threadline, 2026-10-03): "instar pair moving secrets to {secret:true} breaks scripts that read them." After `SecretMigrator` externalizes `authToken`, `.instar/config.json` holds `{ "secret": true }`. Instar's shipped hooks and scripts were already migrated to read `$INSTAR_AUTH_TOKEN` first with a string-type guard, but the CLAUDE.md template's "API Authentication" section still told every agent to run `AUTH=$(python3 -c "...get('authToken',''))")`, which prints the placeholder object. Every curl an agent built from it was rejected (reproduced in this session: `400` with the config read, `200` with the env token).
11
+
12
+ The template (both occurrences in `generateClaudeMd`) now reads `$INSTAR_AUTH_TOKEN`, then `node .instar/scripts/secret-get.mjs authToken`, then the config only when the value is a string. `PostUpdateMigrator.migrateClaudeMd` rewrites the old line in existing agents' CLAUDE.md (content-sniffed, idempotent).
13
+
14
+ ## Decision-point inventory
15
+
16
+ None. Instruction text and a text migration.
17
+
18
+ ## 1. Over-block
19
+
20
+ Nothing is refused.
21
+
22
+ ## 2. Under-block
23
+
24
+ Agent-authored custom scripts that copied the old one-liner are not rewritten (custom files are never touched); the corrected CLAUDE.md instruction is what agents copy from going forward.
25
+
26
+ ## 3. Level-of-abstraction fit
27
+
28
+ CLAUDE.md is where agents learn this; the migration path is the documented one for CLAUDE.md sections.
29
+
30
+ ## 4. Signal vs authority compliance
31
+
32
+ No authority involved.
33
+
34
+ ## 4b. Judgment-point check
35
+
36
+ Not a decision point.
37
+
38
+ ## 5. Interactions
39
+
40
+ Matches the env-first, string-guarded pattern Instar's hooks already use. The secret-externalization lint over `PostUpdateMigrator.ts` stays green: the old line is assembled from pieces because it is the text being removed.
41
+
42
+ ## 6. External surfaces
43
+
44
+ CLAUDE.md text for new and updated agents. Verified live: on a vault-externalized agent and on a plain-config agent, with no session env, the new lines produce a token the server accepts (`200`).
45
+
46
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
47
+
48
+ Each machine's CLAUDE.md is migrated on its own update; the instruction resolves per machine (session env / local secret store / local config).
49
+
50
+ ## 8. Rollback cost
51
+
52
+ Revert; the migration only rewrites one known line.
53
+
54
+ ## Conclusion
55
+
56
+ Closes the last place Instar itself told agents to read a secret that pairing had moved.
@@ -0,0 +1,58 @@
1
+ # Side-Effects Review — Codex rollout scan runs on a worker thread
2
+
3
+ **Version / slug:** `codex-rollout-scan-yield`
4
+ **Date:** `2026-10-05`
5
+ **Author:** `Luna (sagemind) drafted; echo completed`
6
+ **Second-pass reviewer:** `independent subagent (see below)`
7
+
8
+ ## Summary of the change
9
+
10
+ instar#2121. `TokenLedger.scanCodexRolloutsAsync` read and parsed up to 500 Codex rollout files synchronously on the server event loop; on a 14 GB / ~54k-file store that blocked the server 8-12 s about once a minute (a CPU profile put ~84% of frozen time here). Discovery, reads and parsing now run in `src/monitoring/CodexRolloutScan.worker.ts`; the main thread only applies the legacy per-file SQLite upserts in the worker's discovery order. Same `listAllRollouts` selection (500, descending mtime), same age cutoff and cwd attribution, same latest-upsert-wins behavior, no cache.
11
+
12
+ ## Decision-point inventory
13
+
14
+ None. Observability-only scan (read-only over Codex files, writes only the token ledger).
15
+
16
+ ## 1. Over-block
17
+
18
+ Nothing is refused. An overlapping poll while a scan runs reports zero and starts no second worker; the next poll scans normally.
19
+
20
+ ## 2. Under-block
21
+
22
+ SQLite upserts stay synchronous on the main thread and can pause behind a competing writer; the rows are small. Selected files are still reread every poll, as before.
23
+
24
+ ## 3. Level-of-abstraction fit
25
+
26
+ The fix sits at the one place that did the blocking work. The poller and its cadence are unchanged.
27
+
28
+ ## 4. Signal vs authority compliance
29
+
30
+ No authority. Token observability never gates anything.
31
+
32
+ ## 4b. Judgment-point check
33
+
34
+ Not a decision point.
35
+
36
+ ## 5. Interactions
37
+
38
+ A worker error, unexpected exit or 120 s timeout now rejects the scan (logged by the poller's `onError`) where an in-process failure returned zeros; a later poll retries. `close()` terminates an active worker; `finally` resets the running flag. No heap cap on the worker, so one large rollout cannot fail the whole scan (the in-process scan had the main heap). Starting a worker per poll costs roughly 30-50 ms and ~10 MB briefly.
39
+
40
+ ## 6. External surfaces
41
+
42
+ None visible. `/tokens/*` returns the same totals.
43
+
44
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
45
+
46
+ Machine-local by design: each machine scans its own Codex store into its own ledger.
47
+
48
+ ## 8. Rollback cost
49
+
50
+ Revert the commit; no schema or data change.
51
+
52
+ ## Conclusion
53
+
54
+ A relocation of existing work off the event loop with parity preserved, covered by ordering, re-read, timeout, overlap, failure, shutdown and responsiveness tests.
55
+
56
+ ## Second-pass review (if required)
57
+
58
+ Two passes on Luna's draft (astra) found three parity breaks (a size/mtime skip cache, changed processing order, batching/limit changes); all were removed. Independent subagent review on the final diff, 2026-10-05: promise settling, close-during-scan, data copy (`ParsedCodexSession` is strings/numbers/nulls) and parity all check out. **Concern raised:** the 512 MB worker heap cap let one oversized rollout fail every file in the scan. Resolved by removing the cap. A second note — the `src`-run fallback to `dist/` could test a stale worker — is covered by `tests/setup/build-dist.globalSetup.ts`, which rebuilds `dist` before every vitest run. Concur after the fix.
@@ -0,0 +1,59 @@
1
+ # Side-Effects Review — a joined machine ends up supervised by the lifeline, like the first
2
+
3
+ **Version / slug:** `join-autostart-lifeline`
4
+ **Date:** `2026-10-05`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (auto-start self-heal extends an existing check; join output text)`
7
+
8
+ ## Summary of the change
9
+
10
+ instar#2122 (Luna/sagemind second-machine bring-up): "`instar join` installed a `server start` LaunchAgent instead of `lifeline start` (unlike the first machine and Echo's Studio), and it started a stray server outside the supervisor."
11
+
12
+ `instar join` scaffolds a machine-local config with `messaging: []` (the Telegram token is machine-local), so `installAutoStart` correctly picks `server start` at join time — the lifeline refuses to run without Telegram config. Nothing ever revisited that once Telegram was configured on the standby. And join's closing text told the operator to run `instar server start`, though auto-start had already launched one, producing a second, unsupervised server.
13
+
14
+ 1. Server boot's existing auto-start self-heal (which already regenerates stale plist formats) now also regenerates the LaunchAgent with the lifeline when Telegram is configured but the plist starts the bare server (`autoStartNeedsLifeline` in `setup.ts`).
15
+ 2. `instar join` no longer tells the operator to start a server when auto-start was installed.
16
+
17
+ ## Decision-point inventory
18
+
19
+ One added condition in the boot self-heal: "Telegram configured AND plist lacks `lifeline`" → reinstall.
20
+
21
+ ## 1. Over-block
22
+
23
+ Nothing is refused. An agent with Telegram configured that deliberately runs a server-only LaunchAgent is switched to the lifeline — the same outcome the self-heal already produces whenever it reinstalls for any other reason (it passes the Telegram flag).
24
+
25
+ ## 2. Under-block
26
+
27
+ macOS only (the plist check lives in the darwin branch, like the existing format checks). Linux systemd units are unchanged. The standby lifeline's own Telegram-polling behaviour is unchanged (it already suppresses polling under `multiMachine.telegramPolling: false`).
28
+
29
+ ## 3. Level-of-abstraction fit
30
+
31
+ Extends the one place that already repairs auto-start drift at boot.
32
+
33
+ ## 4. Signal vs authority compliance
34
+
35
+ No new authority over agent behaviour; an install-time repair.
36
+
37
+ ## 4b. Judgment-point check
38
+
39
+ Not a competing-signals decision.
40
+
41
+ ## 5. Interactions
42
+
43
+ Reinstalling reloads the LaunchAgent, which restarts the server once under the lifeline — the same one-time restart the existing format self-heals cause. On the next boot the plist matches and nothing further happens.
44
+
45
+ ## 6. External surfaces
46
+
47
+ One boot log line when it switches; `instar join` closing text.
48
+
49
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
50
+
51
+ Machine-local by design: each machine's LaunchAgent is its own.
52
+
53
+ ## 8. Rollback cost
54
+
55
+ Revert; a switched plist keeps running the lifeline, which is the intended state.
56
+
57
+ ## Conclusion
58
+
59
+ A joined standby converges on the same supervision as the first machine once it can, and join stops inviting a duplicate server.
@@ -0,0 +1,62 @@
1
+ # Side-Effects Review — a joining machine never adopts another machine's identity
2
+
3
+ **Version / slug:** `join-refuses-copied-identity`
4
+ **Date:** `2026-10-05`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `independent subagent (see below)`
7
+
8
+ ## Summary of the change
9
+
10
+ ACT-1302 (reported by Luna/sagemind, 2026-10-03): the Mac Studio, set up from a copy of the laptop's agent home, booted as the laptop (`justin-mbp`). `instar join` checked only that `.instar/machine/identity.json` existed ("This machine already has an identity. Using existing."), so a copied identity file made the joiner claim the inviter's machine id. Luna's Studio registry still carries the residue (a removed identity whose endpoints are the Studio's own; see #2123).
11
+
12
+ 1. `POST /api/pair` (inviter) refuses with `409 joiner-identity-is-inviter` when the joiner presents the inviter's own machine id. The check runs before the pairing code is validated, so it spends no attempt and changes no state.
13
+ 2. `instar join` (joiner): if an identity file exists but its private signing key is not on this machine, it first lets this machine's keychain rebuild the keys through the existing boot identity recovery. Only if that fails is the identity treated as a copy: the identity and any key files (current and legacy names) are renamed `*.copied-<timestamp>`, never deleted, and a fresh standby identity is minted. On the inviter's 409 the joiner does the same, re-establishes its pairing recovery root, and retries once.
14
+
15
+ ## Decision-point inventory
16
+
17
+ - Inviter: one new refusal (joiner id equals inviter id).
18
+ - Joiner: whether an existing identity file is this machine's own.
19
+
20
+ ## 1. Over-block
21
+
22
+ The inviter refusal can only match an identity that is literally the inviter's own id. On the joiner, a genuine identity whose key files are missing and which the keychain cannot rebuild (escrow disabled or dry-run, or no keychain backup) is set aside and replaced — the files are kept, so this is reversible, and such a machine could not have signed anything with that identity anyway.
23
+
24
+ ## 2. Under-block
25
+
26
+ A copied home that also carries the inviter's private keys passes the joiner's key check; the inviter's 409 then catches it, and the copied keys (including legacy names) are set aside. A copy joined to a DIFFERENT inviter than the one it was copied from is not caught by the 409; the joiner check still catches it when the keys did not come along.
27
+
28
+ ## 3. Level-of-abstraction fit
29
+
30
+ The inviter is the only party that knows its own id authoritatively; the joiner is where the bad file lives. Both sides act on what they know.
31
+
32
+ ## 4. Signal vs authority compliance
33
+
34
+ The inviter refusal is a deterministic identity-equality check on the pairing endpoint (an existing authority); no brittle heuristic gains authority.
35
+
36
+ ## 4b. Judgment-point check
37
+
38
+ Not a competing-signals decision: equality of ids and possession of a private key are facts.
39
+
40
+ ## 5. Interactions
41
+
42
+ Reuses `runMachineIdentityBootRecovery` (keychain-authenticated key rebuild keeps the id, per the machine-identity-recovery rule) and the existing `establishPairingRecoveryRoot`. The 409 retry sits beside the existing `fresh-pairing-identity-required` retry.
43
+
44
+ ## 6. External surfaces
45
+
46
+ A new 409 error code on `/api/pair`; two new console lines in `instar join`.
47
+
48
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
49
+
50
+ This change exists for multi-machine. The inviter refusal is evaluated on whichever machine issues the code; the joiner check is machine-local by design (it concerns this machine's own files).
51
+
52
+ ## 8. Rollback cost
53
+
54
+ Revert; set-aside files remain on disk and can be renamed back.
55
+
56
+ ## Conclusion
57
+
58
+ Stops a second machine from silently becoming the first, without breaking key recovery on a genuine machine.
59
+
60
+ ## Second-pass review (if required)
61
+
62
+ Independent subagent reviewer, 2026-10-05. **Concern raised (resolved):** treating "no loadable signing key" as proof of a copy would have re-identified a genuine machine that boot recovery can heal from its keychain — fixed by running that recovery first and setting aside only when it does not restore the keys. **Minor (resolved):** legacy key file names were not set aside, leaving another machine's private key live — now renamed too. Confirmed sound: retry ordering (new identity plus recovery root before the retry; the request closure picks up the new identity), the fresh id passes IdentityStore's first-identity path, and answering 409 before the code check leaks only whether a submitted id equals a random 128-bit id.
@@ -0,0 +1,24 @@
1
+ # Side-effects review: luna-stack-ci-ratchets
2
+
3
+ ## Change
4
+ - `src/core/PostUpdateMigrator.ts`: one existing `catch { machineId = undefined; }` reformatted to carry an in-brace `@silent-fallback-ok` comment. Same statement, same control flow.
5
+ - `src/core/SubscriptionPoolAuthority.ts`: `witnessOperation()` and `legacySourceMatches()` catch blocks reformatted with in-brace `@silent-fallback-ok` comments. Same return values (`null`, `false`).
6
+ - `site/src/content/docs/features/observability.md`: one sentence and two component names added to the Token ledger section.
7
+
8
+ ## Over-block / under-block
9
+ None. No runtime logic changed; emitted JavaScript is identical apart from comments.
10
+
11
+ ## Level of abstraction
12
+ Comment annotations satisfy the existing no-silent-fallbacks ratchet; the doc sentence satisfies the docs-coverage floor. Both are the mechanisms the ratchets name.
13
+
14
+ ## Signal vs authority
15
+ No decision points added or changed.
16
+
17
+ ## Interactions
18
+ None. No routes, config, migrations, hooks, or templates touched.
19
+
20
+ ## External surfaces
21
+ Public docs page gains one accurate sentence (verified against the CodexRolloutParser and CodexRolloutScan.worker source headers).
22
+
23
+ ## Rollback
24
+ Revert the commit; only comments and docs change.
@@ -0,0 +1,54 @@
1
+ # Side-Effects Review — Claude usage fallback reads its files without freezing the server
2
+
3
+ **Version / slug:** `quota-jsonl-off-event-loop`
4
+ **Date:** `2026-10-05`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (observability-only read path)`
7
+
8
+ ## Summary of the change
9
+
10
+ instar#2120. When the OAuth quota read fails, `QuotaCollector.collectFromJsonl` estimates usage from the last 7 days of Claude JSONL transcripts. It read each file whole with `readFileSync`, split it, and `JSON.parse`d every line, serially, on the event loop: with 1,875 files / ~1.8 GB the server stopped answering `/ping` and `/health` for 3-20 s every 1-2.5 minutes (CPU profile: `collectFromJsonl` / `parseFile` / `readFileSync` hottest frames). Now `collectFromJsonl` is async: each file is streamed line by line (`JsonlParser.parseFileAsync`, `node:readline` over a read stream) and the loop yields with `setImmediate` between files. Per-line logic moves to `JsonlParser.accumulateLine`, shared by the sync `parseFile` (kept for its callers) and the streamed path, so both count identically. Lines without the substring `assistant` are skipped before `JSON.parse` — an assistant entry always contains it, so results are unchanged and most lines (tool results, user turns) cost a substring check instead of a parse.
11
+
12
+ ## Decision-point inventory
13
+
14
+ None. Estimated quota is observability; the result shape and confidence label ('estimated') are unchanged.
15
+
16
+ ## 1. Over-block
17
+
18
+ Nothing is refused.
19
+
20
+ ## 2. Under-block
21
+
22
+ The total CPU work is the same minus skipped parses; it is now spread across many short turns instead of one long one. A faster OAuth path remains the primary source. Caching per-file counts by mtime would cut repeated work further but complicates the sliding 7-day window; not needed to remove the freeze.
23
+
24
+ ## 3. Level-of-abstraction fit
25
+
26
+ The fix is in the one function that did the blocking work; the polling loop and its cadence are untouched.
27
+
28
+ ## 4. Signal vs authority compliance
29
+
30
+ No authority added or changed.
31
+
32
+ ## 4b. Judgment-point check
33
+
34
+ Not a decision point.
35
+
36
+ ## 5. Interactions
37
+
38
+ `collect()` was already async and awaited by `QuotaManager`; only the fallback step now awaits. A collection takes longer in wall-clock time while other requests interleave; overlapping collections are already serialised by the existing collect flow. The CRLF case is handled identically (readline `crlfDelay: Infinity`; the sync path's trailing `\r` is ignored by `JSON.parse` whitespace rules) — covered by a parity test.
39
+
40
+ ## 6. External surfaces
41
+
42
+ None. Same quota values; the server stays responsive during the fallback.
43
+
44
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
45
+
46
+ Machine-local by design: each machine reads its own transcripts.
47
+
48
+ ## 8. Rollback cost
49
+
50
+ Revert the commit; no state or schema change.
51
+
52
+ ## Conclusion
53
+
54
+ Removes a multi-second event-loop stall from the quota fallback without changing what is counted; parity and responsiveness are pinned by tests.
@@ -0,0 +1,62 @@
1
+ # Side-Effects Review — a second machine no longer goes silent because of state copied from the first
2
+
3
+ **Version / slug:** `second-machine-copied-config`
4
+ **Date:** `2026-10-05`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `independent subagent (see below)`
7
+
8
+ ## Summary of the change
9
+
10
+ instar#2122. Two copied-state defects silenced Luna's second machine:
11
+
12
+ 1. `loadConfig` used `sessions.claudePath` verbatim (`fileConfig.sessions?.claudePath || detectClaudePath()`). A path copied from another machine and absent here made every Claude session die at spawn; each death revoked the session's origin credential, so Telegram replies failed with `invalid-origin-token`. New `resolveConfiguredClaudePath` (src/core/Config.ts) applies the same rule `mergeOperatorBinaryPaths` already applies to `frameworkBinaryPaths`: a path that provably does not exist is ignored in favour of detection, with a warning; a bare name or an unprobeable path is honoured.
13
+ 2. Registry readers trusted `status === 'active'` alone. A hand-copied registry carried `revokedAt` with `status: 'active'`, so a removed identity whose endpoints are this machine's own stayed a live peer and mesh RPCs looped back (`wrong-recipient`). New `isRegistryEntryActive` (status active AND no `revokedAt`) is used by `getActiveMachines`, `isMachineActive`, `getAwakeMachine`, nickname resolution, `instar machine` status/doctor views, the passkey cell-state route, and the origin pool-audit shard list (which previously enumerated every registry key, revoked or not). `MachineIdentityBootRecovery` already used this exact predicate.
14
+
15
+ ## Decision-point inventory
16
+
17
+ - Which Claude binary is spawned (config load) — changed only when the configured path provably does not exist.
18
+ - Which machines count as active peers — narrowed to exclude entries carrying a revocation stamp.
19
+
20
+ ## 1. Over-block
21
+
22
+ A configured path that exists is unchanged. A path absent at boot but installed later (e.g. a binary on an unmounted volume) is replaced by the detected binary until the next restart; a warning names both paths. With no detected binary the configured path is kept. A machine with `revokedAt` set is excluded even if someone set it back to `status: 'active'` by hand without clearing `revokedAt`; `IdentityStore.revoke` always writes both fields, and re-admitting a revoked machine is a deliberate re-pair, so this matches intended semantics.
23
+
24
+ ## 2. Under-block
25
+
26
+ A configured path that exists but is the wrong binary (e.g. a broken wrapper) still spawns dead sessions; that is a separate liveness signal (the "died during startup" warning). The revoke-on-startup-death path itself is unchanged and correct. The other #2122 setup gaps (join LaunchAgent, SIGTERM, git-tracked subscription pool, double Telegram polling, Threadline identity displacement, `instar wakeup` location) are not addressed here and remain open on instar#2122. <!-- tracked: instar#2122 -->
27
+
28
+ ## 3. Level-of-abstraction fit
29
+
30
+ Config load is the single place the binary path is resolved; the registry predicate sits in MachineIdentity next to the readers. Both reuse rules that already existed one layer over.
31
+
32
+ ## 4. Signal vs authority compliance
33
+
34
+ No new blocking authority. The path rule replaces a configured path only when a detected binary exists to replace it with; with nothing detected the configured value stands (unchanged behaviour — this also preserves the hermetic-test convention of a placeholder `claudePath` on hosts without claude, which ~250 test files rely on and which the first cut of this fix broke on CI). The registry predicate narrows a peer list; it blocks nothing.
35
+
36
+ ## 4b. Judgment-point check
37
+
38
+ Not a competing-signals decision point: file existence and a revocation stamp are facts, not heuristics.
39
+
40
+ ## 5. Interactions
41
+
42
+ Peer enumeration feeds lease, presence, origin evidence, pool views and RPC fan-out; every caller already expects revoked machines to be absent, so excluding half-revoked entries only removes a self-addressed peer. Origin `peerEvidence` no longer picks a ghost peer that resolves to self. Pool audit no longer reports a revoked (or `pending`) shard as unreachable; its peer-URL lookup only ever searched active machines, so those shards were always unreadable and marked coverage permanently incomplete.
43
+
44
+ ## 6. External surfaces
45
+
46
+ One new boot warning line when a configured Claude path is missing. `instar machine` shows a half-revoked entry as `[revoked]`. The passkey cell-state route reports `revoked` for it. No API shape changes.
47
+
48
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
49
+
50
+ This change exists for multi-machine. Config resolution is machine-local by design (each machine resolves its own binary). Registry reading is applied identically on every machine; the registry file is not rewritten, so no replication change is needed.
51
+
52
+ ## 8. Rollback cost
53
+
54
+ Revert the commit; no data or state migration. Pure read-side logic.
55
+
56
+ ## Conclusion
57
+
58
+ Two small read-side corrections, each reusing a rule already used elsewhere. Live evidence from Luna's Studio: 34 startup deaths that stopped once the path was corrected by hand, and continuous `wrong-recipient` self-RPCs from the half-revoked entry.
59
+
60
+ ## Second-pass review (if required)
61
+
62
+ Independent subagent reviewer, 2026-10-05: **Concur with the review.** Revoke and restore already write both fields together (`IdentityStore.ts:611-612`), and boot recovery plus IdentityStore already apply the same predicate, so the change brings the remaining readers into line. No caller needs revoked machines; the fallback can only resolve a `claude` binary, so it cannot launch another framework; bare names and backslash-only Windows paths are kept as configured (no change there). Two wording gaps in this review (pool audit also drops `pending`; empty claudePath for non-Claude agents) were folded into §4 and §5 above.