instar 1.3.149 → 1.3.151
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.
- package/dist/core/MachineIdentity.d.ts.map +1 -1
- package/dist/core/MachineIdentity.js +20 -1
- package/dist/core/MachineIdentity.js.map +1 -1
- package/dist/core/MultiMachineCoordinator.d.ts +14 -0
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +29 -2
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +9 -10
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +19 -19
- package/upgrades/1.3.150.md +54 -0
- package/upgrades/1.3.151.md +65 -0
- package/upgrades/side-effects/codex-scope-coherence-stdout-safe.md +42 -0
- package/upgrades/side-effects/silent-standby-lease.md +115 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-05-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-05-31T08:16:51.978Z",
|
|
5
|
+
"instarVersion": "1.3.151",
|
|
6
6
|
"entryCount": 193,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"domain": "identity",
|
|
12
12
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
13
13
|
"installedPath": ".instar/hooks/instar/session-start.sh",
|
|
14
|
-
"contentHash": "
|
|
14
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
15
15
|
"since": "2025-01-01"
|
|
16
16
|
},
|
|
17
17
|
"hook:dangerous-command-guard": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"domain": "safety",
|
|
21
21
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
22
22
|
"installedPath": ".instar/hooks/instar/dangerous-command-guard.sh",
|
|
23
|
-
"contentHash": "
|
|
23
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
24
24
|
"since": "2025-01-01"
|
|
25
25
|
},
|
|
26
26
|
"hook:grounding-before-messaging": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"domain": "safety",
|
|
30
30
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
31
31
|
"installedPath": ".instar/hooks/instar/grounding-before-messaging.sh",
|
|
32
|
-
"contentHash": "
|
|
32
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
33
33
|
"since": "2025-01-01"
|
|
34
34
|
},
|
|
35
35
|
"hook:compaction-recovery": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"domain": "identity",
|
|
39
39
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
40
40
|
"installedPath": ".instar/hooks/instar/compaction-recovery.sh",
|
|
41
|
-
"contentHash": "
|
|
41
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
42
42
|
"since": "2025-01-01"
|
|
43
43
|
},
|
|
44
44
|
"hook:external-operation-gate": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"domain": "safety",
|
|
48
48
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
49
49
|
"installedPath": ".instar/hooks/instar/external-operation-gate.js",
|
|
50
|
-
"contentHash": "
|
|
50
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
51
51
|
"since": "2025-01-01"
|
|
52
52
|
},
|
|
53
53
|
"hook:deferral-detector": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"domain": "safety",
|
|
57
57
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
58
58
|
"installedPath": ".instar/hooks/instar/deferral-detector.js",
|
|
59
|
-
"contentHash": "
|
|
59
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
60
60
|
"since": "2025-01-01"
|
|
61
61
|
},
|
|
62
62
|
"hook:post-action-reflection": {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"domain": "evolution",
|
|
66
66
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
67
67
|
"installedPath": ".instar/hooks/instar/post-action-reflection.js",
|
|
68
|
-
"contentHash": "
|
|
68
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
69
69
|
"since": "2025-01-01"
|
|
70
70
|
},
|
|
71
71
|
"hook:external-communication-guard": {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"domain": "safety",
|
|
75
75
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
76
76
|
"installedPath": ".instar/hooks/instar/external-communication-guard.js",
|
|
77
|
-
"contentHash": "
|
|
77
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
78
78
|
"since": "2025-01-01"
|
|
79
79
|
},
|
|
80
80
|
"hook:scope-coherence-collector": {
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"domain": "coherence",
|
|
84
84
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
85
85
|
"installedPath": ".instar/hooks/instar/scope-coherence-collector.js",
|
|
86
|
-
"contentHash": "
|
|
86
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
87
87
|
"since": "2025-01-01"
|
|
88
88
|
},
|
|
89
89
|
"hook:scope-coherence-checkpoint": {
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"domain": "coherence",
|
|
93
93
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
94
94
|
"installedPath": ".instar/hooks/instar/scope-coherence-checkpoint.js",
|
|
95
|
-
"contentHash": "
|
|
95
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
96
96
|
"since": "2025-01-01"
|
|
97
97
|
},
|
|
98
98
|
"hook:free-text-guard": {
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"domain": "safety",
|
|
102
102
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
103
103
|
"installedPath": ".instar/hooks/instar/free-text-guard.sh",
|
|
104
|
-
"contentHash": "
|
|
104
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
105
105
|
"since": "2025-01-01"
|
|
106
106
|
},
|
|
107
107
|
"hook:claim-intercept": {
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"domain": "coherence",
|
|
111
111
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
112
112
|
"installedPath": ".instar/hooks/instar/claim-intercept.js",
|
|
113
|
-
"contentHash": "
|
|
113
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
114
114
|
"since": "2025-01-01"
|
|
115
115
|
},
|
|
116
116
|
"hook:claim-intercept-response": {
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"domain": "coherence",
|
|
120
120
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
121
121
|
"installedPath": ".instar/hooks/instar/claim-intercept-response.js",
|
|
122
|
-
"contentHash": "
|
|
122
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
123
123
|
"since": "2025-01-01"
|
|
124
124
|
},
|
|
125
125
|
"hook:stop-gate-router": {
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"domain": "safety",
|
|
129
129
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
130
130
|
"installedPath": ".instar/hooks/instar/stop-gate-router.js",
|
|
131
|
-
"contentHash": "
|
|
131
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
132
132
|
"since": "2025-01-01"
|
|
133
133
|
},
|
|
134
134
|
"hook:auto-approve-permissions": {
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"domain": "safety",
|
|
138
138
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
139
139
|
"installedPath": ".instar/hooks/instar/auto-approve-permissions.js",
|
|
140
|
-
"contentHash": "
|
|
140
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
141
141
|
"since": "2025-01-01"
|
|
142
142
|
},
|
|
143
143
|
"job:health-check": {
|
|
@@ -1489,7 +1489,7 @@
|
|
|
1489
1489
|
"type": "subsystem",
|
|
1490
1490
|
"domain": "updates",
|
|
1491
1491
|
"sourcePath": "src/core/PostUpdateMigrator.ts",
|
|
1492
|
-
"contentHash": "
|
|
1492
|
+
"contentHash": "3418f7109a38172a28a3a202c8a878c1064f55911a82155c53a0b1ccde44e804",
|
|
1493
1493
|
"since": "2025-01-01"
|
|
1494
1494
|
},
|
|
1495
1495
|
"subsystem:scheduler": {
|
|
@@ -1545,7 +1545,7 @@
|
|
|
1545
1545
|
"type": "subsystem",
|
|
1546
1546
|
"domain": "coordination",
|
|
1547
1547
|
"sourcePath": "src/core/MultiMachineCoordinator.ts",
|
|
1548
|
-
"contentHash": "
|
|
1548
|
+
"contentHash": "1dc3fb11c8ddea5cfe5f02561636cd54c1661d0e9da04f54607ad7679f027ea6",
|
|
1549
1549
|
"since": "2025-01-01"
|
|
1550
1550
|
},
|
|
1551
1551
|
"subsystem:backup-manager": {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
review-convergence: complete
|
|
3
|
+
approved: true
|
|
4
|
+
approved-by: echo (standing 12h deploy mandate, topic 13435; live-found driving Codey)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Upgrade Guide — vNEXT
|
|
8
|
+
|
|
9
|
+
<!-- bump: patch -->
|
|
10
|
+
|
|
11
|
+
## What Changed — the scope-check Stop hook no longer confuses Codex
|
|
12
|
+
|
|
13
|
+
When an agent finishes a turn, a "scope-coherence checkpoint" Stop hook runs and, on
|
|
14
|
+
the normal allow path, used to print a tiny `{decision:approve}` message. Claude
|
|
15
|
+
treats that as allow, but Codex's Stop-hook contract is stricter: print
|
|
16
|
+
`{decision:block,...}` to interrupt, print **nothing** to allow — an explicit
|
|
17
|
+
approve message is rejected as "invalid stop hook JSON output". So on every Codex
|
|
18
|
+
session completion that hook reported an error (the reply was already sent, so
|
|
19
|
+
nothing actually broke, but the error was noisy and wrong). Found live while
|
|
20
|
+
test-driving the Codex agent.
|
|
21
|
+
|
|
22
|
+
The fix changes every allow path to emit nothing (empty stdout = allow), which is
|
|
23
|
+
byte-equivalent for Claude and removes the error for Codex. The interrupt (block)
|
|
24
|
+
path is unchanged. The hook is regenerated from source on every update, so all
|
|
25
|
+
agents get the fix automatically.
|
|
26
|
+
|
|
27
|
+
## Summary of New Capabilities
|
|
28
|
+
|
|
29
|
+
- `scope-coherence-checkpoint` Stop hook is now Codex-stdout-safe: allow paths emit
|
|
30
|
+
empty stdout (instead of `{decision:approve}`); the block/checkpoint path is
|
|
31
|
+
unchanged. Sibling of the earlier autonomous-stop-hook Codex fix.
|
|
32
|
+
|
|
33
|
+
## What to Tell Your User
|
|
34
|
+
|
|
35
|
+
If you run a Codex-based agent, it will stop logging a spurious "invalid stop hook"
|
|
36
|
+
error every time it finishes a turn. Nothing actually broke before (replies still
|
|
37
|
+
went out), and Claude agents are unaffected — this just makes the scope-check hook
|
|
38
|
+
speak Codex's dialect correctly. It applies automatically on the next update.
|
|
39
|
+
|
|
40
|
+
## Evidence
|
|
41
|
+
|
|
42
|
+
- Live: driving the Codex agent over Telegram, it completed a task + sent its reply,
|
|
43
|
+
then its session reported "Stop hook (failed) — invalid stop hook JSON output".
|
|
44
|
+
- Audited the 5-hook Stop chain: only scope-coherence-checkpoint writes on the allow
|
|
45
|
+
path; the others write only on block (codex-safe), and the autonomous hook was
|
|
46
|
+
fixed earlier.
|
|
47
|
+
- Unit: `tests/unit/scope-coherence-reentry.test.ts` — allow paths now exit 0 with
|
|
48
|
+
empty stdout; the generated hook contains no `{decision:approve}` and keeps
|
|
49
|
+
`{decision:block}`.
|
|
50
|
+
- Regression: PostUpdateMigrator-buildStopHook + migration-parity-hooks +
|
|
51
|
+
installCodexHooks — 28 tests pass.
|
|
52
|
+
- `tsc --noEmit` + `npm run lint` clean.
|
|
53
|
+
- Spec: `docs/specs/codex-scope-coherence-stdout-safe.md` (+ `.eli16.md`).
|
|
54
|
+
- Side-effects: `upgrades/side-effects/codex-scope-coherence-stdout-safe.md`.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
review-convergence: complete
|
|
3
|
+
approved: true
|
|
4
|
+
approved-by: echo (standing 12h deploy mandate, topic 13481; multi-machine live-transfer cascade)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Instar Upgrade Guide — vNEXT
|
|
8
|
+
|
|
9
|
+
<!-- bump: patch -->
|
|
10
|
+
|
|
11
|
+
## What Changed — a silent standby machine now correctly recognizes the awake machine as in charge
|
|
12
|
+
|
|
13
|
+
Two narrow fixes for the multi-machine live session-transfer path (the one that
|
|
14
|
+
moves a conversation from one machine to another when you say "move this to the
|
|
15
|
+
Mac mini"). Both were found by running the transfer live between a laptop and a
|
|
16
|
+
Mac mini and tracing why the handoff was rejected.
|
|
17
|
+
|
|
18
|
+
1. **Signing-key fallback.** Each machine signs its mesh messages with a private
|
|
19
|
+
key file. The loader looked only for the current filename; a machine whose key
|
|
20
|
+
was saved under the older filename threw an error at boot, which silently
|
|
21
|
+
aborted its lease-coordination setup — so it never learned which machine was in
|
|
22
|
+
charge. The loader now also checks the legacy filename before giving up,
|
|
23
|
+
matching the fallback the lifeline loader already had.
|
|
24
|
+
|
|
25
|
+
2. **Silent standby stops crowning itself.** A machine configured as a silent
|
|
26
|
+
standby (it never answers messages on its own) was still trying to claim the
|
|
27
|
+
"I'm in charge" lease at startup. On a machine without the shared git-backed
|
|
28
|
+
coordination medium, that produced a split-brain where both machines thought
|
|
29
|
+
they were in charge and the handoff was refused. A silent standby now only
|
|
30
|
+
watches the lease and never claims one, so the awake machine always wins and the
|
|
31
|
+
handoff is accepted.
|
|
32
|
+
|
|
33
|
+
## Summary of New Capabilities
|
|
34
|
+
|
|
35
|
+
- `MachineIdentity.loadSigningKey()` falls back to the legacy `signing-private.pem`
|
|
36
|
+
when the canonical `signing-key.pem` is absent (rethrows if neither exists).
|
|
37
|
+
- `MultiMachineCoordinator.isLeaseObserveOnly` (driven by
|
|
38
|
+
`multiMachine.telegramPolling === false`): a silent standby skips lease
|
|
39
|
+
acquire/renew in both `initializeLease` and `tickLease`, only reconciling its
|
|
40
|
+
role to the observed lease — removing the git-less split-brain epoch leapfrog.
|
|
41
|
+
|
|
42
|
+
## What to Tell Your User
|
|
43
|
+
|
|
44
|
+
If you run your agent on more than one machine, moving a live conversation from one
|
|
45
|
+
machine to another is now more reliable. A quiet backup machine will correctly
|
|
46
|
+
treat your main machine as the one in charge instead of fighting it for control,
|
|
47
|
+
and a machine whose security key was saved under an older filename will no longer
|
|
48
|
+
fail to join the handoff. Nothing to configure — it applies on the next update.
|
|
49
|
+
|
|
50
|
+
## Evidence
|
|
51
|
+
|
|
52
|
+
- Both bugs were found live on the two-machine setup (laptop plus Mac mini) by
|
|
53
|
+
instrumenting the lease broadcast or observe path: the mini threw ENOENT on the
|
|
54
|
+
canonical key at boot so its lease coordinator never attached, and once that was
|
|
55
|
+
fixed it rejected the laptop's lease as below its own self-issued epoch floor.
|
|
56
|
+
- Unit, `tests/unit/machine-identity.test.ts`: loadSigningKey falls back to the
|
|
57
|
+
legacy name and still throws when neither file exists.
|
|
58
|
+
- Unit, `tests/unit/multi-machine-coordinator.test.ts`: a silent standby never
|
|
59
|
+
acquires or renews (across initializeLease and tickLease); a normal machine does
|
|
60
|
+
acquire; the observe-only flag reflects telegramPolling false.
|
|
61
|
+
- 109 related unit tests pass (machine-identity, multi-machine-coordinator,
|
|
62
|
+
LeaseCoordinator, mesh-signing-key-resolution, multimachine-syncstatus);
|
|
63
|
+
tsc --noEmit clean.
|
|
64
|
+
- Spec, `docs/specs/silent-standby-lease.md` plus the .eli16.md sibling.
|
|
65
|
+
- Side-effects, `upgrades/side-effects/silent-standby-lease.md`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Side-effects — scope-coherence-checkpoint codex stdout-safe
|
|
2
|
+
|
|
3
|
+
## 1. What files/state does this touch at runtime?
|
|
4
|
+
Only the generated `.instar/hooks/instar/scope-coherence-checkpoint.js` (rewritten
|
|
5
|
+
by the always-overwrite built-in-hook migration). At Stop time the hook reads its
|
|
6
|
+
state file + (when deep) fetches active-job context — unchanged. The only change is
|
|
7
|
+
what it writes to stdout on the allow paths (now nothing).
|
|
8
|
+
|
|
9
|
+
## 2. Does it change any functional behavior?
|
|
10
|
+
- **Claude:** none. Empty stdout == approve in Claude Code, identical to the prior
|
|
11
|
+
explicit `{decision:'approve'}`. The scope-checkpoint still blocks (prompts) when
|
|
12
|
+
deep-implementation + cooldown + min-age conditions are met.
|
|
13
|
+
- **codex:** the allow paths no longer emit `{decision:'approve'}`, so codex stops
|
|
14
|
+
reporting "invalid stop hook JSON output" on session completion. The block path is
|
|
15
|
+
unchanged (codex accepts `{decision:'block',...}`).
|
|
16
|
+
|
|
17
|
+
## 3. What happens on failure / weird input?
|
|
18
|
+
The catch-all error path now allows (empty stdout) instead of emitting approve-JSON
|
|
19
|
+
— same allow outcome, codex-safe. Malformed stdin still falls through to allow.
|
|
20
|
+
|
|
21
|
+
## 4. Migration parity — do existing agents get it?
|
|
22
|
+
Yes, automatically. The hook is in the always-overwrite built-in-hook set, so every
|
|
23
|
+
agent regenerates it from the fixed `getScopeCoherenceCheckpointHook()` on its next
|
|
24
|
+
update (incl. instar-codey). No marker/sniff needed.
|
|
25
|
+
|
|
26
|
+
## 5. Could it spam / flood / burn resources?
|
|
27
|
+
No — it REDUCES output (writes less). The hook runs only at Stop, same as before.
|
|
28
|
+
|
|
29
|
+
## 6. Rollback / off-switch?
|
|
30
|
+
Revert the PR; the next update regenerates the prior hook. No residual state, no
|
|
31
|
+
flag. Reverting restores `{decision:'approve'}` (Claude unaffected; codex error
|
|
32
|
+
returns).
|
|
33
|
+
|
|
34
|
+
## 7. Concurrency / ordering?
|
|
35
|
+
None new. Single short-lived hook process per Stop event, unchanged.
|
|
36
|
+
|
|
37
|
+
## Blast radius
|
|
38
|
+
Small + behavior-preserving on Claude, error-fixing on codex. One generator method
|
|
39
|
+
in PostUpdateMigrator (six allow-path stdout writes removed; block path kept) + test
|
|
40
|
+
updates. Mirrors the #604 codex-stdout convention. The broader approve-JSON class
|
|
41
|
+
(other hooks + additionalContext signal hooks) is explicitly out of scope, tracked
|
|
42
|
+
under #32 / ledger issue codex-stop-chain-multi-json-invalid.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Side-Effects Review — Silent-standby git-less lease (observe-only + legacy-key fallback)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `silent-standby-lease`
|
|
4
|
+
**Date:** `2026-05-31`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Two narrowly-scoped fixes for bugs #4 + #5 of the multi-machine live-transfer
|
|
11
|
+
cascade:
|
|
12
|
+
|
|
13
|
+
1. `MachineIdentity.loadSigningKey()` — on ENOENT for the canonical
|
|
14
|
+
`signing-key.pem`, fall back to the legacy `signing-private.pem` if present,
|
|
15
|
+
else rethrow. (The mini's key was under the legacy name; the lease setup threw
|
|
16
|
+
at boot, so the `LeaseCoordinator` never attached and the mini never resolved a
|
|
17
|
+
holder.)
|
|
18
|
+
2. `MultiMachineCoordinator` — new `isLeaseObserveOnly` getter
|
|
19
|
+
(`multiMachine.telegramPolling === false`). A silent standby skips
|
|
20
|
+
`acquireIfEligible`/`renew` in both `initializeLease` and `tickLease`; it only
|
|
21
|
+
reconciles its role to whatever lease it observes. It never holds its own lease,
|
|
22
|
+
so with the git-less `LocalLeaseStore` (no shared CAS) it can't leapfrog the
|
|
23
|
+
primary's epoch and reject the primary as `below-git-floor`.
|
|
24
|
+
|
|
25
|
+
## Decision-point inventory
|
|
26
|
+
|
|
27
|
+
- **loadSigningKey fallback branch** — ENOENT on canonical → try legacy name →
|
|
28
|
+
exists? read it : rethrow original. Both sides covered by unit tests.
|
|
29
|
+
- **isLeaseObserveOnly gate** — true (telegramPolling:false) → observe-only path
|
|
30
|
+
(no acquire/renew); false/undefined → unchanged acquire+renew path. Both sides
|
|
31
|
+
covered by unit tests (silent standby never acquires; normal machine does).
|
|
32
|
+
|
|
33
|
+
## 1. Over-block
|
|
34
|
+
|
|
35
|
+
**What legitimate inputs does this change reject?** A silent standby
|
|
36
|
+
(`telegramPolling:false`) no longer acquires a lease at all — so it can never
|
|
37
|
+
become awake on its own. That is the INTENT: a muted standby that became "awake but
|
|
38
|
+
not serving" is the incoherent state we are removing. A machine that should be able
|
|
39
|
+
to take over sets `telegramPolling:true` (or unsets it), which is the unchanged
|
|
40
|
+
acquire path. The key fallback rejects nothing — it only ADDS a second lookup
|
|
41
|
+
location before the same rethrow.
|
|
42
|
+
|
|
43
|
+
## 2. Under-block
|
|
44
|
+
|
|
45
|
+
**What does this still miss?** It does not add auto-failover for a silent standby
|
|
46
|
+
(out of scope — failover for it is a deliberate un-mute). It does not add a shared
|
|
47
|
+
CAS to `LocalLeaseStore`; the split-brain is avoided by not acquiring rather than
|
|
48
|
+
by making git-less acquisition safe (correct for the one-awake-machine model; the
|
|
49
|
+
active-active pool, which needs real shared CAS, remains git-backed). The key
|
|
50
|
+
fallback covers only the one known legacy name (`signing-private.pem`), matching
|
|
51
|
+
the lifeline loader's existing fallback — not an open-ended search.
|
|
52
|
+
|
|
53
|
+
## 3. Level-of-abstraction fit
|
|
54
|
+
|
|
55
|
+
**Right layer?** Yes. The key fallback lives in the single `loadSigningKey()`
|
|
56
|
+
loader that every lease/transport consumer already calls (one chokepoint). The
|
|
57
|
+
observe-only gate lives in `MultiMachineCoordinator`'s two lease entry points
|
|
58
|
+
(`initializeLease`, `tickLease`) — the only places acquisition is decided — derived
|
|
59
|
+
from the same `telegramPolling` flag that already designates a silent standby
|
|
60
|
+
elsewhere. No new config surface, no duplicated rule.
|
|
61
|
+
|
|
62
|
+
## 4. Signal vs authority compliance
|
|
63
|
+
|
|
64
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
65
|
+
|
|
66
|
+
No blocking authority added. The observe-only gate REMOVES an action (acquisition)
|
|
67
|
+
for a configured-silent machine; it never blocks a message or an operation. The key
|
|
68
|
+
fallback is a pure read-path widen with an unchanged terminal rethrow. Neither path
|
|
69
|
+
gates any user-facing or outbound behavior.
|
|
70
|
+
|
|
71
|
+
## 5. Interactions
|
|
72
|
+
|
|
73
|
+
`loadSigningKey` feeds `HttpLeaseTransport` (lease broadcast/observe) and
|
|
74
|
+
machineAuth signing; widening it only makes a previously-throwing legacy-keyed
|
|
75
|
+
machine succeed — no consumer sees a different value, only a present-vs-throw
|
|
76
|
+
difference. The observe-only gate interacts with `reconcileRoleToLease` (still
|
|
77
|
+
called every tick, now inside each branch) and with `LeaseCoordinator.effectiveView`
|
|
78
|
+
(a standby with an empty `LocalLeaseStore` folds the primary's observed broadcast
|
|
79
|
+
cleanly). Idempotent: both paths produce the same result on repeated boots/ticks.
|
|
80
|
+
|
|
81
|
+
## 6. External surfaces
|
|
82
|
+
|
|
83
|
+
No HTTP routes, no config defaults, no notifications, no Telegram. `/health`'s
|
|
84
|
+
`multiMachine.syncStatus.leaseHolder` will now correctly resolve to the primary on a
|
|
85
|
+
silent standby (the visible, intended effect). Pure code.
|
|
86
|
+
|
|
87
|
+
## 7. Rollback cost
|
|
88
|
+
|
|
89
|
+
Low. Revert the PR: `loadSigningKey` returns to canonical-only (re-bricks a
|
|
90
|
+
legacy-keyed machine's lease setup) and the coordinator returns to always-acquire
|
|
91
|
+
(re-introduces the split-brain on git-less standbys). No schema, no migration, no
|
|
92
|
+
persisted state created by this change. The one deploy-time action (clear the mini's
|
|
93
|
+
stale self-issued `lease-local.json`) is a one-shot cleanup, not a rollback cost.
|
|
94
|
+
|
|
95
|
+
## Conclusion
|
|
96
|
+
|
|
97
|
+
Minimal, additive-or-removing-an-action change with both decision sides unit-tested,
|
|
98
|
+
no new authority, no external surface beyond a corrected `/health` field, and a cheap
|
|
99
|
+
revert. It closes the final two layers blocking the live two-machine transfer.
|
|
100
|
+
|
|
101
|
+
## Second-pass review (if required)
|
|
102
|
+
|
|
103
|
+
Not required — no new blocking authority, no destructive op, both decision branches
|
|
104
|
+
covered by tests, reversible. (Live two-machine transfer proof is the Tier-3 gate
|
|
105
|
+
that follows deploy.)
|
|
106
|
+
|
|
107
|
+
## Evidence pointers
|
|
108
|
+
|
|
109
|
+
- `tests/unit/machine-identity.test.ts` — loadSigningKey legacy fallback + still-throws.
|
|
110
|
+
- `tests/unit/multi-machine-coordinator.test.ts` — observe-only never acquires/renews;
|
|
111
|
+
normal machine acquires; `isLeaseObserveOnly` reflects the flag.
|
|
112
|
+
- 109 related unit tests green (machine-identity, coordinator, LeaseCoordinator,
|
|
113
|
+
mesh-signing-key-resolution, multimachine-syncstatus); `tsc --noEmit` clean.
|
|
114
|
+
- Spec: `docs/specs/silent-standby-lease.md` (+ `.eli16.md`).
|
|
115
|
+
- `upgrades/NEXT.md` — upgrade guide.
|