instar 1.3.461 → 1.3.462
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/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +7 -24
- package/dist/commands/server.js.map +1 -1
- package/dist/monitoring/sentinelConsolidatedSend.d.ts +26 -0
- package/dist/monitoring/sentinelConsolidatedSend.d.ts.map +1 -0
- package/dist/monitoring/sentinelConsolidatedSend.js +80 -0
- package/dist/monitoring/sentinelConsolidatedSend.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.462.md +83 -0
- package/upgrades/side-effects/sentinel-escalation-selfheal.md +65 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Robust consolidated-escalation send for SentinelNotifier.
|
|
3
|
+
*
|
|
4
|
+
* Background (incident 2026-06-09): the sentinel's `sendConsolidated` closures
|
|
5
|
+
* did `try { sendToTopic(lifelineTopicId) } catch { return false }`. The
|
|
6
|
+
* configured lifeline/system topic had been DELETED on the Telegram side, so
|
|
7
|
+
* every send returned `400: message thread not found` — and the bare `catch`
|
|
8
|
+
* black-holed it. 41 stall escalations in one day failed silently; the user got
|
|
9
|
+
* pure silence and could not tell a stalled session from a working one.
|
|
10
|
+
*
|
|
11
|
+
* `TelegramAdapter.ensureLifelineTopic()` already knows how to recreate a
|
|
12
|
+
* deleted lifeline topic and persist the new id — the send path just never
|
|
13
|
+
* invoked it. This helper closes that gap: send to the lifeline topic; on
|
|
14
|
+
* failure, self-heal via ensureLifelineTopic() and retry once; and NEVER
|
|
15
|
+
* swallow the error silently (it is always logged).
|
|
16
|
+
*
|
|
17
|
+
* Pure + adapter-injected so it is unit-testable without a live Telegram.
|
|
18
|
+
*/
|
|
19
|
+
export interface ConsolidatedSendTelegram {
|
|
20
|
+
getLifelineTopicId(): number | undefined | null;
|
|
21
|
+
sendToTopic(topicId: number, text: string): Promise<unknown>;
|
|
22
|
+
/** Recreates the lifeline/system topic if deleted, persists the new id, returns it (or null). */
|
|
23
|
+
ensureLifelineTopic(): Promise<number | null>;
|
|
24
|
+
}
|
|
25
|
+
export declare function sendConsolidatedWithSelfHeal(tg: ConsolidatedSendTelegram, text: string, log: (line: string) => void): Promise<boolean>;
|
|
26
|
+
//# sourceMappingURL=sentinelConsolidatedSend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentinelConsolidatedSend.d.ts","sourceRoot":"","sources":["../../src/monitoring/sentinelConsolidatedSend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,wBAAwB;IACvC,kBAAkB,IAAI,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IAChD,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7D,iGAAiG;IACjG,mBAAmB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC/C;AAkBD,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,wBAAwB,EAC5B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC1B,OAAO,CAAC,OAAO,CAAC,CA6ClB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Robust consolidated-escalation send for SentinelNotifier.
|
|
3
|
+
*
|
|
4
|
+
* Background (incident 2026-06-09): the sentinel's `sendConsolidated` closures
|
|
5
|
+
* did `try { sendToTopic(lifelineTopicId) } catch { return false }`. The
|
|
6
|
+
* configured lifeline/system topic had been DELETED on the Telegram side, so
|
|
7
|
+
* every send returned `400: message thread not found` — and the bare `catch`
|
|
8
|
+
* black-holed it. 41 stall escalations in one day failed silently; the user got
|
|
9
|
+
* pure silence and could not tell a stalled session from a working one.
|
|
10
|
+
*
|
|
11
|
+
* `TelegramAdapter.ensureLifelineTopic()` already knows how to recreate a
|
|
12
|
+
* deleted lifeline topic and persist the new id — the send path just never
|
|
13
|
+
* invoked it. This helper closes that gap: send to the lifeline topic; on
|
|
14
|
+
* failure, self-heal via ensureLifelineTopic() and retry once; and NEVER
|
|
15
|
+
* swallow the error silently (it is always logged).
|
|
16
|
+
*
|
|
17
|
+
* Pure + adapter-injected so it is unit-testable without a live Telegram.
|
|
18
|
+
*/
|
|
19
|
+
function errMsg(err) {
|
|
20
|
+
return err instanceof Error ? err.message : String(err);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* True when the send error means the target topic is GONE (deleted/closed) — the
|
|
24
|
+
* only case where the message definitely did NOT land, so recreating + retrying
|
|
25
|
+
* is safe. We deliberately do NOT retry other (transient/network) errors: those
|
|
26
|
+
* may have landed at Telegram before the response was lost, and a blind retry
|
|
27
|
+
* would double-post the escalation (this path bypasses the /telegram/reply
|
|
28
|
+
* content-dedup). A transient failure is recovered by the sentinel's next sweep.
|
|
29
|
+
*/
|
|
30
|
+
function isTopicGone(msg) {
|
|
31
|
+
return /message thread not found|thread not found|topic[ _-]?deleted|topic[ _-]?closed|chat not found/i.test(msg);
|
|
32
|
+
}
|
|
33
|
+
export async function sendConsolidatedWithSelfHeal(tg, text, log) {
|
|
34
|
+
const topicId = tg.getLifelineTopicId();
|
|
35
|
+
// Fast path: send to the currently-configured lifeline topic.
|
|
36
|
+
if (topicId) {
|
|
37
|
+
try {
|
|
38
|
+
await tg.sendToTopic(topicId, text);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
// De-swallow (the original `catch { return false }` black-holed this).
|
|
43
|
+
const msg = errMsg(err);
|
|
44
|
+
if (!isTopicGone(msg)) {
|
|
45
|
+
// Not a deleted-topic error — could be transient and may have partially
|
|
46
|
+
// landed. Do NOT retry (avoid a duplicate escalation); the sentinel
|
|
47
|
+
// re-escalates on its next sweep if the condition persists.
|
|
48
|
+
log(`escalation send to lifeline topic ${topicId} failed (${msg}) — transient/non-topic-gone, not retrying`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
log(`escalation send to lifeline topic ${topicId} failed (${msg}: topic gone) — self-healing`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
log('escalation has no lifeline topic configured — establishing one');
|
|
56
|
+
}
|
|
57
|
+
// Self-heal: (re)establish the lifeline/system topic (recreates if deleted,
|
|
58
|
+
// persists the new id) and retry the send once.
|
|
59
|
+
let healed;
|
|
60
|
+
try {
|
|
61
|
+
healed = await tg.ensureLifelineTopic();
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
log(`escalation self-heal (ensureLifelineTopic) threw (${errMsg(err)}) — alert NOT delivered`);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (!healed) {
|
|
68
|
+
log('escalation self-heal could not establish a lifeline topic — alert NOT delivered');
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
await tg.sendToTopic(healed, text);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
log(`escalation retry to healed lifeline topic ${healed} failed (${errMsg(err)}) — alert NOT delivered`);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=sentinelConsolidatedSend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sentinelConsolidatedSend.js","sourceRoot":"","sources":["../../src/monitoring/sentinelConsolidatedSend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AASH,SAAS,MAAM,CAAC,GAAY;IAC1B,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,WAAW,CAAC,GAAW;IAC9B,OAAO,gGAAgG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACpH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,EAA4B,EAC5B,IAAY,EACZ,GAA2B;IAE3B,MAAM,OAAO,GAAG,EAAE,CAAC,kBAAkB,EAAE,CAAC;IAExC,8DAA8D;IAC9D,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YACpC,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,uEAAuE;YACvE,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YACxB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;gBACtB,wEAAwE;gBACxE,oEAAoE;gBACpE,4DAA4D;gBAC5D,GAAG,CAAC,qCAAqC,OAAO,YAAY,GAAG,4CAA4C,CAAC,CAAC;gBAC7G,OAAO,KAAK,CAAC;YACf,CAAC;YACD,GAAG,CAAC,qCAAqC,OAAO,YAAY,GAAG,8BAA8B,CAAC,CAAC;QACjG,CAAC;IACH,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,gEAAgE,CAAC,CAAC;IACxE,CAAC;IAED,4EAA4E;IAC5E,gDAAgD;IAChD,IAAI,MAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,EAAE,CAAC,mBAAmB,EAAE,CAAC;IAC1C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,qDAAqD,MAAM,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QAC/F,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,GAAG,CAAC,iFAAiF,CAAC,CAAC;QACvF,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,6CAA6C,MAAM,YAAY,MAAM,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QACzG,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-10T03:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-10T03:41:06.411Z",
|
|
5
|
+
"instarVersion": "1.3.462",
|
|
6
6
|
"entryCount": 199,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
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`).
|
|
9
|
+
|
|
10
|
+
## What to Tell Your User
|
|
11
|
+
|
|
12
|
+
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.)
|
|
13
|
+
|
|
14
|
+
## Summary of New Capabilities
|
|
15
|
+
|
|
16
|
+
| Capability | How to Use |
|
|
17
|
+
|-----------|-----------|
|
|
18
|
+
| Sentinel/stop escalations self-heal a deleted lifeline topic + never swallow the send error | automatic — no config |
|
|
19
|
+
|
|
20
|
+
## Evidence
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
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.)
|
|
25
|
+
|
|
26
|
+
## The problem
|
|
27
|
+
|
|
28
|
+
The operator couldn't tell whether a session had stalled or was just working —
|
|
29
|
+
because for almost an hour it heard nothing. Here's what was actually happening
|
|
30
|
+
under the hood, and it's worse than "the agent didn't notice."
|
|
31
|
+
|
|
32
|
+
The agent's stall-detector **did** notice. It saw a session go quiet, gave it a
|
|
33
|
+
nudge, and tried to send the operator a message: "this session went quiet ~16
|
|
34
|
+
minutes ago, want me to dig in?" That message goes to a dedicated "Lifeline"
|
|
35
|
+
system topic in Telegram.
|
|
36
|
+
|
|
37
|
+
But that Lifeline topic had been **deleted** on the Telegram side, while the
|
|
38
|
+
config still pointed at it. So every send came back with `message thread not
|
|
39
|
+
found` — and the code did `catch { return false }`, i.e. it threw the error in
|
|
40
|
+
the trash and moved on. **41 times in one day** the system had something
|
|
41
|
+
important to tell the operator and it silently failed to send. The operator got
|
|
42
|
+
pure silence, which looks exactly like "nothing's wrong."
|
|
43
|
+
|
|
44
|
+
So the real bug wasn't a missing alert — it was a generated alert dying on the
|
|
45
|
+
way out the door, invisibly.
|
|
46
|
+
|
|
47
|
+
## What already exists
|
|
48
|
+
|
|
49
|
+
The Telegram adapter already has a method, `ensureLifelineTopic()`, that knows
|
|
50
|
+
how to **recreate** a deleted Lifeline topic and remember the new one. The alert
|
|
51
|
+
path just never called it — and worse, it hid the failure instead of reacting to
|
|
52
|
+
it.
|
|
53
|
+
|
|
54
|
+
## What's new
|
|
55
|
+
|
|
56
|
+
One small, shared helper that both alert paths now use. When it tries to send an
|
|
57
|
+
alert and the send fails, it:
|
|
58
|
+
|
|
59
|
+
1. **Logs the real error** instead of swallowing it — so a delivery failure can
|
|
60
|
+
never again be invisible.
|
|
61
|
+
2. **Heals the topic** — calls `ensureLifelineTopic()` to recreate the deleted
|
|
62
|
+
topic, then retries the send once.
|
|
63
|
+
|
|
64
|
+
If the topic is fine, nothing changes — it sends and returns immediately. Only on
|
|
65
|
+
a failure does the heal-and-retry kick in.
|
|
66
|
+
|
|
67
|
+
## The safeguards in plain terms
|
|
68
|
+
|
|
69
|
+
- **No silent failures, ever.** Every send failure is now logged with the real
|
|
70
|
+
reason, so the next time something can't be delivered, it's diagnosable in
|
|
71
|
+
seconds instead of invisible for hours.
|
|
72
|
+
- **Self-healing, not just louder.** A deleted system topic now repairs itself
|
|
73
|
+
and the alert still gets through.
|
|
74
|
+
- **Zero change on the happy path.** A working topic behaves exactly as before —
|
|
75
|
+
the heal logic only runs when a send actually fails.
|
|
76
|
+
- **Bounded.** It retries once, never loops.
|
|
77
|
+
|
|
78
|
+
## What you need to decide
|
|
79
|
+
|
|
80
|
+
Nothing — it ships as a normal patch with safe behavior. This is the foundation
|
|
81
|
+
under the bigger piece you asked for (stall alerts that land in the *stalled
|
|
82
|
+
session's own topic*, and auto-recovering the session, not just alerting): step
|
|
83
|
+
one was making sure these alerts can't silently disappear in the first place.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Side-Effects Review — sentinel escalation self-heal (no silent swallow)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `sentinel-escalation-selfheal`
|
|
4
|
+
**Date:** `2026-06-09`
|
|
5
|
+
**Author:** `Instar Agent (echo)`
|
|
6
|
+
**Second-pass reviewer:** `independent reviewer subagent — concern raised, addressed in code`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Both `sendConsolidated` closures in `server.ts` (sentinel-notify ~6988, stop-notify ~9689) used `try { telegram.sendToTopic(lifelineTopicId) } catch { return false }`. A deleted lifeline/system topic made every send throw `message thread not found`, swallowed silently — 41 stall/stop escalations black-holed in one day. New helper `src/monitoring/sentinelConsolidatedSend.ts#sendConsolidatedWithSelfHeal(tg, text, log)`: send to the lifeline topic; on failure, log the real error and call the adapter's existing `ensureLifelineTopic()` (recreates a deleted topic + persists the new id), then retry once. Both sites now delegate to it. Files: `src/monitoring/sentinelConsolidatedSend.ts` (new), `src/commands/server.ts` (import + 2 call sites), `tests/unit/sentinel-consolidated-send.test.ts` (new).
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- Sentinel/stop escalation DELIVERY (not a block/allow gate) — **modify** — adds a self-heal + retry + de-swallow around the existing send. No new gating decision; it only changes how a notification is delivered and whether failures are visible.
|
|
15
|
+
|
|
16
|
+
## 1. Over-block
|
|
17
|
+
|
|
18
|
+
No block/allow surface — this is a delivery path, not a gate. It cannot reject any input. (Over-block N/A.)
|
|
19
|
+
|
|
20
|
+
## 2. Under-block
|
|
21
|
+
|
|
22
|
+
N/A (no gate). Residual delivery risk: if `ensureLifelineTopic()` can't create a topic (non-forum chat, Telegram down), the alert still isn't delivered — but now it returns false WITH a logged reason (previously silent). The SentinelNotifier's own escalation-state handling of a false return is unchanged.
|
|
23
|
+
|
|
24
|
+
## 3. Level-of-abstraction fit
|
|
25
|
+
|
|
26
|
+
Correct layer. The fix lives at the escalation-delivery callback where the swallow was, and reuses the adapter's existing `ensureLifelineTopic()` (the right owner of topic lifecycle) rather than re-implementing topic creation. Extracted to a small injected-dependency module so it's unit-testable without a live Telegram (the inline closures were untestable, which is why the swallow shipped).
|
|
27
|
+
|
|
28
|
+
## 4. Signal vs authority compliance
|
|
29
|
+
|
|
30
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
31
|
+
|
|
32
|
+
- [x] No — this has no block/allow surface; it is a delivery mechanism. It adds no authority; it makes an existing best-effort send self-heal and stop hiding failures.
|
|
33
|
+
|
|
34
|
+
## 5. Interactions
|
|
35
|
+
|
|
36
|
+
- **Shadowing / double-fire:** retries the send at most once, and ONLY when the first error matches a topic-gone signature (`message thread not found` / topic deleted/closed / chat not found) — the only case where the message definitely did NOT land. A transient/other error (e.g. 429, network blip that may have landed at Telegram before the response was lost) is NOT retried — it logs and returns false, and the sentinel re-escalates on its next sweep. This eliminates the double-post window: a blind retry would NOT be caught by the `/telegram/reply` ~15-min content-dedup, because this helper calls `sendToTopic` directly (bypassing that route). No loop.
|
|
37
|
+
- **ensureLifelineTopic side effect:** it creates a Telegram topic + persists `lifelineTopicId` (via `persistLifelineTopicId`). This is the SAME self-heal it already performs on startup; invoking it on send-failure is the intended use ("called on startup and can be called periodically"). It runs only on a failure path (rare), so no topic-creation spam.
|
|
38
|
+
- **Two call sites, one helper:** both sentinel-notify and stop-notify now share identical, tested behavior (previously duplicated inline). No behavior divergence.
|
|
39
|
+
- **Races:** the helper is stateless; `ensureLifelineTopic` owns its own concurrency. No new shared state.
|
|
40
|
+
|
|
41
|
+
## 6. External surfaces
|
|
42
|
+
|
|
43
|
+
- **Users:** ships to the install base via normal release. Visible effect: system escalations (session-quiet, session-stopped) now actually arrive even if the Lifeline topic was deleted, and never silently fail. A recreated Lifeline topic appears once if the old one was deleted (expected, one-time).
|
|
44
|
+
- **Persistent state:** `ensureLifelineTopic` persists the new `lifelineTopicId` — same as its existing startup behavior. No new state shape.
|
|
45
|
+
- **Telegram:** one extra API call (`ensureLifelineTopic`) only on a send failure.
|
|
46
|
+
|
|
47
|
+
## 7. Rollback cost
|
|
48
|
+
|
|
49
|
+
Pure code change (new module + 2 call-site swaps), no migration. Back-out = revert; behavior returns to the prior silent-swallow. Low. (The original incident — a dead topic 2 — self-heals on the first escalation after deploy; no manual topic surgery needed.)
|
|
50
|
+
|
|
51
|
+
## Conclusion
|
|
52
|
+
|
|
53
|
+
A focused robustness fix: it converts a 100%-silent escalation-delivery failure (dead lifeline topic, error swallowed) into self-heal-and-retry with full logging, reusing the adapter's existing topic-recreation. No gating surface, signal-vs-authority compliant, no migration. It is the foundation for the follow-on work the operator requested (route stall alerts to the stalled session's own topic + confidence-gated auto-recovery of the session) — tracked separately. Because it touches the messaging-escalation delivery path, a Phase-5 second-pass review is requested.
|
|
54
|
+
|
|
55
|
+
## Second-pass review (if required)
|
|
56
|
+
|
|
57
|
+
**Reviewer:** independent reviewer subagent (general-purpose)
|
|
58
|
+
**Independent read of the artifact: concern raised → addressed in code**
|
|
59
|
+
|
|
60
|
+
The reviewer confirmed the self-heal flow is correct on all 7 paths (happy path doesn't call ensureLifelineTopic; dead-topic heals+retries on the new id; no-topic-configured establishes one; all three failure modes return false + log, never throw to the caller; bounded single retry; both call sites converted with the old swallow gone; tsc-clean type compatibility; ensureLifelineTopic only on failure). It raised ONE valid concern: the original artifact claimed the ~15-min `/telegram/reply` content-dedup covered the partial-send-then-throw duplicate window, but this helper calls `sendToTopic` directly and bypasses that dedup — so a blind retry on a transient (non-topic-gone) error that had actually landed could double-post one escalation. **Addressed in code** (the reviewer's recommended belt-and-suspenders): the retry is now gated on an `isTopicGone(err)` signature check — only deleted/closed-topic errors (where the message definitely didn't land) self-heal + retry; transient errors log + return false (no retry), recovered by the next sentinel sweep. Eliminates the duplicate window entirely. New test case `transient (non-topic-gone) send error → does NOT retry` pins it (exactly one send attempt, ensureLifelineTopic not called). 7/7 helper tests green; tsc clean.
|
|
61
|
+
|
|
62
|
+
## Evidence pointers
|
|
63
|
+
|
|
64
|
+
- Live: `logs/sentinel-events.jsonl` — 41 × `notify-error: sendConsolidated returned false` paired with `active-silence`/`stop` escalations on 2026-06-09; `POST /telegram/reply/2` → `400: message thread not found`; config `lifelineTopicId: 2` (deleted on Telegram).
|
|
65
|
+
- Tests: `tests/unit/sentinel-consolidated-send.test.ts` (6 cases, both sides incl. all failure modes). tsc + lint clean. (15 unrelated A2A/SessionActivity sentinel test failures are pre-existing on main, verified by stash-compare.)
|