instar 1.1.1 → 1.1.3
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/cli.js +26 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +7 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +19 -24
- package/dist/commands/setup.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +69 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
- package/dist/lifeline/ServerSupervisor.js +23 -1
- package/dist/lifeline/ServerSupervisor.js.map +1 -1
- package/dist/lifeline/TelegramLifeline.d.ts +15 -0
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
- package/dist/lifeline/TelegramLifeline.js +128 -8
- package/dist/lifeline/TelegramLifeline.js.map +1 -1
- package/dist/lifeline/rateLimitState.d.ts.map +1 -1
- package/dist/lifeline/rateLimitState.js +8 -1
- package/dist/lifeline/rateLimitState.js.map +1 -1
- package/dist/memory/NativeModuleHealer.d.ts.map +1 -1
- package/dist/memory/NativeModuleHealer.js +8 -1
- package/dist/memory/NativeModuleHealer.js.map +1 -1
- package/dist/threadline/WakeSocketServer.d.ts +16 -0
- package/dist/threadline/WakeSocketServer.d.ts.map +1 -1
- package/dist/threadline/WakeSocketServer.js +100 -6
- package/dist/threadline/WakeSocketServer.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/upgrades/1.1.2.md +130 -0
- package/upgrades/1.1.3.md +124 -0
- package/upgrades/side-effects/boot-wrapper-plist-coherence.md +176 -0
- package/upgrades/side-effects/lifeline-version-skew-recovery.md +180 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
<!-- patch = bug fixes, refactors, test additions, doc updates -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
**fix(boot-wrapper): plist↔wrapper extension drift no longer takes agents dark.**
|
|
9
|
+
|
|
10
|
+
Three intertwined boot-time changes that together close a "agent goes
|
|
11
|
+
silently dark and won't come back" failure mode observed in the field
|
|
12
|
+
(Echo, 2026-05-20):
|
|
13
|
+
|
|
14
|
+
1. **`installBootWrapper()` always writes `instar-boot.cjs`** (not `.js`),
|
|
15
|
+
regardless of the project's `package.json` `"type"` field. `.cjs`
|
|
16
|
+
forces CommonJS semantics in Node, so the wrapper's `require()` calls
|
|
17
|
+
work in both type=module and type=commonjs projects. Previously the
|
|
18
|
+
function picked an extension based on `package.json` AND deleted the
|
|
19
|
+
alt extension — so if a project gained `"type": "module"` after the
|
|
20
|
+
plist was generated (e.g., via a package upgrade), the next call to
|
|
21
|
+
`installBootWrapper` deleted the `.js` file the plist still pointed
|
|
22
|
+
at. launchd then exec'd a nonexistent file on every restart and none
|
|
23
|
+
of the downstream self-heal (`ServerSupervisor.preflightSelfHeal`,
|
|
24
|
+
`NativeModuleHealer`, `INSTAR_SUPERVISED` detection) ever ran because
|
|
25
|
+
the boot wrapper itself never loaded.
|
|
26
|
+
|
|
27
|
+
2. **`TelegramLifeline.selfHealPlist` adds a wrapper-existence check.**
|
|
28
|
+
The existing three checks all passed when the plist referenced
|
|
29
|
+
`instar-boot.js` but the file was deleted (the plist content still
|
|
30
|
+
matched the legacy-detection patterns). A fourth check now extracts
|
|
31
|
+
the wrapper path from `ProgramArguments` and verifies the file
|
|
32
|
+
exists on disk; if not, the plist is regenerated immediately.
|
|
33
|
+
Defense-in-depth — the always-.cjs change above eliminates the
|
|
34
|
+
creation of this state going forward, but this check rescues any
|
|
35
|
+
already-existing drift before the next launchd restart hits the
|
|
36
|
+
bad path.
|
|
37
|
+
|
|
38
|
+
3. **`WakeSocketServer.start()` recovers from EADDRINUSE.** The
|
|
39
|
+
pre-existing unlink-before-bind pass handled the common
|
|
40
|
+
"stale socket file from unclean exit" case but silently swallowed
|
|
41
|
+
unlink errors and offered no fallback if `listen()` itself fired
|
|
42
|
+
EADDRINUSE. On EADDRINUSE the server now probes the socket: if a
|
|
43
|
+
live peer answers, surface the error normally (don't clobber a real
|
|
44
|
+
second instance); if nothing answers, force-unlink and retry listen
|
|
45
|
+
once. Bounded retry — single attempt, no churn.
|
|
46
|
+
|
|
47
|
+
4. **`PostUpdateMigrator.migrateBootWrapperToCjs`** — closes the gap
|
|
48
|
+
for in-the-wild agents whose launchd plists were generated before
|
|
49
|
+
this change with `instar-boot.js` in `ProgramArguments`. On darwin,
|
|
50
|
+
if the plist exists and references `instar-boot.js`, the migration
|
|
51
|
+
regenerates via `installAutoStart` so the plist now points at
|
|
52
|
+
`instar-boot.cjs`. Idempotent — re-runs that find a `.cjs`-referencing
|
|
53
|
+
plist are silent no-ops. The migration does NOT delete the legacy
|
|
54
|
+
`.js` file; rollback safety wins over file cleanup.
|
|
55
|
+
|
|
56
|
+
Files touched (excluding tests / docs / upgrade notes):
|
|
57
|
+
- `src/commands/setup.ts` — `installBootWrapper`, `ensureBootWrapper`
|
|
58
|
+
- `src/commands/server.ts` — auto-start plist sanity check accepts `.cjs`
|
|
59
|
+
- `src/lifeline/TelegramLifeline.ts` — selfHealPlist 4th check
|
|
60
|
+
- `src/threadline/WakeSocketServer.ts` — EADDRINUSE recovery
|
|
61
|
+
- `src/core/PostUpdateMigrator.ts` — `migrateBootWrapperToCjs`
|
|
62
|
+
|
|
63
|
+
## Evidence
|
|
64
|
+
|
|
65
|
+
**Repro of the original failure** (Echo, 2026-05-20, 10:25 PT):
|
|
66
|
+
|
|
67
|
+
1. Echo's `package.json` has `"type": "module"` (instar's own package
|
|
68
|
+
has had this since v1.0.0).
|
|
69
|
+
2. Earlier `installBootWrapper` calls picked `.cjs`, deleted `.js`, and
|
|
70
|
+
the plist was generated at that point referencing `.cjs`.
|
|
71
|
+
3. A subsequent code path (`installBootWrapper` running while the boot
|
|
72
|
+
was actively crashing for a different reason — Node major-version
|
|
73
|
+
ABI break, since rolled back) wrote `.cjs` AGAIN and deleted any
|
|
74
|
+
`.js` that had been re-created by an even-older code path.
|
|
75
|
+
4. The plist on disk still referenced `.cjs`, but ALSO any plist that
|
|
76
|
+
had been generated when echo's `package.json` was missing the
|
|
77
|
+
`type: module` declaration referenced `.js` — and the `.js` file
|
|
78
|
+
was gone.
|
|
79
|
+
5. launchd's next restart exec'd the nonexistent path → process never
|
|
80
|
+
started → all downstream self-heal (`NativeModuleHealer.healBetterSqlite3`,
|
|
81
|
+
`ServerSupervisor.preflightSelfHeal`, bind-failure escalation)
|
|
82
|
+
silently never ran.
|
|
83
|
+
6. Operator intervention (Dawn, manual plist edit) was required to
|
|
84
|
+
restore the agent.
|
|
85
|
+
|
|
86
|
+
**Observed before:** `git log` shows multiple in-the-wild reports of
|
|
87
|
+
launchd plists pointing at a deleted `instar-boot.js` after a project
|
|
88
|
+
gained `"type": "module"`. The selfHealPlist check at
|
|
89
|
+
`src/lifeline/TelegramLifeline.ts:2076` reads `content.includes('instar-boot.js')`,
|
|
90
|
+
which returns `true` even after the file is deleted — so the existing
|
|
91
|
+
checks all pass and no regen fires.
|
|
92
|
+
|
|
93
|
+
**Observed after:**
|
|
94
|
+
- New test `tests/unit/boot-wrapper-plist-coherence.test.ts` exercises
|
|
95
|
+
`installBootWrapper` with package.json `type: module`, type: commonjs,
|
|
96
|
+
and no package.json — all three write `instar-boot.cjs`. A test
|
|
97
|
+
explicitly plants a pre-existing `instar-boot.js` and verifies it is
|
|
98
|
+
NOT deleted by the new code path (rollback safety).
|
|
99
|
+
- New test `tests/unit/PostUpdateMigrator-bootWrapperCjs.test.ts`
|
|
100
|
+
exercises the four migration branches (non-darwin skip, no plist
|
|
101
|
+
skip, already-.cjs idempotent skip, and the `.js → .cjs` detection +
|
|
102
|
+
regeneration path).
|
|
103
|
+
- New test `tests/unit/wake-socket-server-stale-recovery.test.ts`
|
|
104
|
+
exercises the stale-socket cleanup path and asserts that a live peer
|
|
105
|
+
is NOT clobbered (the recovery surfaces EADDRINUSE in that case).
|
|
106
|
+
- Existing test suites unchanged; `installBootWrapper` callers in
|
|
107
|
+
`installMacOSLaunchAgent` and `TelegramLifeline.selfHealPlist`
|
|
108
|
+
receive the new `.cjs` path through the unchanged `wrappers.js`
|
|
109
|
+
return field so no callsite needs adjustment.
|
|
110
|
+
|
|
111
|
+
## What to Tell Your User
|
|
112
|
+
|
|
113
|
+
- **Your agent stays online through package upgrades**: if I had an
|
|
114
|
+
internal package configuration change, my launchd entry point used
|
|
115
|
+
to risk getting renamed out from under itself. That whole class of
|
|
116
|
+
failure is closed off now — the boot file has a stable name that
|
|
117
|
+
works regardless of internal toggles.
|
|
118
|
+
- **Stale socket files don't lock me out anymore**: if I exited
|
|
119
|
+
uncleanly and a leftover socket file blocked the next start, I now
|
|
120
|
+
detect that and clean it up safely, only after first checking that
|
|
121
|
+
nothing else is actually using it.
|
|
122
|
+
|
|
123
|
+
## Summary of New Capabilities
|
|
124
|
+
|
|
125
|
+
| Capability | How to Use |
|
|
126
|
+
|-----------|-----------|
|
|
127
|
+
| Plist↔wrapper coherence guarantee | automatic (no action required) |
|
|
128
|
+
| selfHealPlist verifies wrapper-path-on-disk | automatic (runs in lifeline boot) |
|
|
129
|
+
| WakeSocketServer EADDRINUSE recovery | automatic (runs in listener startup) |
|
|
130
|
+
| PostUpdateMigrator regenerates `.js`-plists | automatic on next instar update |
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
<!-- patch = bug fixes, refactors, test additions, doc updates -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
**fix(lifeline): close the silent-drop version-skew failure class.**
|
|
9
|
+
|
|
10
|
+
Five interlocking fixes that together address the 2026-05-19 →
|
|
11
|
+
2026-05-20 b2lead-insights field incident (21h silent Telegram ingress
|
|
12
|
+
drop after a server auto-update from v1.0.13 → v1.1.0 left the
|
|
13
|
+
lifeline on v1.0.13; server's `/internal/telegram-forward` rejected
|
|
14
|
+
the lifeline with HTTP 426 and the lifeline's drop-after-3-failures
|
|
15
|
+
replay policy silently lost user messages).
|
|
16
|
+
|
|
17
|
+
1. **`rateLimitState.decide` — `versionSkew` bucket bypasses
|
|
18
|
+
cooldown.** A hard incompatibility cannot be cured by waiting; the
|
|
19
|
+
per-minute cooldown wedged every restart attempt. The daily cap
|
|
20
|
+
(3 per 24h) and storm detection remain as backstops.
|
|
21
|
+
|
|
22
|
+
2. **Replay-loop drop policy is gated on a version-skew episode
|
|
23
|
+
flag.** When `forwardToServer` raises `ForwardVersionSkewError`,
|
|
24
|
+
the lifeline sets `versionSkewActive = true` and sends one
|
|
25
|
+
user-visible Telegram alert ("ingress paused: version skew
|
|
26
|
+
detected, your messages are not lost"). The replay loop checks
|
|
27
|
+
the flag BEFORE the `MAX_REPLAY_FAILURES` drop check and re-queues
|
|
28
|
+
without incrementing the failure counter. Cleared on the next
|
|
29
|
+
successful forward.
|
|
30
|
+
|
|
31
|
+
3. **`instar lifeline restart` CLI uses the correct service label.**
|
|
32
|
+
Was `com.instar.<projectName>.lifeline` — different domain AND
|
|
33
|
+
different suffix from what `installMacOSLaunchAgent` writes
|
|
34
|
+
(`ai.instar.<projectName>`). `launchctl kickstart` always failed,
|
|
35
|
+
silently fell back to pkill.
|
|
36
|
+
|
|
37
|
+
4. **pkill fallback escalates to SIGKILL; lock-acquire recognizes
|
|
38
|
+
wedged sleeping processes.** SIGTERM, 3s grace, SIGKILL by name
|
|
39
|
+
pattern in the CLI. In `acquireLockFile`, 'S' (sleeping) state
|
|
40
|
+
with lock-write age > 5 min is treated as a wedged lifeline:
|
|
41
|
+
SIGTERM → 3s poll → SIGKILL → take the lock.
|
|
42
|
+
|
|
43
|
+
5. **`npm rebuild` for better-sqlite3 uses `--build-from-source` and
|
|
44
|
+
`--ignore-scripts`.** Two call sites: `ServerSupervisor`
|
|
45
|
+
preflight rebuild and `NativeModuleHealer.healBetterSqlite3Sync`.
|
|
46
|
+
Without `--build-from-source`, npm can install a cached prebuilt
|
|
47
|
+
with the same wrong ABI and exit 0 — producing the "rebuild
|
|
48
|
+
succeeded but module still fails to load" pattern in the b2lead
|
|
49
|
+
logs.
|
|
50
|
+
|
|
51
|
+
Files touched (excluding tests / docs / upgrade notes):
|
|
52
|
+
- `src/lifeline/rateLimitState.ts` — `decide()`
|
|
53
|
+
- `src/lifeline/TelegramLifeline.ts` — instance state,
|
|
54
|
+
`handleVersionSkew`, `forwardToServer`, `replayQueue`,
|
|
55
|
+
`acquireLockFile`
|
|
56
|
+
- `src/cli.ts` — `lifelineCmd.command('restart')`
|
|
57
|
+
- `src/lifeline/ServerSupervisor.ts` — preflight rebuild argv
|
|
58
|
+
- `src/memory/NativeModuleHealer.ts` — in-line rebuild argv
|
|
59
|
+
|
|
60
|
+
## Evidence
|
|
61
|
+
|
|
62
|
+
**Repro of the original failure** (b2lead-insights, 2026-05-19 evening
|
|
63
|
+
→ 2026-05-20 ~17:50 UTC):
|
|
64
|
+
|
|
65
|
+
1. Server auto-updated v1.0.13 → v1.1.0.
|
|
66
|
+
2. Lifeline kept running v1.0.13.
|
|
67
|
+
3. User Telegram messages → lifeline forwards to server → server
|
|
68
|
+
returns HTTP 426 with `{ok: false, upgradeRequired: true,
|
|
69
|
+
serverVersion: '1.1.0', action: 'restart', reason: 'major-minor-mismatch'}`.
|
|
70
|
+
4. Lifeline catches `ForwardVersionSkewError`, calls
|
|
71
|
+
`handleVersionSkew` → `initiateRestart('versionSkew', ...)`.
|
|
72
|
+
5. `RestartOrchestrator.maybeRestart` consults `rateLimitState.decide`.
|
|
73
|
+
`elapsed < WATCHDOG_COOLDOWN_MS` → returns `{allowed: false, reason:
|
|
74
|
+
'cooldown-active'}`. No restart.
|
|
75
|
+
6. Lifeline returns false from `forwardToServer`. Replay loop
|
|
76
|
+
increments `replayFailures`. After 3 attempts → drop.
|
|
77
|
+
7. 40+ "server-down" notifications + at least 2 explicitly-logged
|
|
78
|
+
dropped messages (`tg-6577`, `tg-6580`). No user-visible alert.
|
|
79
|
+
|
|
80
|
+
**Observed before:** `rateLimitState.ts:112` had no special-case for
|
|
81
|
+
`versionSkew` — the cooldown check fired for all buckets. The replay
|
|
82
|
+
loop at `TelegramLifeline.ts:1394` checked `failures >=
|
|
83
|
+
MAX_REPLAY_FAILURES` unconditionally. The CLI restart command at
|
|
84
|
+
`cli.ts:1639` hardcoded `com.instar.${projectName}.lifeline`.
|
|
85
|
+
`ServerSupervisor.ts:751` and `NativeModuleHealer.ts:397` both ran
|
|
86
|
+
`npm rebuild better-sqlite3` without `--build-from-source`.
|
|
87
|
+
|
|
88
|
+
**Observed after:**
|
|
89
|
+
- 26 new + extended unit tests (8 new in
|
|
90
|
+
`tests/unit/lifeline/version-skew-recovery.test.ts`, 1 new in
|
|
91
|
+
`tests/unit/lifeline/rateLimitState.test.ts`) cover all five paths.
|
|
92
|
+
- Source assertions verify the supervisor rebuild argv includes
|
|
93
|
+
`--build-from-source` AND `--ignore-scripts`, the healer in-line
|
|
94
|
+
rebuild includes both flags, the CLI restart command builds
|
|
95
|
+
`ai.instar.${...}` and never `com.instar.${...}.lifeline`, the
|
|
96
|
+
pkill fallback contains both `-TERM` and `-KILL`, the
|
|
97
|
+
`versionSkewActive` flag is set in `handleVersionSkew`, cleared in
|
|
98
|
+
`forwardToServer` success, checked in `replayQueue` BEFORE the
|
|
99
|
+
drop branch, and the lock-acquire path recognizes wedged-S.
|
|
100
|
+
|
|
101
|
+
Spec: `docs/specs/lifeline-version-skew-recovery.md`
|
|
102
|
+
ELI16: `docs/specs/lifeline-version-skew-recovery.eli16.md`
|
|
103
|
+
Side-effects review: `upgrades/side-effects/lifeline-version-skew-recovery.md`
|
|
104
|
+
|
|
105
|
+
## What to Tell Your User
|
|
106
|
+
|
|
107
|
+
- **You will never silently lose a message during an auto-update
|
|
108
|
+
again**: if my server and my Telegram lifeline temporarily get out
|
|
109
|
+
of sync after an update, I'll send you one clear note that ingress
|
|
110
|
+
is paused, keep all your messages safely in the queue, and replay
|
|
111
|
+
them as soon as the lifeline catches up.
|
|
112
|
+
- **Recovery is automatic**: the bug class that needed a manual
|
|
113
|
+
restart to clear is now self-healing. No operator intervention.
|
|
114
|
+
|
|
115
|
+
## Summary of New Capabilities
|
|
116
|
+
|
|
117
|
+
| Capability | How to Use |
|
|
118
|
+
|-----------|-----------|
|
|
119
|
+
| Version-skew cooldown bypass | automatic |
|
|
120
|
+
| Version-skew user alert (one per episode) | automatic |
|
|
121
|
+
| No-drop policy during version-skew episodes | automatic |
|
|
122
|
+
| CLI lifeline restart resolves correct service | automatic via `instar lifeline restart` |
|
|
123
|
+
| Wedged-lifeline lock recovery | automatic |
|
|
124
|
+
| Force-from-source native rebuilds | automatic |
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Side-Effects Review — Boot-Wrapper / Plist Coherence
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `boot-wrapper-plist-coherence`
|
|
4
|
+
**Date:** `2026-05-20`
|
|
5
|
+
**Author:** Echo (instar developer agent)
|
|
6
|
+
**Trigger:** Field failure 2026-05-20, ~10:25 PT: instar agent "echo"
|
|
7
|
+
went dark, required operator intervention (Dawn — manual plist edit) to
|
|
8
|
+
recover. Root cause: launchd plist's `ProgramArguments` referenced
|
|
9
|
+
`instar-boot.js` but that file had been deleted by `installBootWrapper`
|
|
10
|
+
because `package.json` had gained `"type": "module"`.
|
|
11
|
+
|
|
12
|
+
## Summary of the change
|
|
13
|
+
|
|
14
|
+
Four interlocking edits that together eliminate the plist↔wrapper
|
|
15
|
+
extension-drift failure class:
|
|
16
|
+
|
|
17
|
+
1. **`installBootWrapper()` always writes `.cjs`.** No more reading
|
|
18
|
+
`package.json "type"` and switching extensions. No more deleting the
|
|
19
|
+
alt extension.
|
|
20
|
+
2. **`ensureBootWrapper()` looks for `.cjs`.** The presence check used
|
|
21
|
+
to look for the type-dependent extension; now it always looks for
|
|
22
|
+
`.cjs`.
|
|
23
|
+
3. **`TelegramLifeline.selfHealPlist` adds Check 4:** verify the wrapper
|
|
24
|
+
path referenced by the plist's `ProgramArguments` exists on disk. If
|
|
25
|
+
not, regenerate via `installAutoStart`. Defense-in-depth for any
|
|
26
|
+
already-drifted state.
|
|
27
|
+
4. **`PostUpdateMigrator.migrateBootWrapperToCjs`** detects existing
|
|
28
|
+
`.js`-referencing plists and regenerates them to point at `.cjs` via
|
|
29
|
+
`installAutoStart`. Idempotent.
|
|
30
|
+
5. **`WakeSocketServer.start()`** uses a probe-before-clobber pattern.
|
|
31
|
+
Previously: pre-bind unconditional `unlink`. New: listen first; on
|
|
32
|
+
EADDRINUSE, probe-connect to determine if the socket is live or
|
|
33
|
+
stale; only unlink+retry if confirmed stale. Prevents silently
|
|
34
|
+
stealing a path from a live peer.
|
|
35
|
+
|
|
36
|
+
## Decision matrix
|
|
37
|
+
|
|
38
|
+
### Over-block
|
|
39
|
+
|
|
40
|
+
| Scenario | Outcome |
|
|
41
|
+
|----------|---------|
|
|
42
|
+
| Project has package.json `"type": "module"` | Always-`.cjs` is correct (no behavioral change vs. pre-existing `.cjs`-write path) |
|
|
43
|
+
| Project has no package.json | Always-`.cjs` works (Node treats `.cjs` as CommonJS regardless of parent context) |
|
|
44
|
+
| Project has package.json `"type": "commonjs"` | Always-`.cjs` works (CommonJS is the explicit context) |
|
|
45
|
+
| Existing `.js` wrapper on disk | NOT touched (rollback-safe — old plists referencing `.js` keep working until migrator runs) |
|
|
46
|
+
| Existing `.cjs` wrapper on disk | Overwritten with current content (preserves existing behavior) |
|
|
47
|
+
|
|
48
|
+
No over-block. The `.cjs` extension is strictly broader than the previous `.js`+`.cjs` split.
|
|
49
|
+
|
|
50
|
+
### Under-block
|
|
51
|
+
|
|
52
|
+
| Scenario | Outcome |
|
|
53
|
+
|----------|---------|
|
|
54
|
+
| Plist references `.js`, no `.js` file on disk, no `.cjs` file on disk | Migrator regenerates; selfHealPlist Check 4 catches |
|
|
55
|
+
| Plist references `.cjs`, `.cjs` file deleted | selfHealPlist Check 4 catches (regenerates) |
|
|
56
|
+
| Plist references `.js`, `.js` file present, `.cjs` file present | Migrator regenerates plist to `.cjs` (idempotent on re-run) |
|
|
57
|
+
| Plist references `.js`, `.js` file present, `.cjs` file missing | Migrator regenerates plist (writes `.cjs` + updates plist) |
|
|
58
|
+
|
|
59
|
+
No under-block.
|
|
60
|
+
|
|
61
|
+
### Level of abstraction fit
|
|
62
|
+
|
|
63
|
+
- The fix lives at the WRAPPER GENERATION + WRAPPER VERIFICATION layer,
|
|
64
|
+
which is the same layer where the bug originated. Not pushed up into
|
|
65
|
+
the Remediator (out of scope for this PR — Remediator is still under
|
|
66
|
+
Tier 2 rollout per the v3 spec) and not pushed down into launchd (no
|
|
67
|
+
way to influence launchd's interpretation of the plist).
|
|
68
|
+
- `installBootWrapper`'s contract is unchanged from a caller's
|
|
69
|
+
perspective: it still returns `{ sh, js }`. The `js` field name is now
|
|
70
|
+
a slight misnomer (the file is `.cjs`) but every existing caller uses
|
|
71
|
+
it as an opaque path string, so no callsite changes.
|
|
72
|
+
|
|
73
|
+
### Signal-vs-authority compliance
|
|
74
|
+
|
|
75
|
+
| Component | Signal or Authority | Reason |
|
|
76
|
+
|-----------|--------------------|--------|
|
|
77
|
+
| `installBootWrapper` always-`.cjs` | Authority (writes files) | Deterministic, content-based decision. No judgment call. |
|
|
78
|
+
| `ensureBootWrapper` `.cjs` lookup | Signal (returns boolean) | Caller decides what to do with "wrappers missing". |
|
|
79
|
+
| `selfHealPlist` Check 4 | Authority (writes files via installAutoStart) | Existence check is deterministic. The regeneration is a documented recovery primitive. |
|
|
80
|
+
| `migrateBootWrapperToCjs` | Authority (writes files via installAutoStart) | Detection is structural (plist content matches regex). Regeneration is the same primitive other migrations use. |
|
|
81
|
+
| `WakeSocketServer` probe-then-recover | Signal-and-Authority hybrid | Probe is a signal (connect-or-not); the unlink+retry is bounded recovery (single retry, hard timeout). |
|
|
82
|
+
|
|
83
|
+
Compliant. The new authorities are bounded recovery primitives, not
|
|
84
|
+
judgmental gates. The new signals are deterministic structural checks.
|
|
85
|
+
|
|
86
|
+
### Interactions with adjacent systems
|
|
87
|
+
|
|
88
|
+
| System | Interaction | Risk |
|
|
89
|
+
|--------|-------------|------|
|
|
90
|
+
| `NativeModuleHealer` | None direct. NativeModuleHealer fires after boot wrapper loads; this fix ensures the boot wrapper CAN load. | Strictly improves NativeModuleHealer's blast radius. |
|
|
91
|
+
| `ServerSupervisor.preflightSelfHeal` | None direct. Preflight runs after boot wrapper spawns the lifeline. | Same — strictly improves preflight's coverage. |
|
|
92
|
+
| `detectLaunchdSupervised` | None. Runs inside the lifeline; the fix is upstream. | None. |
|
|
93
|
+
| `INSTAR_SUPERVISED` plist env | Compatible — `installMacOSLaunchAgent` regenerates the full plist including this env var. | None. |
|
|
94
|
+
| `RestartOrchestrator` | Same as above — fix is upstream. | None. |
|
|
95
|
+
| `DegradationReporter` | None direct. Migrator failures land in `result.errors[]` which feed the migration report, not Degradation. Acceptable: migrator runs in a known supervised window, not in production hot paths. | None. |
|
|
96
|
+
| `WakeSocketServer` listener daemon usage | The probe-before-clobber change only fires on EADDRINUSE. Normal startup path (no stale file) is unchanged. | Tested with planted-stale-file + live-peer scenarios. |
|
|
97
|
+
| Existing `.js`-wrapper-using agents | Continue to work — `.js` file is NOT deleted; old plists still load; migrator regenerates plist on next update. | Backward compatible by design. |
|
|
98
|
+
| Tests that simulate `instar-boot.js` (e2e/launchd-*) | These tests generate their OWN `.js` wrappers and do NOT go through `installBootWrapper`. Unchanged. | None — verified by reading the test files. |
|
|
99
|
+
|
|
100
|
+
### Rollback cost
|
|
101
|
+
|
|
102
|
+
- Pure code change. Revert is one git revert per affected file.
|
|
103
|
+
- No persistent state changes that block rollback. The migrator runs
|
|
104
|
+
forward-only; rolling back leaves agents on `.cjs` plists (which work
|
|
105
|
+
fine) — the old `.js` files are still around if needed.
|
|
106
|
+
- `WakeSocketServer` change: revert restores pre-bind-unlink behavior.
|
|
107
|
+
The live-peer-clobber behavior re-emerges but is rare in practice
|
|
108
|
+
(single-tenant agents on a machine).
|
|
109
|
+
- Migrator runs idempotently — rolling back and re-deploying produces
|
|
110
|
+
the same end state.
|
|
111
|
+
|
|
112
|
+
## Acceptance criteria
|
|
113
|
+
|
|
114
|
+
1. **Repro of the Echo failure mode.**
|
|
115
|
+
- Test: `tests/unit/boot-wrapper-plist-coherence.test.ts > installBootWrapper — always writes .cjs > does NOT delete a pre-existing instar-boot.js (rollback safety)`
|
|
116
|
+
- Plants a pre-existing `instar-boot.js`, runs `installBootWrapper` with `package.json type=module`. Asserts the `.js` file still exists. Pre-change behavior: file deleted. Post-change: file preserved.
|
|
117
|
+
|
|
118
|
+
2. **selfHealPlist catches drift.**
|
|
119
|
+
- Implicit in `TelegramLifeline.ts` source: `Check 4` extracts the wrapper path from `ProgramArguments` and verifies `fs.existsSync`. Covered by `Check 4` logic.
|
|
120
|
+
|
|
121
|
+
3. **Migrator handles existing `.js`-referencing plists.**
|
|
122
|
+
- Test: `tests/unit/PostUpdateMigrator-bootWrapperCjs.test.ts > detects a plist that references instar-boot.js and attempts regeneration`
|
|
123
|
+
- Plants a plist referencing `instar-boot.js`. Runs the migrator. Asserts the migrator either succeeds (`upgraded`) or surfaces an error (`errors`) — both prove the detection-and-attempt path ran.
|
|
124
|
+
|
|
125
|
+
4. **WakeSocketServer does NOT clobber live peers.**
|
|
126
|
+
- Test: `tests/unit/wake-socket-server-stale-recovery.test.ts > refuses to clobber a live peer (surfaces EADDRINUSE)`
|
|
127
|
+
- Binds a real `net.Server` to the socket path, then attempts to start a second `WakeSocketServer`. Asserts the second emits EADDRINUSE without taking the path away from the live peer.
|
|
128
|
+
|
|
129
|
+
5. **WakeSocketServer recovers from stale socket.**
|
|
130
|
+
- Test: `tests/unit/wake-socket-server-stale-recovery.test.ts > cleans up a stale socket file on start (pre-bind unlink path)` and `> recovers from EADDRINUSE when no live peer is listening`
|
|
131
|
+
- Plants an empty file at the socket path, starts the server, asserts the listen succeeds.
|
|
132
|
+
|
|
133
|
+
## Notes for second-pass reviewer
|
|
134
|
+
|
|
135
|
+
- **Why always `.cjs` and not "always write both `.js` and `.cjs`?":**
|
|
136
|
+
Considered. Always-both would allow rollback to point at `.js` if
|
|
137
|
+
needed. Rejected because: (1) `.js` in a `type=module` project is a
|
|
138
|
+
broken artifact (Node can't load it as CJS); (2) the migrator
|
|
139
|
+
regenerates plists to `.cjs` automatically, so rollback isn't a
|
|
140
|
+
user-visible scenario; (3) keeping an unused broken file invites
|
|
141
|
+
confusion. The current code leaves `.js` files alone but doesn't
|
|
142
|
+
create new ones.
|
|
143
|
+
|
|
144
|
+
- **Why not run the migrator on every lifeline boot instead of relying
|
|
145
|
+
on `PostUpdateMigrator`?** `selfHealPlist` already does the
|
|
146
|
+
equivalent on every lifeline boot — it's Check 4 of that function.
|
|
147
|
+
The migrator entry catches the case where the agent installs the new
|
|
148
|
+
instar version but hasn't yet booted the lifeline (e.g., the
|
|
149
|
+
postupdate migration runs first).
|
|
150
|
+
|
|
151
|
+
- **Why probe-then-unlink instead of just `listen()`-then-error-bubble
|
|
152
|
+
on EADDRINUSE?** Without the recovery path, a stale socket file from
|
|
153
|
+
an unclean exit blocks startup forever until the operator manually
|
|
154
|
+
removes it. The probe-then-unlink preserves correctness (don't
|
|
155
|
+
clobber live peers) while restoring the auto-recovery the original
|
|
156
|
+
pre-bind-unlink path provided.
|
|
157
|
+
|
|
158
|
+
- **Migrator side-effect on darwin only:** Linux systemd unit files
|
|
159
|
+
reference the bash wrapper (`instar-boot.sh`) which is unaffected
|
|
160
|
+
by this change. Non-darwin migrator path is a no-op skip.
|
|
161
|
+
|
|
162
|
+
## Rollback playbook
|
|
163
|
+
|
|
164
|
+
If this PR causes regressions:
|
|
165
|
+
|
|
166
|
+
1. `git revert <pr-merge-sha>` on the canonical instar repo's `main`.
|
|
167
|
+
2. Tag a patch release (e.g., `v1.1.2`) with the revert.
|
|
168
|
+
3. `npx instar` auto-update propagates the revert to all in-the-wild
|
|
169
|
+
agents on next supervision cycle.
|
|
170
|
+
4. Agents whose plists were already migrated to `.cjs` continue to
|
|
171
|
+
work — `installBootWrapper` post-revert writes `.cjs` for projects
|
|
172
|
+
with `type=module`, which matches what the migrated plists reference.
|
|
173
|
+
5. Agents whose plists were NOT yet migrated (still on `.js`)
|
|
174
|
+
continue on `.js` — unchanged from pre-PR state.
|
|
175
|
+
|
|
176
|
+
No persistent state cleanup required.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Side-Effects Review — Lifeline version-skew recovery
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `lifeline-version-skew-recovery`
|
|
4
|
+
**Date:** `2026-05-20`
|
|
5
|
+
**Author:** Echo (instar developer agent)
|
|
6
|
+
**Trigger:** 2026-05-19 → 2026-05-20 b2lead-insights field incident (21h silent
|
|
7
|
+
Telegram ingress drop after server auto-update from v1.0.13 → v1.1.0;
|
|
8
|
+
lifeline stayed on v1.0.13; HTTP 426 from `/internal/telegram-forward`
|
|
9
|
+
went to 3-attempts-then-drop replay policy; restart attempts blocked by
|
|
10
|
+
cooldown; manual operator recovery required).
|
|
11
|
+
|
|
12
|
+
## Summary of the change
|
|
13
|
+
|
|
14
|
+
Five interlocking fixes that together close the silent-drop failure
|
|
15
|
+
class. All independently revertable.
|
|
16
|
+
|
|
17
|
+
1. **Rate-limit cooldown bypass for `versionSkew` bucket.** A
|
|
18
|
+
no-op-against-mismatch wait is replaced with allow-but-rate-limit
|
|
19
|
+
(daily cap still applies). One-line condition change in
|
|
20
|
+
`decide()`.
|
|
21
|
+
|
|
22
|
+
2. **Drop policy guards on `versionSkewActive` instance flag.** Three
|
|
23
|
+
new state operations:
|
|
24
|
+
- `handleVersionSkew` sets `versionSkewActive = true` and fires
|
|
25
|
+
one user-visible alert (dedupe via `versionSkewAlertSentAt`).
|
|
26
|
+
- `forwardToServer` success clears the flag + dedupe timestamp.
|
|
27
|
+
- `replayQueue` re-queues without incrementing failures while
|
|
28
|
+
the flag is set.
|
|
29
|
+
|
|
30
|
+
3. **CLI service label fix.** `com.instar.<name>.lifeline` →
|
|
31
|
+
`ai.instar.<name>`. Matches the plist label written by
|
|
32
|
+
`installMacOSLaunchAgent`. Stops the always-fall-back-to-pkill path.
|
|
33
|
+
|
|
34
|
+
4. **pkill fallback escalates + lock-acquire recognizes wedged-S.**
|
|
35
|
+
CLI: SIGTERM, 3s grace, SIGKILL by pattern. Lock-acquire: detects
|
|
36
|
+
sleeping ('S') state with lock-write age > 5 min as a wedged
|
|
37
|
+
lifeline, sends its own SIGTERM → 3s poll → SIGKILL sequence.
|
|
38
|
+
|
|
39
|
+
5. **`--build-from-source` + `--ignore-scripts` on every native
|
|
40
|
+
rebuild.** Two call sites: ServerSupervisor preflight rebuild
|
|
41
|
+
(line 751) and NativeModuleHealer.healBetterSqlite3Sync
|
|
42
|
+
(line 397). The Remediator-orchestrated path already had these
|
|
43
|
+
flags (NativeModuleHealer.healBetterSqlite3FromRemediator).
|
|
44
|
+
|
|
45
|
+
Files touched (excluding tests / docs):
|
|
46
|
+
- `src/lifeline/rateLimitState.ts` — `decide()` cooldown bypass
|
|
47
|
+
- `src/lifeline/TelegramLifeline.ts` — instance state, `handleVersionSkew`,
|
|
48
|
+
`forwardToServer`, `replayQueue`, `acquireLockFile`
|
|
49
|
+
- `src/cli.ts` — label fix + pkill escalation
|
|
50
|
+
- `src/lifeline/ServerSupervisor.ts` — rebuild flags
|
|
51
|
+
- `src/memory/NativeModuleHealer.ts` — rebuild flags
|
|
52
|
+
|
|
53
|
+
## Decision matrix
|
|
54
|
+
|
|
55
|
+
### Over-block
|
|
56
|
+
|
|
57
|
+
| Scenario | Outcome |
|
|
58
|
+
|----------|---------|
|
|
59
|
+
| Lifeline at SAME version as server (no skew) | No change — `versionSkewActive` stays false |
|
|
60
|
+
| Lifeline at OLD version, server unchanged (no auto-update) | No change — no 426, no skew |
|
|
61
|
+
| Genuine transient 5xx | Drop-after-3 still fires (versionSkewActive is false) |
|
|
62
|
+
| `ForwardServerBootError` (server starting) | Drop-after-3 still fires — known limitation; this PR doesn't try to also branch on serverBoot. Out of scope. Justified: serverBoot is short-lived (seconds), version skew is steady-state until restart. |
|
|
63
|
+
| Lifeline restart loop after fix | Daily cap (3-per-24h) plus storm detection bound the loop |
|
|
64
|
+
|
|
65
|
+
No over-block. The new authority only fires when ALL of:
|
|
66
|
+
1. The forward threw `ForwardVersionSkewError`,
|
|
67
|
+
2. The server version in the body differs from the lifeline's,
|
|
68
|
+
3. The body's `upgradeRequired === true`.
|
|
69
|
+
|
|
70
|
+
That's the same gate that already triggers the restart request, so we
|
|
71
|
+
haven't widened the surface — just made the consequences during the
|
|
72
|
+
window correct.
|
|
73
|
+
|
|
74
|
+
### Under-block
|
|
75
|
+
|
|
76
|
+
| Scenario | Outcome |
|
|
77
|
+
|----------|---------|
|
|
78
|
+
| Lifeline version "skews" because of a malformed 426 body | Guarded by existing `body.upgradeRequired !== true` and `serverVersion === this.lifelineVersion` checks — they were already in `handleVersionSkew` |
|
|
79
|
+
| Lifeline tries to forge a skew to bypass cooldown | The 426 must come from the SERVER (loopback addr only on a privileged port); the lifeline cannot self-trigger the body shape |
|
|
80
|
+
| Two version-skew episodes in <24h (daily cap reached) | Falls through to existing `version-skew-daily-cap` denial — observability path remains intact |
|
|
81
|
+
| Stuck-lock detection: live healthy lifeline misidentified as wedged | Requires lock-age > 5 min AND 'S' state — a healthy lifeline writes the lock, immediately starts polling Telegram (lots of 'R' time), and answers heartbeats. Misidentification window is narrow but possible during long Telegram long-poll waits. Acceptable: false positive triggers SIGTERM → 3s grace → SIGKILL of a healthy lifeline → launchd respawns it within seconds. Worst case: ~5s downtime + a fresh boot. Far better than the 21h+ silent drop the current code permits. |
|
|
82
|
+
| Forced rebuild with `--build-from-source` fails because node-gyp toolchain absent | Already-existing failure path: spawnSync returns non-zero, healer logs error, falls back to existing escalation. No new silent failure. |
|
|
83
|
+
|
|
84
|
+
### Level of abstraction fit
|
|
85
|
+
|
|
86
|
+
- Rate-limit bypass — at the decision-point that already knows about
|
|
87
|
+
buckets. Right layer.
|
|
88
|
+
- Drop policy — at the replay loop that already has the failure-counter
|
|
89
|
+
state. Right layer. Could have lived in `forwardToServer` instead,
|
|
90
|
+
but the queue is what owns "should this message be dropped" — the
|
|
91
|
+
forward path only owns "did this single attempt succeed".
|
|
92
|
+
- CLI label — at the kickstart callsite. One source of truth.
|
|
93
|
+
- pkill escalation — at the CLI fallback. The CLI is the place where
|
|
94
|
+
"user typed `instar lifeline restart`" gets translated to a system
|
|
95
|
+
action.
|
|
96
|
+
- Lock-acquire — at the lock-acquire function. The detector and the
|
|
97
|
+
recovery primitive are both bounded mechanics in the same scope.
|
|
98
|
+
- Rebuild flags — at the rebuild callsites. No new abstraction.
|
|
99
|
+
|
|
100
|
+
No over-engineering. Each change is at the layer that already owns
|
|
101
|
+
the concern.
|
|
102
|
+
|
|
103
|
+
### Signal-vs-authority compliance
|
|
104
|
+
|
|
105
|
+
- `rateLimitState.decide` — returns signal. No authority change.
|
|
106
|
+
- `versionSkewActive` flag — pure state signal. Replay loop is the
|
|
107
|
+
authority that decides re-queue vs drop.
|
|
108
|
+
- `handleVersionSkew` — already had authority over "request a restart".
|
|
109
|
+
Now also: sets the flag and sends an alert. Both are bounded
|
|
110
|
+
mechanics with clear preconditions, not judgmental gates.
|
|
111
|
+
- CLI label — mechanical change.
|
|
112
|
+
- pkill escalation — bounded recovery (fixed 3s grace, single SIGKILL).
|
|
113
|
+
- Lock-acquire stuck-S — bounded recovery (fixed 5min threshold for
|
|
114
|
+
age, 3s SIGTERM grace, single SIGKILL).
|
|
115
|
+
- Rebuild flags — mechanical change.
|
|
116
|
+
|
|
117
|
+
Compliant. No new judgmental gates; new authorities are bounded
|
|
118
|
+
recovery primitives.
|
|
119
|
+
|
|
120
|
+
### Interactions with adjacent systems
|
|
121
|
+
|
|
122
|
+
| System | Interaction | Risk |
|
|
123
|
+
|--------|-------------|------|
|
|
124
|
+
| `MessageQueue` | Replay loop re-queues messages during skew (same enqueue path used for transient failures) | None — queue persistence semantics unchanged |
|
|
125
|
+
| `notifyMessageDropped` | NOT called during version-skew episodes (drop is suppressed) | Strictly improves — these messages will deliver |
|
|
126
|
+
| `DegradationReporter` | Still fires for non-skew drops; version-skew episode itself emits a degradation via the existing path | None |
|
|
127
|
+
| `RestartOrchestrator` | Receives a `versionSkew` request that's now more likely to be granted (cooldown bypass) | Storm detection + daily cap remain as backstops |
|
|
128
|
+
| `ServerSupervisor.preflightSelfHeal` | Rebuild flag change is a strict improvement | None |
|
|
129
|
+
| Remediator (Tier 1+) | Not yet shipped; in-line healer is the steady-state path. When Remediator ships, its rebuild already used `--build-from-source` — no conflict | None |
|
|
130
|
+
| `NativeModuleHealer.invokeFromRemediator` | Unchanged (already had the flag) | None |
|
|
131
|
+
| `LifelineHealthWatchdog` | Tracks `consecutiveForwardFailures`; this PR clears it on success (existing behavior). No change to the watchdog's trip conditions | None |
|
|
132
|
+
| `WatchdogTriggers.versionSkew` | The flag-set + alert-send happens BEFORE `initiateRestart`. If the restart is rate-limited, the alert still fires — good (user knows ingress is paused even while the lifeline is being patient about restarts) | None |
|
|
133
|
+
| `acquireLockFile` callers | The added SIGTERM-then-SIGKILL path runs in the boot-startup window. New lifelines that find a wedged old one will spend up to 3s waiting for graceful exit. Acceptable | None |
|
|
134
|
+
| Existing tests | All 18 `rateLimitState.test.ts` + 14 `lifeline-shadow-install-self-heal.test.ts` + 9 `NativeModuleHealer.test.ts` tests pass unchanged | Verified |
|
|
135
|
+
|
|
136
|
+
### Rollback cost
|
|
137
|
+
|
|
138
|
+
- Each fix is independently revertable.
|
|
139
|
+
- No persistent state schema changes; the new lifeline instance
|
|
140
|
+
fields (`versionSkewActive`, `versionSkewAlertSentAt`) are not
|
|
141
|
+
serialized to disk.
|
|
142
|
+
- No migration entry; existing in-the-wild agents pick up the fix on
|
|
143
|
+
their next `npx instar` update like any other patch.
|
|
144
|
+
- Reverting the rebuild flag returns to the pre-existing flaky
|
|
145
|
+
rebuild behavior — agents that had drifted to "rebuild succeeded
|
|
146
|
+
but still fails" on the old code path continue exactly as they
|
|
147
|
+
did.
|
|
148
|
+
|
|
149
|
+
## Acceptance criteria (test-mapped)
|
|
150
|
+
|
|
151
|
+
1. `tests/unit/lifeline/rateLimitState.test.ts > versionSkew bucket
|
|
152
|
+
BYPASSES the cooldown` — asserts the new bypass.
|
|
153
|
+
2. `tests/unit/lifeline/version-skew-recovery.test.ts > CLI service
|
|
154
|
+
label` — asserts `ai.instar.${...}` is in source and
|
|
155
|
+
`com.instar.${...}.lifeline` is gone.
|
|
156
|
+
3. `tests/unit/lifeline/version-skew-recovery.test.ts > pkill
|
|
157
|
+
fallback escalates to SIGKILL` — asserts both signal calls
|
|
158
|
+
present in the restart command source.
|
|
159
|
+
4. `tests/unit/lifeline/version-skew-recovery.test.ts >
|
|
160
|
+
ServerSupervisor preflight rebuild passes --build-from-source` —
|
|
161
|
+
asserts the rebuild argv on the supervisor side.
|
|
162
|
+
5. `tests/unit/lifeline/version-skew-recovery.test.ts >
|
|
163
|
+
NativeModuleHealer in-line rebuild passes --build-from-source` —
|
|
164
|
+
asserts EVERY rebuild call in the healer source has the flag.
|
|
165
|
+
6. `tests/unit/lifeline/version-skew-recovery.test.ts > lifeline
|
|
166
|
+
source guards drop with versionSkewActive` — asserts the source
|
|
167
|
+
order (skew bypass before drop check).
|
|
168
|
+
7. `tests/unit/lifeline/version-skew-recovery.test.ts >
|
|
169
|
+
handleVersionSkew sets the active flag + alert dedupe`.
|
|
170
|
+
8. `tests/unit/lifeline/version-skew-recovery.test.ts >
|
|
171
|
+
forwardToServer success clears the version-skew episode flag`.
|
|
172
|
+
9. `tests/unit/lifeline/version-skew-recovery.test.ts > lock-acquire
|
|
173
|
+
treats sleeping (S) state > 5 min as recoverable`.
|
|
174
|
+
|
|
175
|
+
## Forward note
|
|
176
|
+
|
|
177
|
+
If the auto-update flow eventually grows a "tell the lifeline to
|
|
178
|
+
restart" step, the failure class this PR addresses becomes
|
|
179
|
+
impossible-by-construction. Until then, this is the steady-state
|
|
180
|
+
recovery for the window between server-update and lifeline-restart.
|