instar 1.3.326 → 1.3.328

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 (44) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +2 -1
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/ForwardedTopicContext.d.ts.map +1 -1
  5. package/dist/core/ForwardedTopicContext.js +4 -6
  6. package/dist/core/ForwardedTopicContext.js.map +1 -1
  7. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  8. package/dist/core/PostUpdateMigrator.js +23 -5
  9. package/dist/core/PostUpdateMigrator.js.map +1 -1
  10. package/dist/lifeline/ServerSupervisor.d.ts +8 -0
  11. package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
  12. package/dist/lifeline/ServerSupervisor.js +25 -0
  13. package/dist/lifeline/ServerSupervisor.js.map +1 -1
  14. package/dist/lifeline/SlowRetrySentinelEscalation.d.ts +45 -0
  15. package/dist/lifeline/SlowRetrySentinelEscalation.d.ts.map +1 -0
  16. package/dist/lifeline/SlowRetrySentinelEscalation.js +51 -0
  17. package/dist/lifeline/SlowRetrySentinelEscalation.js.map +1 -0
  18. package/dist/lifeline/TelegramLifeline.d.ts +6 -0
  19. package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
  20. package/dist/lifeline/TelegramLifeline.js +25 -1
  21. package/dist/lifeline/TelegramLifeline.js.map +1 -1
  22. package/dist/memory/TopicMemory.d.ts.map +1 -1
  23. package/dist/memory/TopicMemory.js +3 -2
  24. package/dist/memory/TopicMemory.js.map +1 -1
  25. package/dist/messaging/shared/compactionResumePayload.d.ts.map +1 -1
  26. package/dist/messaging/shared/compactionResumePayload.js +2 -1
  27. package/dist/messaging/shared/compactionResumePayload.js.map +1 -1
  28. package/dist/server/routes.d.ts.map +1 -1
  29. package/dist/server/routes.js +3 -2
  30. package/dist/server/routes.js.map +1 -1
  31. package/dist/utils/localTime.d.ts +42 -0
  32. package/dist/utils/localTime.d.ts.map +1 -0
  33. package/dist/utils/localTime.js +60 -0
  34. package/dist/utils/localTime.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/data/builtin-manifest.json +67 -67
  37. package/src/templates/hooks/compaction-recovery.sh +15 -3
  38. package/src/templates/hooks/slack-channel-context.sh +1 -1
  39. package/src/templates/hooks/telegram-topic-context.sh +8 -2
  40. package/upgrades/1.3.327.md +57 -0
  41. package/upgrades/1.3.328.md +44 -0
  42. package/upgrades/local-time-coherence.eli16.md +11 -0
  43. package/upgrades/side-effects/local-time-coherence.md +89 -0
  44. package/upgrades/side-effects/supervisor-sentinel-escalation.md +61 -0
@@ -105,6 +105,12 @@ fi
105
105
  # Format and output context with unanswered message detection
106
106
  echo "$RECENT_MSGS" | python3 -c "
107
107
  import sys, json
108
+ def _localts(raw):
109
+ try:
110
+ from datetime import datetime
111
+ return datetime.fromisoformat(str(raw).replace('Z', '+00:00')).astimezone().strftime('%Y-%m-%d %H:%M %Z')
112
+ except Exception:
113
+ return str(raw)[:16].replace('T', ' ')
108
114
  try:
109
115
  data = json.load(sys.stdin)
110
116
  msgs = data.get('messages', [])
@@ -114,7 +120,7 @@ try:
114
120
  print('TOPIC ${TOPIC_ID} RECENT HISTORY (auto-injected — read this before responding):')
115
121
 
116
122
  for m in msgs:
117
- ts = m.get('timestamp', '')[:16].replace('T', ' ')
123
+ ts = _localts(m.get('timestamp', ''))
118
124
  from_user = m.get('fromUser', m.get('direction', 'in') == 'in')
119
125
  text = m.get('text', '').strip()
120
126
  sender = 'User' if from_user else 'Agent'
@@ -139,7 +145,7 @@ try:
139
145
  print('*** UNANSWERED MESSAGE(S) FROM USER ***')
140
146
  for pm in pending_user:
141
147
  pm_text = pm.get('text', '')[:200]
142
- pm_ts = pm.get('timestamp', '')[:16].replace('T', ' ')
148
+ pm_ts = _localts(pm.get('timestamp', ''))
143
149
  print(f' [{pm_ts}] \"{pm_text}\"')
144
150
  print()
145
151
  print('You MUST address these messages substantively. Do NOT respond with just')
@@ -0,0 +1,57 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Fixes a **time-incoherency** class of bug: every context block that shows an
9
+ agent its conversation history rendered timestamps as **unlabeled UTC**
10
+ (`[21:23:10]`), while the CURRENT TIME hook block speaks labeled local time.
11
+ Agents read both as one clock — live incident 2026-06-05: an agent told its
12
+ user "you heard nothing between **9:23pm** and now" about an event at
13
+ **2:23pm** the user's local time.
14
+
15
+ Now every agent-facing timestamp renders in the **host's local timezone with
16
+ an explicit tz label and date**: `[2026-06-05 14:23 PDT]`.
17
+
18
+ Surfaces covered:
19
+
20
+ - Bootstrap Thread History (new/respawned sessions), auto-spawn history
21
+ - Moved-session context relay (multi-machine transfers)
22
+ - Per-message Telegram topic history hook + unanswered-messages block
23
+ - Post-compaction recovery context (Telegram + Slack blocks)
24
+ - Session-start RECENT MESSAGES, TopicMemory context blocks
25
+ - Slack channel context (tz label added)
26
+ - Lifeline "Last healthy" status line
27
+
28
+ New shared helper `src/utils/localTime.ts` (`formatLocalTimestamp`); hook
29
+ python blocks carry an equivalent `_localts()` with a safe fallback to the
30
+ old rendering on any parse failure. Stored timestamps remain ISO-UTC —
31
+ rendering only. Existing agents pick the hook changes up automatically
32
+ (built-in hooks are always-overwritten on migration).
33
+
34
+ ## What to Tell Your User
35
+
36
+ Your agent no longer gets confused about what time things happened. Before
37
+ this, the agent saw conversation history stamped in UTC but the current time
38
+ in your local timezone — and could misquote a 2pm event as "9pm." Now every
39
+ timestamp it reads carries your local time and timezone, so "when did I last
40
+ hear from you?" answers come out right.
41
+
42
+ ## Summary of New Capabilities
43
+
44
+ - None user-invocable — this is a correctness fix to how agents perceive
45
+ time. (Internal: `formatLocalTimestamp()` in `src/utils/localTime.ts` is
46
+ the one helper all history/status renderers now use.)
47
+
48
+ ## Evidence
49
+
50
+ - New `tests/unit/localTime.test.ts` pins local-not-UTC semantically
51
+ (TZ-portable: compares against the same instant's local Date getters),
52
+ including the verbatim incident instant `2026-06-05T21:23:10Z`.
53
+ - Hook python smoke test: the incident payload now renders
54
+ `[2026-06-05 14:23 PDT] Agent: executing now` (was `[2026-06-05 21:23]`).
55
+ - Updated `ForwardedTopicContext` / `compactionResumePayload` /
56
+ `telegram-autospawn-history` tests to TZ-portable assertions through the
57
+ helper; affected-area suites green; `tsc --noEmit` clean.
@@ -0,0 +1,44 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The ServerSupervisor's slow-retry mode (respawn a dead server every 2h, forever)
9
+ is a sanctioned Eternal Sentinel under the new "No Unbounded Loops" standard —
10
+ but it violated condition 4: it never told anyone. A multi-day crash-revive loop
11
+ was indistinguishable from silence. Now a pure one-shot latch
12
+ (`SlowRetrySentinelEscalation`, the AgeKillBackoff suppressor shape) fires
13
+ exactly once per outage episode after a sustained-failure threshold (default
14
+ 12h ≈ 6 failed cycles): the supervisor emits `sentinelStalled` and the lifeline
15
+ sends ONE operator message with the two useful levers (`/lifeline doctor`,
16
+ `/lifeline reset`), then the sentinel keeps quietly retrying. The message goes
17
+ directly to the Telegram Bot API from the lifeline process — no dependency on
18
+ the (down) agent server. Recovery re-arms the latch via the single
19
+ `resetCircuitBreaker()` funnel; retry/kill/spawn decisions are unchanged.
20
+
21
+ ## What to Tell Your User
22
+
23
+ If your agent's server ever gets stuck in a crash loop, you'll now get one
24
+ clear heads-up after ~12 hours — "still down, here's how to diagnose or force a
25
+ retry, I'll keep trying" — instead of discovering the outage by noticing your
26
+ agent went quiet for days. One message per outage, never a stream.
27
+
28
+ ## Summary of New Capabilities
29
+
30
+ - Slow-retry escalation: a sustained server outage (default 12h in slow-retry)
31
+ produces exactly one operator notification per episode; the never-give-up
32
+ revival behavior is unchanged. In-code default; no config required.
33
+
34
+ ## Evidence
35
+
36
+ Loop-safety audit finding (CMT-1109), verified at source: the slow-retry block
37
+ in `src/lifeline/ServerSupervisor.ts` was annotated "never truly give up" with
38
+ no operator signal. Fix is the first PR under constitution P19 ("No Unbounded
39
+ Loops", incl. Justin's Eternal Sentinel caveat — this loop is its namesake
40
+ case). Tests: 10 green in `tests/unit/SlowRetrySentinelEscalation.test.ts`
41
+ including the P19 sustained-failure bound (week-long never-recovering episode →
42
+ exactly 1 escalation) and source-shape wiring pins. Independent adversarial
43
+ second-pass: CONCUR (all probes safe — per-tick reachability, no double-fire,
44
+ server-independent delivery, emit-before-spawn ordering). tsc clean.
@@ -0,0 +1,11 @@
1
+ # The agent stops misreading its own clock
2
+
3
+ The agent kept two clocks without knowing it. Every hook that tells the agent "here's the current time" speaks in YOUR local time ("5:49pm PDT") — but every block that shows it conversation history spoke in unlabeled UTC ("[21:23]"). The agent has no way to tell those apart, so it did the natural thing: read both as the same clock. That's how it told Justin "you heard nothing between **9:23pm** and now" about a message sent at **2:23pm** his time — off by exactly the 7-hour UTC gap. The user experiences this as the agent being confused about basic reality.
4
+
5
+ The fix makes every timestamp the agent ever sees speak ONE language: the machine's local time, with the timezone written right on it — `[2026-06-05 14:23 PDT]` instead of `[21:23:10]`. History lines also now carry the date, so a conversation spanning midnight can't be misread either ("was that 12:30am today or yesterday?" stops being a guess).
6
+
7
+ Where this applies — every surface that renders history or status timestamps into an agent's context: the thread history a new session boots with, the history relayed when a conversation moves between machines, the recent-messages block injected on every Telegram message, the post-compaction recovery context, the Slack channel context, the session-start recent messages, and the lifeline's "last healthy" status line. One new shared helper does the rendering in code; the hook scripts get a tiny equivalent helper with a safe fallback (if a timestamp ever fails to parse, it renders the old way instead of crashing the hook).
8
+
9
+ Existing agents get this automatically: the built-in hook scripts are always refreshed on update, and the code paths ship with the next release. Nothing about WHEN things happen changes — only how the time is written down. There is no new config, no new decision surface, and the worst possible failure (a malformed timestamp) renders the way it used to render rather than breaking anything.
10
+
11
+ What you need to decide: nothing — this is a pure rendering fix for a live mis-statement the agent made to you. The PR is the review surface.
@@ -0,0 +1,89 @@
1
+ # Side-Effects Review — Local-time + tz-label rendering for all agent-facing timestamps
2
+
3
+ **Version / slug:** `local-time-coherence`
4
+ **Date:** `2026-06-05`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required (no block/allow surface, no lifecycle decisions — pure rendering)`
7
+
8
+ ## Summary of the change
9
+
10
+ Every surface that renders timestamps into agent-facing context blocks switched from unlabeled UTC (`toISOString().slice(11, 19)` in TS, `timestamp[:16].replace('T', ' ')` in hook python) to host-local time with an explicit timezone label and date (`[2026-06-05 14:23 PDT]`). New shared helper `src/utils/localTime.ts` (`formatLocalTimestamp`, `localTzAbbreviation`); call sites: `ForwardedTopicContext.ts`, `TopicMemory.ts` ×2, `commands/server.ts` (bootstrap Thread History), `server/routes.ts` ×2 (auto-spawn/respawn history), `compactionResumePayload.ts`, `TelegramLifeline.ts` (status line). Hook python blocks get an equivalent `_localts()` helper with parse-failure fallback to the old rendering: `templates/hooks/telegram-topic-context.sh`, `templates/hooks/compaction-recovery.sh` (telegram + slack blocks), `templates/hooks/slack-channel-context.sh` (tz label only), and the `PostUpdateMigrator.ts` inline copies (session-start, telegram-topic-context, compaction-recovery). Driven by the 2026-06-05 live incident: the agent reported a 14:23-local event as "9:23pm" because history was unlabeled UTC while CURRENT TIME blocks are local-labeled.
11
+
12
+ ## Decision-point inventory
13
+
14
+ No decision-point surface. This change renders strings; it adds, removes, or modifies no block/allow decision, no gate, no lifecycle action. The only conditional logic is "did the timestamp parse?" with a render-the-old-way fallback.
15
+
16
+ ---
17
+
18
+ ## 1. Over-block
19
+
20
+ No block/allow surface — over-block not applicable.
21
+
22
+ ---
23
+
24
+ ## 2. Under-block
25
+
26
+ No block/allow surface — under-block not applicable.
27
+
28
+ ---
29
+
30
+ ## 3. Level-of-abstraction fit
31
+
32
+ Right layer: a formatting primitive at the render chokepoints. The alternative — teaching agents (prompt-level) that history is UTC — is exactly the willpower-dependent design that caused the incident. A lower-level primitive did not exist (`formatLocalTimeHHMM` in `RestartCascadeDampener.ts` is HH:MM-only, no tz label, no date, not exported for this purpose); this PR creates the shared primitive and routes all sites through it. Hook python cannot import TS, so it carries a minimal equivalent — acceptable duplication at the language boundary, kept identical across all blocks.
33
+
34
+ ---
35
+
36
+ ## 4. Signal vs authority compliance
37
+
38
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
39
+
40
+ - [x] No — this change has no block/allow surface.
41
+
42
+ Pure rendering. Nothing here holds authority over any action.
43
+
44
+ ---
45
+
46
+ ## 5. Interactions
47
+
48
+ - **Shadowing:** none — no checks run before/after; the rendered string feeds the same downstream consumers (context injection, tmux paste) unchanged in shape (`[ts] Sender: text`).
49
+ - **Double-fire:** none — formatting is idempotent and side-effect-free.
50
+ - **Races:** none — no shared mutable state; `Intl.DateTimeFormat` is constructed per call.
51
+ - **Feedback loops:** one considered: agents QUOTE history timestamps back into conversation, and those quotes can land in future history. Since both the injected history and the CURRENT TIME hook now speak local-labeled time, the loop converges toward coherence instead of propagating UTC misreads.
52
+ - **Unanswered-message detection:** verified untouched — detection logic compares message order/sender, never parses the rendered timestamp.
53
+ - **Tests that mirror renderers:** `telegram-autospawn-history.test.ts` contains a local mirror of the routes.ts logic; updated in the same PR so the mirror stays faithful.
54
+
55
+ ---
56
+
57
+ ## 6. External surfaces
58
+
59
+ - **Other agents on the same machine:** the hook-template changes reach every instar agent on next update (built-in hooks are always-overwritten by `PostUpdateMigrator`). The change is rendering-only; no agent behavior contract depends on UTC timestamps (verified: no consumer parses the bracketed time back out of context blocks — grepped for consumers of the rendered format).
60
+ - **External systems:** none — Telegram/Slack/GitHub payloads unchanged; only locally-injected context strings change.
61
+ - **Persistent state:** none written. Stored timestamps remain ISO-UTC everywhere (JSONL, SQLite); ONLY rendering changes. No migration of stored data.
62
+ - **Timing/runtime:** `%Z` in python `strftime` and `Intl` tz short-names vary by platform locale data; on a host where no label resolves, the label is simply omitted (TS) or the naive fallback renders (python) — degraded to the status quo, never an error.
63
+
64
+ ---
65
+
66
+ ## 7. Rollback cost
67
+
68
+ Pure code + template change — revert and ship a patch. No persistent state, no data migration, no agent state repair. During a rollback window, agents would briefly render local-labeled and then UTC again; cosmetic only.
69
+
70
+ ---
71
+
72
+ ## Conclusion
73
+
74
+ The review confirmed no decision surface and no persistence surface; the main risks identified and addressed were (a) hook python crashing on unparseable timestamps — covered by the try/except fallback to the previous rendering, and (b) test mirrors drifting from real renderers — the mirror was updated in the same change. One pre-existing inconsistency was found and normalized: `formatInlineHistory` treated numeric-0 timestamps as missing while `ForwardedTopicContext` rendered them; the shared helper renders them (epoch is a valid instant). Clear to ship.
75
+
76
+ ---
77
+
78
+ ## Second-pass review (if required)
79
+
80
+ **Reviewer:** not required
81
+ **Independent read of the artifact:** not required — no messaging block/allow, no session lifecycle, no sentinel/gate/guard surface.
82
+
83
+ ---
84
+
85
+ ## Evidence pointers
86
+
87
+ - `tests/unit/localTime.test.ts` — semantic local-not-UTC pinning incl. the verbatim incident instant `2026-06-05T21:23:10Z`.
88
+ - Hook python smoke test: incident payload renders `[2026-06-05 14:23 PDT] Agent: executing now` (was `[2026-06-05 21:23]`).
89
+ - Full local suite (74 min on a loaded box): 28329 passed / 8 failed across 5 files. The one failure whose identity survived output truncation (`session-management-e2e`, a waitFor timeout) re-ran green in isolation; a full re-run with complete capture was started for the rest, and CI (clean, sharded runners) is the merge gate either way.
@@ -0,0 +1,61 @@
1
+ # Side-Effects Review — Slow-Retry Sentinel Escalation
2
+
3
+ **Version / slug:** `supervisor-sentinel-escalation`
4
+ **Date:** `2026-06-05`
5
+ **Author:** `Echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `independent adversarial reviewer subagent — CONCUR (all four probes safe; no blocking defects; non-blocking notes recorded below)`
7
+
8
+ ## Summary of the change
9
+
10
+ The ServerSupervisor's slow-retry mode (2h respawn cadence, deliberately forever) gains the Eternal Sentinel's condition-4 observability: a pure one-shot latch (`src/lifeline/SlowRetrySentinelEscalation.ts`) fires once per outage episode after `escalateAfterMs` (default 12h), the supervisor emits `'sentinelStalled'`, and `TelegramLifeline.notifySentinelStalled` delivers ONE operator message (doctor/reset levers), after which the sentinel keeps retrying unchanged. `resetCircuitBreaker()` — the single episode-ending funnel — re-arms the latch in lockstep with zeroing `slowRetryStartedAt`. Files: `SlowRetrySentinelEscalation.ts` (new, pure), `ServerSupervisor.ts` (declaration comment + per-tick check + emit + reset hook + one constructor option), `TelegramLifeline.ts` (listener + notify method), one test file.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `ServerSupervisor` slow-retry block — **modify (additive)** — gains a read-only latch check + event emit; the retry decision, cadence, kill, and spawn are untouched.
15
+ - `SlowRetrySentinelEscalation` — **add** — a pure signal producer with no authority of any kind.
16
+ - `TelegramLifeline` event wiring — **add** — delivery only; mirrors the existing `circuitBroken` handler shape (fire-and-forget, catch-swallowed).
17
+
18
+ ## 1. Over-block
19
+
20
+ Nothing is blocked — this change cannot suppress, delay, or alter any retry or recovery action. The only "cost" added is one Telegram message per ≥12h outage episode.
21
+
22
+ ## 2. Under-block
23
+
24
+ (a) A lifeline PROCESS restart zeroes the in-memory latch with all breaker state; after ~13.5h of freshly rebuilt sustained failure it would notify again. Reviewer-assessed as correct-by-design ("still broken after a bounce" is a legitimate re-notification), and persisting the latch was explicitly rejected — a stale on-disk latch could suppress a legitimate notification, the worse failure. (b) The threshold is time-based, not attempt-based: a machine asleep for 12h would escalate on wake even though few attempts ran — acceptable; the operator-facing claim ("down ~N hours") remains true. (c) Other eternal-sentinel-shaped loops (lease pull) are NOT covered here — next audit PR <!-- tracked: CMT-1109 -->.
25
+
26
+ ## 3. Level-of-abstraction fit
27
+
28
+ Yes. The latch lives beside the loop it observes (lifeline package), as a pure helper in the established suppressor shape (`AgeKillBackoff`: injectable clock, bounded state, unit-testable). Delivery rides the existing supervisor→lifeline event channel (`circuitBroken` precedent) rather than inventing a new notification path — and deliberately does NOT use the agent server's attention queue, which is definitionally down when this fires.
29
+
30
+ ## 4. Signal vs authority compliance
31
+
32
+ **Required reference:** `docs/signal-vs-authority.md`
33
+
34
+ - [x] No — pure signal. The latch can only cause one message; it holds no authority over retries, kills, spawns, or the circuit breaker. The healer's behavior is byte-identical with the latch removed.
35
+
36
+ ## 5. Interactions
37
+
38
+ - **Double-fire:** impossible within a process — the episode key (`slowRetryStartedAt`) is written only at episode start (when 0) and zeroed only inside `resetCircuitBreaker()`, which re-arms the latch in the same method; key and latch cannot desync (reviewer traced every write).
39
+ - **Starvation:** the slow-retry block runs every 10s health-check tick while broken (the branch returns without resetting `consecutiveFailures`), so the latch check cannot be starved (reviewer traced `evaluateUnhealthyServer` → `handleUnhealthy` reachability).
40
+ - **Ordering:** the emit's listener is fire-and-forget and catch-swallowed; it cannot block or throw into the same tick's kill/spawn (mirrors `circuitBroken`).
41
+ - **Feedback loops:** none — a message cannot change health-check outcomes.
42
+
43
+ ## 6. External surfaces
44
+
45
+ - **User-visible:** one new Telegram message class, bounded at one per episode (Bounded Notification Surface-compatible: no topic creation — it posts to the existing lifeline topic).
46
+ - **Delivery independence:** `sendToTopic` → Telegram Bot API directly over HTTPS from the lifeline process; zero dependency on the down agent server (reviewer-verified at `apiCall`).
47
+ - **Persistent state / config / schema:** none. In-code default; `slowRetryEscalateAfterMs` is a constructor option for tests only. No migration (Migration Parity: nothing installed changes).
48
+
49
+ ## 7. Rollback cost
50
+
51
+ Revert the commit. No state to clean, no config to unwind. The only observable regression of rollback is the silence returning.
52
+
53
+ ## Conclusion
54
+
55
+ Signal-only observability for the constitution's namesake Eternal Sentinel: persistence preserved, silence eliminated, volume bounded at one message per episode and proven by the P19 sustained-failure test (week-long never-recovering episode → exactly 1). Second-pass reviewer CONCUR with zero blocking findings; both non-blocking notes (restart re-fire window, no Tier-2/3 for a route-less signal change) are recorded in §2/§6 with their rationale.
56
+
57
+ ---
58
+
59
+ ## Phase 5 — Second-pass review (server-lifecycle-adjacent → performed)
60
+
61
+ An independent adversarial reviewer audited the diff and final files at line level against four probes: (1) any >1-fire-per-outage path — traced every `slowRetryStartedAt`/`circuitBroken` write; none desync the episode key from the latch; lifeline-restart re-fire requires ~13.5h of rebuilt failure and is a correct re-notification; (2) any never-fire path — the slow-retry branch is reached every 10s tick while broken (per-tick reachability traced through `evaluateUnhealthyServer`/`handleUnhealthy`); (3) delivery dependence on the down server — none: the lifeline posts straight to the Telegram Bot API; (4) emit-vs-spawn ordering — fire-and-forget listener, cannot block or throw into the spawn. Ran the 10-test suite (green) and `tsc --noEmit` (clean). **Verdict: CONCUR.**