instar 1.3.1319 → 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 (110) 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 +86 -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/config/ConfigDefaults.d.ts.map +1 -1
  13. package/dist/config/ConfigDefaults.js +6 -0
  14. package/dist/config/ConfigDefaults.js.map +1 -1
  15. package/dist/core/Config.d.ts +12 -0
  16. package/dist/core/Config.d.ts.map +1 -1
  17. package/dist/core/Config.js +38 -1
  18. package/dist/core/Config.js.map +1 -1
  19. package/dist/core/JevReviewFlagShadow.d.ts +161 -0
  20. package/dist/core/JevReviewFlagShadow.d.ts.map +1 -0
  21. package/dist/core/JevReviewFlagShadow.js +456 -0
  22. package/dist/core/JevReviewFlagShadow.js.map +1 -0
  23. package/dist/core/MachineIdentity.d.ts +10 -0
  24. package/dist/core/MachineIdentity.d.ts.map +1 -1
  25. package/dist/core/MachineIdentity.js +15 -4
  26. package/dist/core/MachineIdentity.js.map +1 -1
  27. package/dist/core/PostUpdateMigrator.d.ts +8 -0
  28. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  29. package/dist/core/PostUpdateMigrator.js +138 -3
  30. package/dist/core/PostUpdateMigrator.js.map +1 -1
  31. package/dist/core/SubscriptionPool.d.ts +30 -0
  32. package/dist/core/SubscriptionPool.d.ts.map +1 -1
  33. package/dist/core/SubscriptionPool.js +52 -1
  34. package/dist/core/SubscriptionPool.js.map +1 -1
  35. package/dist/core/SubscriptionPoolAuthority.d.ts +33 -0
  36. package/dist/core/SubscriptionPoolAuthority.d.ts.map +1 -1
  37. package/dist/core/SubscriptionPoolAuthority.js +150 -0
  38. package/dist/core/SubscriptionPoolAuthority.js.map +1 -1
  39. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  40. package/dist/core/devGatedFeatures.js +6 -0
  41. package/dist/core/devGatedFeatures.js.map +1 -1
  42. package/dist/core/shutdownDeadline.d.ts +26 -0
  43. package/dist/core/shutdownDeadline.d.ts.map +1 -0
  44. package/dist/core/shutdownDeadline.js +25 -0
  45. package/dist/core/shutdownDeadline.js.map +1 -0
  46. package/dist/core/types.d.ts +18 -0
  47. package/dist/core/types.d.ts.map +1 -1
  48. package/dist/core/types.js.map +1 -1
  49. package/dist/data/standards-guard-index.json +1 -1
  50. package/dist/data/standards-guard-index.meta.json +2 -2
  51. package/dist/data/standards-registry.meta.json +1 -1
  52. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  53. package/dist/messaging/TelegramAdapter.js +12 -1
  54. package/dist/messaging/TelegramAdapter.js.map +1 -1
  55. package/dist/monitoring/CodexRolloutScan.worker.d.ts +16 -0
  56. package/dist/monitoring/CodexRolloutScan.worker.d.ts.map +1 -0
  57. package/dist/monitoring/CodexRolloutScan.worker.js +49 -0
  58. package/dist/monitoring/CodexRolloutScan.worker.js.map +1 -0
  59. package/dist/monitoring/QuotaCollector.d.ts +8 -0
  60. package/dist/monitoring/QuotaCollector.d.ts.map +1 -1
  61. package/dist/monitoring/QuotaCollector.js +73 -32
  62. package/dist/monitoring/QuotaCollector.js.map +1 -1
  63. package/dist/monitoring/TokenLedger.d.ts +11 -1
  64. package/dist/monitoring/TokenLedger.d.ts.map +1 -1
  65. package/dist/monitoring/TokenLedger.js +79 -37
  66. package/dist/monitoring/TokenLedger.js.map +1 -1
  67. package/dist/scaffold/templates.d.ts +4 -0
  68. package/dist/scaffold/templates.d.ts.map +1 -1
  69. package/dist/scaffold/templates.js +17 -4
  70. package/dist/scaffold/templates.js.map +1 -1
  71. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  72. package/dist/server/CapabilityIndex.js +1 -0
  73. package/dist/server/CapabilityIndex.js.map +1 -1
  74. package/dist/server/machineRoutes.d.ts.map +1 -1
  75. package/dist/server/machineRoutes.js +16 -0
  76. package/dist/server/machineRoutes.js.map +1 -1
  77. package/dist/server/routes.d.ts.map +1 -1
  78. package/dist/server/routes.js +16 -3
  79. package/dist/server/routes.js.map +1 -1
  80. package/dist/threadline/ThreadlineBootstrap.d.ts +9 -0
  81. package/dist/threadline/ThreadlineBootstrap.d.ts.map +1 -1
  82. package/dist/threadline/ThreadlineBootstrap.js +5 -1
  83. package/dist/threadline/ThreadlineBootstrap.js.map +1 -1
  84. package/dist/threadline/client/ThreadlineClient.d.ts +14 -0
  85. package/dist/threadline/client/ThreadlineClient.d.ts.map +1 -1
  86. package/dist/threadline/client/ThreadlineClient.js +18 -0
  87. package/dist/threadline/client/ThreadlineClient.js.map +1 -1
  88. package/package.json +1 -1
  89. package/src/data/builtin-manifest.json +64 -64
  90. package/src/data/standards-guard-index.json +1 -1
  91. package/src/data/standards-guard-index.meta.json +2 -2
  92. package/src/data/standards-registry.meta.json +1 -1
  93. package/src/scaffold/templates.ts +18 -4
  94. package/upgrades/1.3.1320.md +47 -0
  95. package/upgrades/1.3.1321.md +153 -0
  96. package/upgrades/luna-stack-ci-ratchets.eli16.md +9 -0
  97. package/upgrades/side-effects/agent-health-reopen-duplicate.md +58 -0
  98. package/upgrades/side-effects/auth-read-after-secret-migration.md +56 -0
  99. package/upgrades/side-effects/codex-rollout-scan-yield.md +58 -0
  100. package/upgrades/side-effects/jev-review-flag-shadow.md +175 -0
  101. package/upgrades/side-effects/join-autostart-lifeline.md +59 -0
  102. package/upgrades/side-effects/join-refuses-copied-identity.md +62 -0
  103. package/upgrades/side-effects/luna-stack-ci-ratchets.md +24 -0
  104. package/upgrades/side-effects/quota-jsonl-off-event-loop.md +54 -0
  105. package/upgrades/side-effects/second-machine-copied-config.md +62 -0
  106. package/upgrades/side-effects/shutdown-hard-deadline.md +62 -0
  107. package/upgrades/side-effects/subscription-pool-legacy-migrate.md +68 -0
  108. package/upgrades/side-effects/threadline-relay-standby.md +54 -0
  109. package/upgrades/side-effects/threadline-wire-thread-id.md +60 -0
  110. package/upgrades/side-effects/wakeup-targets-awake-machine.md +58 -0
@@ -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.
@@ -0,0 +1,62 @@
1
+ # Side-Effects Review — graceful shutdown has a hard deadline
2
+
3
+ **Version / slug:** `shutdown-hard-deadline`
4
+ **Date:** `2026-10-06`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (bounded exit path; no decision authority)`
7
+
8
+ ## Summary of the change
9
+
10
+ instar#2122: "The Luna server on the Studio ignores SIGTERM. Only SIGKILL stops it, whereas the laptop's stops on SIGTERM." The server's `shutdown` handler awaits ~10 subsystem stops in sequence (origin worker close, notification flush, tunnel, Threadline relay, Telegram, HTTP server) with no overall bound, so one hanging stop kept the process alive indefinitely. Now:
11
+
12
+ 1. `armShutdownDeadline` (new `src/core/shutdownDeadline.ts`) arms an unref'd timer at the start of teardown (default 20 s, `INSTAR_SHUTDOWN_DEADLINE_MS` override, floor 2 s). On expiry it logs the step in flight, closes SQLite handles and releases the single-instance lock best-effort, and exits 1.
13
+ 2. A second SIGINT/SIGTERM during teardown exits immediately with the same cleanup, naming the step.
14
+ 3. The awaited steps are labelled (`telegram-origin`, `notification-flush`, `tunnel`, `threadline`, `telegram`, `http-server`, `sqlite`) so the log says what hung.
15
+
16
+ ## Decision-point inventory
17
+
18
+ None. An exit-path bound.
19
+
20
+ ## 1. Over-block
21
+
22
+ A teardown that genuinely needs more than 20 s is cut short with exit code 1; the operator can raise `INSTAR_SHUTDOWN_DEADLINE_MS`. The existing `process.on('exit')` SQLite net still runs. Resume-UUID saving and the sidecar flush happen early in the sequence, before the steps that have hung in practice.
23
+
24
+ ## 2. Under-block
25
+
26
+ Does not diagnose WHICH subsystem hangs on Luna's Studio; the new step label in the log will. The lifeline's own supervisor restart behaviour is unchanged.
27
+
28
+ ## 3. Level-of-abstraction fit
29
+
30
+ The bound belongs where the sequence is awaited; the helper is extracted only so the timing is testable.
31
+
32
+ ## 4. Signal vs authority compliance
33
+
34
+ No authority added.
35
+
36
+ ## 4b. Judgment-point check
37
+
38
+ Not a decision point.
39
+
40
+ ## 5. Interactions
41
+
42
+ The restart/update paths that send SIGTERM (lifeline supervisor, launchd, `instar server restart`) now see the process exit within the bound instead of escalating to SIGKILL after their own timeouts. Exit code 1 on a forced exit lets a supervisor tell a clean stop from a cut-short one.
43
+
44
+ ## 6. External surfaces
45
+
46
+ Two new log lines on an overrun or a second signal.
47
+
48
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
49
+
50
+ Machine-local by design; each machine's process bounds its own teardown.
51
+
52
+ ## 8. Rollback cost
53
+
54
+ Revert; no state change.
55
+
56
+ ## Conclusion
57
+
58
+ A SIGTERM now always ends the process, and the log names the step that would have hung forever.
59
+
60
+ ## Class-Closure Declaration (display-only mirror)
61
+
62
+ `{defectClass: "unbounded-self-action", closure: "n/a", reason: "no self-triggered action: a one-shot bound on an operator/supervisor-initiated shutdown; it fires at most once per process"}`
@@ -0,0 +1,68 @@
1
+ # Side-Effects Review — subscription pool: legacy shared file → per-machine authority
2
+
3
+ **Version / slug:** `subscription-pool-legacy-migrate`
4
+ **Date:** `2026-10-06`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `independent subagent (see below)`
7
+
8
+ ## Summary of the change
9
+
10
+ Spec `docs/specs/subscription-pool-authority-foundation.md` (converged 2026-08-27, approved by Justin) defines the per-machine authority store and names "PostUpdateMigrator (legacy single-file staged migration)" as a changed file — but that migration was never built: the witness type carries `legacy-migrate`, nothing wrote it. Meanwhile instar#2122 showed the cost: the legacy `.instar/subscription-pool.json` was shared between Luna's laptop and Studio through the agent home's git repo, so the Studio inherited the laptop's login locations and its sessions used `/Users/justin/...` paths.
11
+
12
+ 1. `SubscriptionPoolAuthorityStore.migrateLegacy(root, { removeLegacy })`: publishes the authority from the validated, untouched legacy source; the witness is `legacy-migrate` bound to the source's SHA-256 + size; staging under `subscription-pool.candidate-legacy-<gen>`; cleanup of staging/witness on failure. `recoverLegacyMigrate` implements the spec's recovery table (finalize committed directory; rename complete staging; rebuild from a byte-matching source; fail closed on a mutated/missing source).
13
+ 2. `SubscriptionPool.migrateLegacyToMachineLocal({ removeLegacy })`: reads the legacy file, drops rows whose `configHome` does not exist on this machine (reported), publishes the rest. `persist()` now writes to the authority whenever a published generation was loaded, even if a legacy file is still on disk (before, load read the authority but persist would have written the legacy file — a divergence).
14
+ 3. `PostUpdateMigrator.migrateSubscriptionPoolToMachineLocal`: adds `.instar/subscription-pool.json` to the agent home's `.gitignore`; runs the migration once per machine; removes the legacy file only on git's POSITIVE "not tracked" answer (`git ls-files --error-unmatch` via `SafeGitExecutor.readSync`, tri-state: tracked / untracked / unknown — a missing repo, a refused source-tree guard or any other failure is `unknown` and keeps the file). `migrateSubscriptionPoolInteractiveReady` now reads the authority when one is published (machineId-aware pool) instead of gating on the legacy path. `GITIGNORE_ENTRIES` gains the same entry for new homes.
15
+
16
+ **Deliberate deviation from the spec's "remove matching legacy":** a git-tracked legacy file is left in place. Deleting it would be committed by the agent home's git-sync and propagate to every peer on its next pull; a peer that has not yet migrated would lose its only copy. The authority wins on load and persist, so the lingering shared copy is inert. Removal of tracked copies is a later step once every machine of an agent has migrated.
17
+
18
+ ## Decision-point inventory
19
+
20
+ - Which rows are published: `fs.existsSync(configHome)` on this machine.
21
+ - Whether the legacy source is removed: git-tracked or not.
22
+
23
+ ## 1. Over-block
24
+
25
+ A row whose login home is temporarily unavailable at migration time (unmounted volume) is dropped from this machine's authority; the legacy file (kept when tracked; removed when untracked) still lists it, and re-enrollment restores it. Rows are never deleted anywhere else.
26
+
27
+ ## 2. Under-block
28
+
29
+ A peer that pulls a `.gitignore` change gains the ignore entry but keeps tracking the file until a later release untracks it. Luna's Studio has already untracked its copy by hand; the laptop's copy is still tracked.
30
+
31
+ ## 3. Level-of-abstraction fit
32
+
33
+ Store-level protocol in the authority store (where first-create and update live); row filtering in the pool (which knows the account shape); git/ignore handling in the migrator (the documented path for existing agents).
34
+
35
+ ## 4. Signal vs authority compliance
36
+
37
+ No decision authority over agent behaviour; a data-store migration. The migration only ever ADDS the per-machine authority; it deletes a legacy file only when untracked and byte-matching the witness.
38
+
39
+ ## 4b. Judgment-point check
40
+
41
+ Not a competing-signals decision: file existence, git tracking and digest equality are facts.
42
+
43
+ ## 5. Interactions
44
+
45
+ - `load()` already preferred the authority; `persist()` precedence is corrected to match.
46
+ - Existing `migrateSubscriptionPoolInteractiveReady` still reads the legacy path to find claude homes; it runs on the authority-less pool and is unaffected.
47
+ - Quota polling, swaps and enrollment read the pool; after migration they see only this machine's homes — the intended fix for #2122.
48
+ - E2E: the production update path + server boot on a home with a legacy file serves only this machine's logins over `/subscription-pool`.
49
+
50
+ ## 6. External surfaces
51
+
52
+ Migration log lines name published/dropped account ids (never credentials). `.gitignore` gains one entry.
53
+
54
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
55
+
56
+ Machine-local BY DESIGN per the approved spec (login locations live on one disk; `machineId` witness, `foreign-authority` refusal). Account METADATA replication for pool-scope views is the existing WS5.2 projection and is untouched.
57
+
58
+ ## 8. Rollback cost
59
+
60
+ Revert the code; the authority directory and witness remain on disk and the previous release ignores them (it reads the legacy file, which was kept when tracked). For an untracked, removed legacy file the prior release would see an unconfigured pool until re-enrollment; the authority directory holds the data.
61
+
62
+ ## Conclusion
63
+
64
+ Builds the migration the approved spec already called for, with one safety deviation (never delete a shared copy) that closes the #2122 cross-machine inheritance without risking a peer's data.
65
+
66
+ ## Second-pass review (if required)
67
+
68
+ Independent subagent reviewer, 2026-10-06. **Two concerns raised, both fixed:** (1) a crash-recovery rebuild republishes the WHOLE legacy source and the pool then answered `already-machine-local`, so foreign homes would have stayed published under this machine's id — now, while the witness operation is still `legacy-migrate`, the next migration pass re-applies the home filter with an `update()` (test: rebuild → prune → no-op); (2) `isGitTracked` returned `false` on any error, including the source-tree guard refusing `ls-files` on an agent home that is the instar checkout, which would have forced removal — now tri-state, and only git's own "not tracked" answer permits removal (test: no repo → kept). Also noted and fixed: `migrateSubscriptionPoolInteractiveReady` skipped forever once an untracked legacy was removed — it now reads the published authority. Concurred on witness/sibling conformance, `persist()` precedence, deletion safety (digest+size-gated unlink only; recovery never deletes), and the `configHome` filter. The spec's "then remove matching legacy" is deliberately not applied to a tracked copy (recorded above).
@@ -0,0 +1,54 @@
1
+ # Side-Effects Review — a quiet standby does not connect to the Threadline relay
2
+
3
+ **Version / slug:** `threadline-relay-standby`
4
+ **Date:** `2026-10-06`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (one boolean gate on an opt-in connection, reusing an existing per-machine flag)`
7
+
8
+ ## Summary of the change
9
+
10
+ instar#2122: "Both machines connected to the Threadline relay with the same identity and displaced each other. Disabled on the standby for now." The relay admits one connection per agent identity (the agent identity is shared across a multi-machine agent by design), so two machines of one agent knock each other off. `bootstrapThreadline` gains `relayStandby`; `server.ts` passes `!shouldOwnTelegramPoll(config)` — the SAME per-machine flag (`multiMachine.telegramPolling: false`) the lifeline already uses to keep a standby off the Telegram poll. A standby logs `relay connection SUPPRESSED (standby…)`, keeps MCP tools, local discovery and handshake, and never opens the relay socket. CLAUDE.md template + migration gain one awareness line so a standby agent explains "unreachable" correctly.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - Whether this machine opens the relay connection: `relayEnabled && !relayStandby`.
15
+
16
+ ## 1. Over-block
17
+
18
+ A standby cannot be reached by peers over the relay; the awake machine answers for the agent (the Threadline Conversation Coherence model already says a conversation lives on one machine). Single-machine agents (flag absent) are unchanged. A standby that becomes awake needs a restart to connect — the same restart `telegramPolling` already requires.
19
+
20
+ ## 2. Under-block
21
+
22
+ Lease-following (connect on acquire, disconnect on demote) would remove the restart; this ships the static rule Luna applied by hand, now structural. Two machines both configured as pollers still displace each other, as before.
23
+
24
+ ## 3. Level-of-abstraction fit
25
+
26
+ The bootstrap owns the connection decision; the server supplies the machine role from the existing flag.
27
+
28
+ ## 4. Signal vs authority compliance
29
+
30
+ No authority over messages; a connection gate.
31
+
32
+ ## 4b. Judgment-point check
33
+
34
+ Not a competing-signals decision.
35
+
36
+ ## 5. Interactions
37
+
38
+ `shouldOwnTelegramPoll` reads `multiMachine.telegramPolling` only (default true), so the gate is dark for every agent that has not set the standby flag. The listener daemon path (`daemonHandlingRelay`) is downstream of `relayEnabled` and so also stays off on a standby.
39
+
40
+ ## 6. External surfaces
41
+
42
+ One boot log line on a standby; one CLAUDE.md bullet.
43
+
44
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
45
+
46
+ This change exists for multi-machine: one relay connection per agent, held by the awake machine. Machine-local by design (a socket is per machine).
47
+
48
+ ## 8. Rollback cost
49
+
50
+ Revert; or set `multiMachine.telegramPolling` back to true on the standby.
51
+
52
+ ## Conclusion
53
+
54
+ Makes structural what Luna did by hand on the Studio.
@@ -0,0 +1,60 @@
1
+ # Side-Effects Review — a relay send reports the conversation id the peer will reply on
2
+
3
+ **Version / slug:** `threadline-wire-thread-id`
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-1304 (reported by Luna/sagemind on Threadline, 2026-10-04). On the relay path of `POST /threadline/relay-send`, a send without an explicit `threadId` let `ThreadlineClient` pick the wire thread (client affinity or a fresh `thread-<ts>-<rand>`), but the route recorded `threadId ?? messageId`. Every downstream record — the canonical outbox, the thread log, the Telegram bridge mirror, the A2A delivery tracker, the reply waiter, and `captureOrigin` (the thread→origin-topic binding that routes a reply back to the session that asked) — was keyed on the `msg-…` id. The peer replied on `thread-…`, which matched none of them, so the reply cold-spawned a new session; at a full session cap that spawn was refused and the queued reply was held indefinitely.
11
+
12
+ `ThreadlineClient.sendAutoWithThread` returns `{ messageId, threadId }` where `threadId` is the id that went on the wire (both the encrypted `send` and plaintext `sendPlaintext` paths record it). The route uses it for every record above. Wire behaviour is unchanged.
13
+
14
+ ## Decision-point inventory
15
+
16
+ None changed. Routing of an inbound reply (topic linkage / live inject / resume / spawn) is unchanged; it now receives the correct key.
17
+
18
+ ## 1. Over-block
19
+
20
+ Nothing is refused.
21
+
22
+ ## 2. Under-block
23
+
24
+ ACT-1304's other parts are not in this change: the spawn drain's give-up alert being held by the origin and duplicate-content gates, and a reported thread id truncated at its last hyphen (no cause located yet). A reply to a send from a session that is NOT topic-bound has no origin binding, so it still takes the spawn path (as before). <!-- tracked: ACT-1304 -->
25
+
26
+ ## 3. Level-of-abstraction fit
27
+
28
+ The client is the only place that knows the wire thread; it now reports it. The route stops guessing.
29
+
30
+ ## 4. Signal vs authority compliance
31
+
32
+ No authority added.
33
+
34
+ ## 4b. Judgment-point check
35
+
36
+ Not a decision point.
37
+
38
+ ## 5. Interactions
39
+
40
+ The local-delivery path already used the minted `effectiveThreadId` and is untouched. Threads recorded before this change under `msg-…` ids keep those records; new sends use the wire id. Callers that pass an explicit `threadId` see no difference. Test mocks of the relay client gained `sendAutoWithThread`. Side benefit: the A2A delivery tracker acknowledges by thread id, so relay sends' `stale` flag becomes accurate (a `msg-…` key could never be acked by a reply). The stored-retry path re-sends old records with their recorded `msg-…` thread id, behaving as before.
41
+
42
+ ## 6. External surfaces
43
+
44
+ `threadline_send`'s returned `threadId` is now the real conversation id (previously the message id on relay sends). `threadline_history` on that id now finds the thread.
45
+
46
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
47
+
48
+ Machine-local send path; the corrected id flows into the same replicated/mesh records as before (thread log, conversation mesh view), now consistent with the peer's.
49
+
50
+ ## 8. Rollback cost
51
+
52
+ Revert; no data migration. Records written with wire ids remain valid.
53
+
54
+ ## Conclusion
55
+
56
+ A one-field correctness fix at the source of the mismatch, which lets the existing reply-to-originator path work for relay sends.
57
+
58
+ ## Second-pass review (if required)
59
+
60
+ Independent subagent reviewer, 2026-10-05: **Concur with the review.** Both send paths always put a thread id on the wire and the receiver keeps an arriving id (it mints only when none arrives); the anti-hijack isolation fires only for an id owned by a different participant, which a fresh `thread-…` id cannot be. No consumer relies on threadId===messageId; the delivery tracker and reply waiter key on thread id. `lastWireThreadId` is set and read synchronously. Two notes folded into §5. A pre-existing window where a very fast reply could beat the waiter registration (after `await captureOrigin`) is unchanged by this change.
@@ -0,0 +1,58 @@
1
+ # Side-Effects Review — `instar wakeup` talks to the machine that is actually awake
2
+
3
+ **Version / slug:** `wakeup-targets-awake-machine`
4
+ **Date:** `2026-10-06`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (CLI target/URL resolution; the handoff protocol itself is unchanged)`
7
+
8
+ ## Summary of the change
9
+
10
+ instar#2122: "`instar wakeup` reported 'Current location: mac-studio' while the laptop held the lease, then failed with `Invalid challenge signature`." Two causes in `wakeup()`:
11
+
12
+ 1. It took the awake machine from the registry's ROLE field (`getAwakeMachine`), which lags the live lease and on Luna's Studio still named a removed identity. New `resolveAwakeMachine` asks the local server's `/health` for `multiMachine.syncStatus.leaseHolder` first (an active, non-revoked registry entry), falling back to the registry role.
13
+ 2. It sent the handoff challenge to its OWN server (`localhost:<port>/health` → `tunnelUrl || localhost`). The server builds the challenge message with ITSELF as receiver while the client signed for the awake machine's id, so the signature never matched. New `resolveAwakeServerUrl` probes the awake machine's advertised endpoints (tailscale, lan, cloudflare) and last-known URL with `/health` and uses the first that answers; `localhost` is never a candidate.
14
+
15
+ ## Decision-point inventory
16
+
17
+ - Which machine is "awake": live lease holder → registry role.
18
+ - Which URL receives the handoff: the awake machine's reachable rope.
19
+
20
+ ## 1. Over-block
21
+
22
+ If none of the awake machine's addresses answer, wakeup now says so and points at `--force` — before, it would have contacted the wrong server and failed later with a confusing signature error.
23
+
24
+ ## 2. Under-block
25
+
26
+ The handoff protocol, `--force` and the no-awake promotion path are unchanged.
27
+
28
+ ## 3. Level-of-abstraction fit
29
+
30
+ Both helpers live in the CLI next to `wakeup()`, exported for tests.
31
+
32
+ ## 4. Signal vs authority compliance
33
+
34
+ No authority change; the server's challenge verification is untouched.
35
+
36
+ ## 4b. Judgment-point check
37
+
38
+ Not a competing-signals decision.
39
+
40
+ ## 5. Interactions
41
+
42
+ Uses `MachineRegistryEntry.endpoints` (multi-transport mesh) and `lastKnownUrl` as the mesh RPC client does. The lease read via `/health` is the same field `instar doctor` shows.
43
+
44
+ ## 6. External surfaces
45
+
46
+ CLI output: "Reaching <name> at <redacted url>".
47
+
48
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
49
+
50
+ This change exists for multi-machine; it reads the mesh's own records.
51
+
52
+ ## 8. Rollback cost
53
+
54
+ Revert.
55
+
56
+ ## Conclusion
57
+
58
+ The handoff request now goes to the machine that can answer it, named from the live lease.