# Upgrade Guide — vNEXT

<!-- assembled-by: assemble-next-md -->
<!-- bump: patch -->

## What Changed

Makes the SentinelNotifier's consolidated-escalation delivery **self-heal a deleted lifeline/system topic instead of silently swallowing the error**. Both `sendConsolidated` closures in `server.ts` (the sentinel-notify path and the stop-notify path) did `try { telegram.sendToTopic(lifelineTopicId, text) } catch { return false }`. When the lifeline/system topic is deleted on the Telegram side, every send returns `400: message thread not found`, and the bare `catch` black-holed it — so stall/stop escalations the system generated never reached the user, with zero log trace. New shared helper `sendConsolidatedWithSelfHeal` (`src/monitoring/sentinelConsolidatedSend.ts`): sends to the lifeline topic; on failure it (1) logs the real error (de-swallow) and (2) calls the adapter's existing `ensureLifelineTopic()` — which recreates a deleted topic and persists the new id — then retries the send once. Both call sites now use it. No behavior change on the happy path (a working topic sends and returns immediately, never touching `ensureLifelineTopic`).

## What to Tell Your User

If your "Lifeline"/system topic ever gets deleted, the alerts the system tries to send you there (a session went quiet, a session stopped) used to vanish without a trace — you'd just get silence. Now those alerts heal themselves: the system recreates the topic and delivers the alert, and any delivery failure is logged instead of disappearing. (This is the foundation under the broader "tell me when a session stalls, in that session's own topic, and auto-recover it" work.)

## Summary of New Capabilities

| Capability | How to Use |
|-----------|-----------|
| Sentinel/stop escalations self-heal a deleted lifeline topic + never swallow the send error | automatic — no config |

## Evidence

Reproduction (live, 2026-06-09): the configured lifeline topic (`lifelineTopicId: 2`, "Lifeline") had been deleted on Telegram. `logs/sentinel-events.jsonl` showed the active-silence sentinel correctly detecting stalled sessions and emitting `escalated` ("session went quiet ~16 min ago, want me to dig in?") — immediately followed by `notify-error: sendConsolidated returned false`, **41 times in one day**. A direct probe (`POST /telegram/reply/2`) returned exactly `400: Bad Request: message thread not found`, confirming the dead topic. The user received pure silence, so a stalled session was indistinguishable from a working one.

After the fix: `tests/unit/sentinel-consolidated-send.test.ts` (7 cases) pins the happy path (no ensureLifelineTopic call), the dead-topic self-heal (recreate id 2→new, retry, deliver), the no-topic-configured path, and all three failure modes (ensure returns null / ensure throws / retry fails) — each returning false WITH a logged reason, never a silent swallow. tsc + repo lint clean. (The 15 unrelated A2ARedeliverySentinel / SessionActivitySentinel local failures are pre-existing on main, not introduced here.)

## The problem

The operator couldn't tell whether a session had stalled or was just working —
because for almost an hour it heard nothing. Here's what was actually happening
under the hood, and it's worse than "the agent didn't notice."

The agent's stall-detector **did** notice. It saw a session go quiet, gave it a
nudge, and tried to send the operator a message: "this session went quiet ~16
minutes ago, want me to dig in?" That message goes to a dedicated "Lifeline"
system topic in Telegram.

But that Lifeline topic had been **deleted** on the Telegram side, while the
config still pointed at it. So every send came back with `message thread not
found` — and the code did `catch { return false }`, i.e. it threw the error in
the trash and moved on. **41 times in one day** the system had something
important to tell the operator and it silently failed to send. The operator got
pure silence, which looks exactly like "nothing's wrong."

So the real bug wasn't a missing alert — it was a generated alert dying on the
way out the door, invisibly.

## What already exists

The Telegram adapter already has a method, `ensureLifelineTopic()`, that knows
how to **recreate** a deleted Lifeline topic and remember the new one. The alert
path just never called it — and worse, it hid the failure instead of reacting to
it.

## What's new

One small, shared helper that both alert paths now use. When it tries to send an
alert and the send fails, it:

1. **Logs the real error** instead of swallowing it — so a delivery failure can
   never again be invisible.
2. **Heals the topic** — calls `ensureLifelineTopic()` to recreate the deleted
   topic, then retries the send once.

If the topic is fine, nothing changes — it sends and returns immediately. Only on
a failure does the heal-and-retry kick in.

## The safeguards in plain terms

- **No silent failures, ever.** Every send failure is now logged with the real
  reason, so the next time something can't be delivered, it's diagnosable in
  seconds instead of invisible for hours.
- **Self-healing, not just louder.** A deleted system topic now repairs itself
  and the alert still gets through.
- **Zero change on the happy path.** A working topic behaves exactly as before —
  the heal logic only runs when a send actually fails.
- **Bounded.** It retries once, never loops.

## What you need to decide

Nothing — it ships as a normal patch with safe behavior. This is the foundation
under the bigger piece you asked for (stall alerts that land in the *stalled
session's own topic*, and auto-recovering the session, not just alerting): step
one was making sure these alerts can't silently disappear in the first place.
