instar 1.3.460 → 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/skills/instar-dev/SKILL.md +9 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.461.md +30 -0
- package/upgrades/1.3.462.md +83 -0
- package/upgrades/side-effects/instar-dev-automerge-on-green.md +88 -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
|
@@ -173,11 +173,19 @@ If any check fails, the commit is rejected. This is not a warning — it's a blo
|
|
|
173
173
|
|
|
174
174
|
The pre-push gate (`scripts/pre-push-gate.js`) re-verifies at push time: any release commit whose upgrade notes claim a fix or feature must have a matching artifact in `upgrades/side-effects/`.
|
|
175
175
|
|
|
176
|
+
### Phase 7 — Auto-merge on green (EVERY tier — never pause to ask)
|
|
177
|
+
|
|
178
|
+
When the PR is open and CI goes green, the agent **merges it** — it does NOT pause to ask the operator "ready to merge?" / "want me to merge?". By the time CI is green, the change has already cleared every quality gate this skill enforces: the converged + approved spec (Tier 2) or the staged ELI16 + side-effects artifact (Tier 1), the side-effects review (plus the second-pass review for high-risk changes), the full instar-dev pre-commit + pre-push gates, and the complete CI suite (unit shards + integration + e2e). **Green CI = mergeable. Full stop.** Asking the operator to merge a green PR is redundant ceremony that stalls autonomous delivery — a *Structure-over-Willpower* regression this phase exists to prevent.
|
|
179
|
+
|
|
180
|
+
Perform the merge with `node scripts/safe-merge.mjs <PR#> --squash --admin`. That wrapper re-imposes the requirement `--admin` removes: it waits for every check to finish, REFUSES if any check is red (and specifically confirms an e2e check ran and passed), and only then merges — so the branch-protection safety is preserved even on a behind / hot branch (no separate `update-branch` + full CI re-run needed). After the merge lands, narrate the ship via `POST /telegram/post-update` (the Agent Updates channel) — never a "ready to merge?" question in the working topic.
|
|
181
|
+
|
|
182
|
+
The ONLY thing that stops the merge is a genuinely-red check **on this change**: fix it and re-run. An unrelated environmental flake (a different test failing run-to-run, a tmux/server-boot timeout, a CDN 504) is re-run (`gh run rerun --failed` or a fresh push) — never escalated to the operator as a "should I merge?" question. (Source: operator directive 2026-06-09, topic 23178 — "never pause and ask me to merge; we have enough infra in place to ensure it's good to merge by the time it gets there.")
|
|
183
|
+
|
|
176
184
|
## Tiered development (tier signal → you decide → audited)
|
|
177
185
|
|
|
178
186
|
Not every change is the same size or risk, so not every change pays the same process cost. The commit gate (`scripts/instar-dev-precommit.js`) prints a **tier SIGNAL** — a suggested tier from the change's size (LOC + files) and a **risk floor** raised by any safety-invariant, irreversibility, migration/fleet-rollout, or new-capability signal (`scripts/lib/classify-tier.mjs`). The signal **informs**; it never decides. **You DECLARE the tier** in the trace via `write-trace.mjs --tier <1|2|3> --tier-reasoning "<why>"`. This is the constitution's **The Body and the Mind** made executable (`docs/STANDARDS-REGISTRY.md` → The Substrate): the gate (body) informs, the agent (mind) decides, the decision is audited.
|
|
179
187
|
|
|
180
|
-
- **Tier 1 (small / low-risk):** lighter requirement set — a staged **ELI16** + a staged **side-effects** artifact, no pre-approved converged spec. Declare it with `--tier 1 --eli16-path <path> --side-effects-path <path>` (`--spec` is optional at Tier 1). The PR is the review surface
|
|
188
|
+
- **Tier 1 (small / low-risk):** lighter requirement set — a staged **ELI16** + a staged **side-effects** artifact, no pre-approved converged spec. Declare it with `--tier 1 --eli16-path <path> --side-effects-path <path>` (`--spec` is optional at Tier 1). The PR is the review surface. (Auto-merge-on-green is **not** a Tier-1 privilege — per Phase 7 it applies to EVERY tier; Tier 1 differs only in the lighter pre-merge gate, never in the merge behavior.)
|
|
181
189
|
- **Tier 2+ (everything else):** the full chain above — converged + approved spec, ELI16, side-effects, fresh trace. A **Tier-3 project step** is just a Tier-2 spec; nothing extra is enforced at the gate. **No declared tier → Tier-2** (back-compatible).
|
|
182
190
|
|
|
183
191
|
**The decision is audited.** Every in-scope commit appends one line to `.instar/instar-dev-decisions.jsonl` (signal, declared tier, risk floor + reasons). When you declare **under** the risk-signaled floor, the gate prints a loud `belowFloor` notice and records `belowFloor:true` — it does **not** block (you hold authority), but the override is now a reviewable record. Per **Close the Loop**, those `belowFloor` rates get reviewed on a cadence so the risk-floor list grows.
|
|
@@ -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,30 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Adds **Phase 7 — Auto-merge on green** to the `instar-dev` skill: once an instar-dev PR's CI is fully
|
|
9
|
+
green, the instar-developing agent merges it via the existing `scripts/safe-merge.mjs` wrapper (which
|
|
10
|
+
waits for every check, refuses on any red, and confirms the e2e job ran and passed) and narrates the
|
|
11
|
+
ship via the Agent Updates channel — it no longer pauses to ask the operator "ready to merge?".
|
|
12
|
+
Auto-merge-on-green now applies to **every tier**, not just Tier-1; the Tier-1 wording is corrected so
|
|
13
|
+
it no longer reads as a Tier-1-only privilege. No runtime code changes — the merge mechanism and CI
|
|
14
|
+
gates it relies on are unchanged. Structure-over-Willpower fix for an operator directive (never pause
|
|
15
|
+
to ask for merge; the gates already guarantee mergeability by the time CI is green).
|
|
16
|
+
|
|
17
|
+
## What to Tell Your User
|
|
18
|
+
|
|
19
|
+
None — internal change (no user-facing surface).
|
|
20
|
+
|
|
21
|
+
## Summary of New Capabilities
|
|
22
|
+
|
|
23
|
+
None — internal change (no user-facing surface).
|
|
24
|
+
|
|
25
|
+
## Evidence
|
|
26
|
+
|
|
27
|
+
- `skills/instar-dev/SKILL.md` — new Phase 7 + corrected Tier-1 line.
|
|
28
|
+
- Mechanism unchanged: `scripts/safe-merge.mjs` already re-imposes all-checks-green (incl. e2e) before
|
|
29
|
+
merging. Tier-1 instar-dev gate passed (ELI16 + side-effects + trace); this PR auto-merges on green
|
|
30
|
+
per the very phase it adds.
|
|
@@ -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,88 @@
|
|
|
1
|
+
# Side-Effects Review — instar-dev: auto-merge on green (every tier)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `instar-dev-automerge-on-green`
|
|
4
|
+
**Date:** `2026-06-09`
|
|
5
|
+
**Author:** `Instar Agent (echo)`
|
|
6
|
+
**Second-pass reviewer:** `not required (Tier 1 — documentation/process change to the dev skill; the merge mechanism it mandates (safe-merge.mjs + CI) is unchanged and already gated)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Adds **Phase 7 — Auto-merge on green (every tier)** to `skills/instar-dev/SKILL.md`, and corrects the
|
|
11
|
+
Tier-1 description so auto-merge no longer reads as a Tier-1-only privilege. The new phase mandates that
|
|
12
|
+
once an instar-dev PR's CI is fully green, the agent merges it via the existing
|
|
13
|
+
`scripts/safe-merge.mjs <PR> --squash --admin` wrapper and narrates the ship via `/telegram/post-update`
|
|
14
|
+
— it does NOT pause to ask the operator "ready to merge?". Documentation/process change only; no runtime
|
|
15
|
+
code, no new gate, no new merge mechanism (safe-merge.mjs already exists and re-imposes all-checks-green
|
|
16
|
+
before merging, including an explicit e2e-ran-and-passed check). Driven by the operator directive
|
|
17
|
+
(2026-06-09, topic 23178): "never pause and ask me to merge; we have enough infra in place to ensure
|
|
18
|
+
it's good to merge by the time it gets there. Lets fix this via infrastructure."
|
|
19
|
+
|
|
20
|
+
## Decision-point inventory
|
|
21
|
+
|
|
22
|
+
- No decision point added/changed. This is a process-doc change to the instar-dev skill. The actual
|
|
23
|
+
merge safety lives in `safe-merge.mjs` (waits for all checks, refuses on any red, confirms e2e) + CI
|
|
24
|
+
branch protection — both unchanged.
|
|
25
|
+
|
|
26
|
+
## 1. Over-block
|
|
27
|
+
|
|
28
|
+
No block/allow surface — over-block not applicable.
|
|
29
|
+
|
|
30
|
+
## 2. Under-block
|
|
31
|
+
|
|
32
|
+
No block/allow surface — under-block not applicable. (The only behavior changed is removing a human
|
|
33
|
+
pause AFTER green CI; the green verdict itself — the actual quality gate — is unchanged. `safe-merge.mjs`
|
|
34
|
+
still refuses to merge anything not fully green.)
|
|
35
|
+
|
|
36
|
+
## 3. Level-of-abstraction fit
|
|
37
|
+
|
|
38
|
+
Correct layer. The fix Justin asked for is behavioral: stop pausing to ask for merge. The instar-dev
|
|
39
|
+
skill is exactly where the dev-agent's process is defined, so adding Phase 7 there is the
|
|
40
|
+
right-altitude structural fix (Structure > Willpower — the behavior is now baked into the governing
|
|
41
|
+
process doc, not left to the agent "remembering" not to pause). The merge mechanism it points at
|
|
42
|
+
(`safe-merge.mjs`) already existed; this just makes invoking it the mandated final step for every tier.
|
|
43
|
+
|
|
44
|
+
## 4. Signal vs authority compliance
|
|
45
|
+
|
|
46
|
+
- [x] No — this change has no block/allow surface (documentation/process change).
|
|
47
|
+
|
|
48
|
+
## 5. Interactions
|
|
49
|
+
|
|
50
|
+
- **Shadowing / double-fire:** none. `safe-merge.mjs` is idempotent-safe (it no-ops / refuses if the PR
|
|
51
|
+
isn't green or is already merged).
|
|
52
|
+
- **Races:** none introduced.
|
|
53
|
+
- **Existing Tier-1 auto-merge wording:** the prior line implied auto-merge was a Tier-1 privilege;
|
|
54
|
+
Phase 7 generalizes it and the Tier-1 line is corrected to point at Phase 7, so the two no longer
|
|
55
|
+
conflict.
|
|
56
|
+
|
|
57
|
+
## 6. External surfaces
|
|
58
|
+
|
|
59
|
+
- **Merge behavior:** the agent now auto-merges green instar-dev PRs instead of asking. This is the
|
|
60
|
+
intended change. It is bounded by CI + `safe-merge.mjs` (refuses on any red, confirms e2e ran+passed),
|
|
61
|
+
so an unsafe PR cannot be auto-merged. Ship narration moves to the Agent Updates channel
|
|
62
|
+
(`/telegram/post-update`) rather than a "ready to merge?" question in the working topic.
|
|
63
|
+
- **Migration Parity:** this updates the **instar-dev DEV skill** — used only by the instar-developing
|
|
64
|
+
agent (Echo / a dev-assigned agent), not an end-user feature skill. Per the known skill-install gap
|
|
65
|
+
(the root `skills/` dir, including `instar-dev`, is not synced by `installBuiltinSkills()` and has no
|
|
66
|
+
PostUpdateMigrator path — a separately-tracked issue), the canonical source change here reaches the
|
|
67
|
+
repo; the installed-copy sync for dev agents rides that pre-existing gap. The authoring agent updates
|
|
68
|
+
its own installed `.claude/skills/instar-dev/SKILL.md` directly so the behavior is live immediately.
|
|
69
|
+
A general fix to dev-skill sync is out of scope (it is the standing skill-install-gap project).
|
|
70
|
+
- **No config / CLAUDE.md template / hook / route change.** No persisted state.
|
|
71
|
+
|
|
72
|
+
## 7. Rollback cost
|
|
73
|
+
|
|
74
|
+
Pure documentation revert — revert the PR; the skill returns to the Tier-1-only auto-merge wording. No
|
|
75
|
+
data migration, no runtime impact, no agent-state repair.
|
|
76
|
+
|
|
77
|
+
## Conclusion
|
|
78
|
+
|
|
79
|
+
A focused Structure-over-Willpower fix for the operator's directive: auto-merge-on-green becomes the
|
|
80
|
+
mandated final phase for every instar-dev tier, removing the redundant "ask to merge" pause while
|
|
81
|
+
leaving the actual merge-safety mechanism (`safe-merge.mjs` + CI) untouched. Tier 1 (doc/process change
|
|
82
|
+
to the dev skill); second-pass not required. It will be auto-merged on green per the very phase it adds.
|
|
83
|
+
|
|
84
|
+
## Second-pass review (if required)
|
|
85
|
+
|
|
86
|
+
**Reviewer:** not required — Tier-1 documentation/process change to the dev skill; no block/allow,
|
|
87
|
+
session-lifecycle, sentinel/guard/gate, or recovery-path code is modified. The merge mechanism
|
|
88
|
+
(`safe-merge.mjs`) and CI gates it relies on are unchanged.
|
|
@@ -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.)
|