@teamscala/service-boot 0.23.430 → 0.23.431

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/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.430",
9
+ "version": "0.23.431",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -72,6 +72,12 @@ export async function deliverEscalation(
72
72
  last_seen_at: aggregate.lastSeen.toISOString(),
73
73
  event_count: aggregate.count,
74
74
  resolved_at: null,
75
+ // CLEARING THIS IS WHAT STOPS AN ESCALATION STORM. A re-armed row
76
+ // keeps resolved_at NULL, so it still loads as open, and
77
+ // `escalate` fires for any episode that is resolved OR re-armed.
78
+ // Leaving rearmed_at set would therefore re-escalate the same kind
79
+ // on EVERY sweep — the firing state must be consumed by the firing.
80
+ rearmed_at: null,
75
81
  },
76
82
  });
77
83
 
@@ -165,6 +171,36 @@ export async function deliverEscalation(
165
171
  return { messageId, workItemId: config.orchestrator_work_item_id };
166
172
  }
167
173
 
174
+ /**
175
+ * Re-arm an episode whose kind went quiet but whose condition NOBODY CHECKED.
176
+ * The alarm becomes audible again and no claim is made that the fault
177
+ * stopped, so there is deliberately NO operator comment: "Fault stopped" on
178
+ * the strength of silence is the sentence this change exists to stop writing.
179
+ * The event is still recorded — re-arming is a state change. See migration 811.
180
+ */
181
+ export async function rearmEpisode(
182
+ episode: EscalationEpisode,
183
+ quietMinutes: number,
184
+ ): Promise<void> {
185
+ const now = new Date();
186
+ await (await orpc()).platform_fault_escalations.update({
187
+ where: { kind: episode.kind },
188
+ data: { rearmed_at: now.toISOString(), updated_at: now.toISOString() },
189
+ });
190
+ recordEvent({
191
+ kind: "platform.fault.rearmed",
192
+ payload: {
193
+ event_kind: episode.kind,
194
+ quiet_minutes: Math.round(quietMinutes),
195
+ work_item_id: episode.workItemId,
196
+ // Named so a reader never mistakes this for a resolution: no probe
197
+ // exists for this kind, so nothing verified the condition ended.
198
+ verified: false,
199
+ reason: "no capability probe declared for this kind",
200
+ },
201
+ });
202
+ }
203
+
168
204
  export async function resolveEpisode(
169
205
  episode: EscalationEpisode,
170
206
  quietMinutes: number,
@@ -26,7 +26,13 @@ export interface EscalationEpisode {
26
26
  eventCount: number;
27
27
  escalatedAt: Date | null;
28
28
  workItemId: string | null;
29
+ /** VERIFIED over — a capability probe answered OK, or a declared recovery
30
+ * transition arrived. Never written from silence. */
29
31
  resolvedAt: Date | null;
32
+ /** The kind went quiet and the alarm is ARMED AGAIN. This is all that
33
+ * quiet ever honestly supported, and it is deliberately weaker than
34
+ * `resolvedAt`: it claims nothing about the condition. */
35
+ rearmedAt: Date | null;
30
36
  lastUpdatedAt: Date | null;
31
37
  deliveredCommentId?: string | null;
32
38
  }
@@ -51,8 +57,18 @@ export interface KindThresholdOverride {
51
57
  export interface SweepDecision {
52
58
  /** Sustained faults needing a FIRST delivery (new episode, or a claim whose send failed). */
53
59
  escalate: Array<{ aggregate: FaultAggregate; episode: EscalationEpisode | null }>;
54
- /** Open episodes whose kind went quiet past the resolve window — close them. */
60
+ /** Open episodes whose kind went quiet past the resolve window — close them.
61
+ * ONLY reachable from the probe gate: quiet alone never populates this. */
55
62
  resolve: Array<{ episode: EscalationEpisode; quietMinutes: number; clearZeroVolumeFlag: boolean }>;
63
+ /** Open episodes quiet past the resolve window whose kind declares NO
64
+ * capability probe: the alarm is RE-ARMED (the next occurrence escalates
65
+ * again) and nothing is claimed about the condition. Quiet used to write
66
+ * `resolved_at` and post "Fault stopped", which is the unsound close
67
+ * `emit-transitions-not-conditions` names. It is re-armed rather than
68
+ * merely held open because `escalate` fires only for an absent, resolved
69
+ * or re-armed episode — holding it open would silence the alarm for good.
70
+ * Rationale + measurements: migration 811. */
71
+ rearm: Array<{ episode: EscalationEpisode; quietMinutes: number; clearZeroVolumeFlag: boolean }>;
56
72
  /** Open episodes quiet past the resolve window whose producer ALSO went
57
73
  * silent: zero failure rows AND zero call volume that would produce them.
58
74
  * Quiet is NOT evidence of health here (measured 2026-08-27: llm-router
@@ -150,14 +166,17 @@ export function decideSweep(
150
166
  const byKind = new Map(episodes.map((e) => [e.kind, e]));
151
167
  const aggregateByKind = new Map(aggregates.map((a) => [a.kind, a]));
152
168
  const decision: SweepDecision =
153
- { escalate: [], resolve: [], resolveProbeGate: [], zeroVolumeHold: [] };
169
+ { escalate: [], resolve: [], rearm: [], resolveProbeGate: [], zeroVolumeHold: [] };
154
170
 
155
171
  for (const aggregate of aggregates) {
156
172
  const episode = byKind.get(aggregate.kind);
157
173
  if (!isSustained(aggregate, policy, kindThresholds[aggregate.kind], now)) continue;
158
174
  // No episode, or the previous episode closed (quiet past resolve window):
159
175
  // a NEW episode begins and must be delivered.
160
- if (!episode || episode.resolvedAt) {
176
+ // A RE-ARMED episode starts a new one exactly as a resolved one does.
177
+ // This is the half that keeps the alarm audible: without it, dropping
178
+ // the quiet-close would leave every probe-less kind permanently silent.
179
+ if (!episode || episode.resolvedAt || episode.rearmedAt) {
161
180
  decision.escalate.push({ aggregate, episode: episode ?? null });
162
181
  continue;
163
182
  }
@@ -174,7 +193,7 @@ export function decideSweep(
174
193
  }
175
194
 
176
195
  for (const episode of episodes) {
177
- if (episode.resolvedAt) continue;
196
+ if (episode.resolvedAt || episode.rearmedAt) continue;
178
197
  const aggregate = aggregateByKind.get(episode.kind);
179
198
  // A future-dated aggregate lastSeen (producer clock skew) cannot serve
180
199
  // as the quiet reference — it would freeze quietMinutes negative and
@@ -218,7 +237,9 @@ export function decideSweep(
218
237
  });
219
238
  continue;
220
239
  }
221
- decision.resolve.push({
240
+ // NO PROBE DECLARED: re-arm, never resolve. Quiet re-arms the alarm and
241
+ // says nothing about the condition — see the `rearm` field's comment.
242
+ decision.rearm.push({
222
243
  episode,
223
244
  quietMinutes,
224
245
  clearZeroVolumeFlag: Boolean(source && zeroVolume?.flagActiveByKind[episode.kind]),
@@ -44,6 +44,7 @@ function episode(overrides: Partial<EscalationEpisode> & { kind: string }): Esca
44
44
  escalatedAt: new Date(NOW.getTime() - 60 * 60_000),
45
45
  workItemId: "wi-1",
46
46
  resolvedAt: null,
47
+ rearmedAt: null,
47
48
  lastUpdatedAt: null,
48
49
  ...overrides,
49
50
  };
@@ -117,6 +118,7 @@ describe("decideSweep — dedup (one escalation per fault, no re-fire)", () => {
117
118
  expect(decision.resolve).toHaveLength(0);
118
119
  expect(Object.keys(decision).sort()).toEqual([
119
120
  "escalate",
121
+ "rearm",
120
122
  "resolve",
121
123
  "resolveProbeGate",
122
124
  "zeroVolumeHold",
@@ -133,17 +135,60 @@ describe("decideSweep — dedup (one escalation per fault, no re-fire)", () => {
133
135
  });
134
136
 
135
137
  describe("decideSweep — resolution + re-arm", () => {
136
- test("quiet past the resolve window closes the episode", () => {
138
+ // QUIET RE-ARMS, IT DOES NOT RESOLVE. These two asserted `resolve` until
139
+ // 2026-08-29, which is the unsound close: only a probe can say a condition
140
+ // ended, and no probe is declared for these kinds.
141
+ test("quiet past the resolve window RE-ARMS the episode, and claims nothing", () => {
137
142
  const gone = episode({ kind: "wake.failed", lastSeenAt: new Date(NOW.getTime() - 60 * 60_000) });
138
143
  const decision = decideSweep([], [gone], POLICY, NOW);
139
- expect(decision.resolve).toHaveLength(1);
140
- expect(decision.resolve[0]!.quietMinutes).toBeGreaterThanOrEqual(30);
144
+ expect(decision.rearm).toHaveLength(1);
145
+ expect(decision.rearm[0]!.quietMinutes).toBeGreaterThanOrEqual(30);
146
+ expect(decision.resolve).toHaveLength(0);
141
147
  });
142
148
 
143
- test("an absent kind resolves by its episode's last_seen, not the sweep clock", () => {
149
+ test("an absent kind re-arms by its episode's last_seen, not the sweep clock", () => {
144
150
  const gone = episode({ kind: "listen.connect_failed", lastSeenAt: new Date(NOW.getTime() - 45 * 60_000) });
145
151
  const decision = decideSweep([], [gone], POLICY, NOW);
146
- expect(decision.resolve).toHaveLength(1);
152
+ expect(decision.rearm).toHaveLength(1);
153
+ expect(decision.resolve).toHaveLength(0);
154
+ });
155
+
156
+ // THE INVARIANT THAT MAKES RE-ARMING SAFE. If a re-armed episode did not
157
+ // escalate again, dropping the quiet-close would have left every
158
+ // probe-less alarm permanently silent — worse than the false close.
159
+ test("a RE-ARMED episode escalates again when its fault recurs", () => {
160
+ const recurring = agg("guard.fail.open", 50, 40, 0);
161
+ const rearmed = episode({
162
+ kind: "guard.fail.open",
163
+ rearmedAt: new Date(NOW.getTime() - 35 * 60_000),
164
+ });
165
+ const decision = decideSweep([recurring], [rearmed], POLICY, NOW);
166
+ expect(decision.escalate).toHaveLength(1);
167
+ });
168
+
169
+ // A re-armed episode is already armed; re-arming it every sweep would emit
170
+ // a transition per sweep, which is the repeating-condition shape itself.
171
+ test("an already re-armed episode is not re-armed again while it stays quiet", () => {
172
+ const rearmed = episode({
173
+ kind: "guard.fail.open",
174
+ lastSeenAt: new Date(NOW.getTime() - 60 * 60_000),
175
+ rearmedAt: new Date(NOW.getTime() - 35 * 60_000),
176
+ });
177
+ const decision = decideSweep([], [rearmed], POLICY, NOW);
178
+ expect(decision.rearm).toHaveLength(0);
179
+ expect(decision.resolve).toHaveLength(0);
180
+ });
181
+
182
+ // A kind WITH a probe keeps the stronger path: it is never re-armed on
183
+ // quiet, because something can actually be asked about it.
184
+ test("a kind declaring a capability probe goes to the probe gate, never to re-arm", () => {
185
+ const gone = episode({ kind: "llm.provider.call.failed", lastSeenAt: new Date(NOW.getTime() - 60 * 60_000) });
186
+ const decision = decideSweep([], [gone], POLICY, NOW, {}, undefined, {
187
+ "llm.provider.call.failed": { url: "http://127.0.0.1:9/health" },
188
+ });
189
+ expect(decision.resolveProbeGate).toHaveLength(1);
190
+ expect(decision.rearm).toHaveLength(0);
191
+ expect(decision.resolve).toHaveLength(0);
147
192
  });
148
193
 
149
194
  test("after resolution the same fault escalates again (new episode)", () => {
@@ -161,6 +206,7 @@ describe("decideSweep — resolution + re-arm", () => {
161
206
  const open = episode({ kind: "slow.error", lastSeenAt: active.lastSeen });
162
207
  const decision = decideSweep([active], [open], POLICY, NOW);
163
208
  expect(decision.resolve).toHaveLength(0);
209
+ expect(decision.rearm).toHaveLength(0);
164
210
  });
165
211
  });
166
212
 
@@ -188,7 +234,9 @@ describe("decideSweep — a future-dated lastSeen is not recurrence evidence", (
188
234
  const phantom = agg("guard.fail.open", 40, 720, -44);
189
235
  const decision = decideSweep([phantom], [quietEpisode], POLICY, NOW);
190
236
  expect(decision.escalate).toHaveLength(0);
191
- expect(decision.resolve).toHaveLength(1);
237
+ // Re-armed rather than resolved: no probe is declared for this kind.
238
+ expect(decision.rearm).toHaveLength(1);
239
+ expect(decision.resolve).toHaveLength(0);
192
240
  });
193
241
  });
194
242
 
@@ -354,19 +402,21 @@ describe("decideSweep — zero-volume gate (quiet vs dead producer)", () => {
354
402
  expect(decision.zeroVolumeHold[0].volumeCount).toBe(0);
355
403
  });
356
404
 
357
- test("quiet WITH healthy call volume still resolves (the normal close)", () => {
405
+ test("quiet WITH healthy call volume re-arms (the normal path for a probe-less kind)", () => {
358
406
  const episodes = [episode({ kind: "llm.provider.call.failed", lastSeenAt: new Date(NOW.getTime() - 45 * 60_000) })];
359
407
  const inputs = { ...VOLUME_BACKED, countByKind: { "llm.provider.call.failed": 120 } };
360
408
  const decision = decideSweep([], episodes, POLICY, NOW, {}, inputs);
361
409
  expect(decision.zeroVolumeHold).toHaveLength(0);
362
- expect(decision.resolve).toHaveLength(1);
363
- expect(decision.resolve[0].clearZeroVolumeFlag).toBe(false);
410
+ expect(decision.rearm).toHaveLength(1);
411
+ expect(decision.resolve).toHaveLength(0);
412
+ expect(decision.rearm[0]!.clearZeroVolumeFlag).toBe(false);
364
413
  });
365
414
 
366
- test("a sourceless kind resolves on quiet exactly as before", () => {
415
+ test("a sourceless kind re-arms on quiet", () => {
367
416
  const episodes = [episode({ kind: "guard.fail.open", lastSeenAt: new Date(NOW.getTime() - 45 * 60_000) })];
368
417
  const decision = decideSweep([], episodes, POLICY, NOW, {}, { ...VOLUME_BACKED, countByKind: {} });
369
- expect(decision.resolve).toHaveLength(1);
418
+ expect(decision.rearm).toHaveLength(1);
419
+ expect(decision.resolve).toHaveLength(0);
370
420
  expect(decision.zeroVolumeHold).toHaveLength(0);
371
421
  });
372
422
 
@@ -378,13 +428,14 @@ describe("decideSweep — zero-volume gate (quiet vs dead producer)", () => {
378
428
  flagActiveByKind: { "llm.provider.call.failed": true },
379
429
  };
380
430
  const decision = decideSweep([], episodes, POLICY, NOW, {}, inputs);
381
- expect(decision.resolve[0].clearZeroVolumeFlag).toBe(true);
431
+ expect(decision.rearm[0]!.clearZeroVolumeFlag).toBe(true);
382
432
  });
383
433
 
384
434
  test("without a sixth argument the legacy shape is untouched", () => {
385
435
  const episodes = [episode({ kind: "llm.provider.call.failed", lastSeenAt: new Date(NOW.getTime() - 45 * 60_000) })];
386
436
  const decision = decideSweep([], episodes, POLICY, NOW);
387
- expect(decision.resolve).toHaveLength(1);
437
+ expect(decision.rearm).toHaveLength(1);
438
+ expect(decision.resolve).toHaveLength(0);
388
439
  });
389
440
  });
390
441
 
@@ -422,10 +473,11 @@ describe("decideSweep — probe-gated resolution (operator ruling 2026-08-28)",
422
473
  expect(decision.resolveProbeGate[0]!.quietMinutes).toBeGreaterThanOrEqual(30);
423
474
  });
424
475
 
425
- test("a kind without a declared probe resolves on quiet exactly as before", () => {
476
+ test("a kind without a declared probe RE-ARMS on quiet — it is never closed unverified", () => {
426
477
  const gone = episode({ kind: "wake.failed", lastSeenAt: new Date(NOW.getTime() - 60 * 60_000) });
427
478
  const decision = decideSweep([agg("wake.failed", 10, 700, 60)], [gone], POLICY, NOW, {}, undefined, PROBE_SOURCES);
428
- expect(decision.resolve).toHaveLength(1);
479
+ expect(decision.rearm).toHaveLength(1);
480
+ expect(decision.resolve).toHaveLength(0);
429
481
  expect(decision.resolveProbeGate).toHaveLength(0);
430
482
  });
431
483
 
@@ -39,6 +39,7 @@ import {
39
39
  deliverEscalation,
40
40
  deliverZeroVolumeHold,
41
41
  refreshOpenEpisode,
42
+ rearmEpisode,
42
43
  resolveEpisode,
43
44
  } from "./platform-fault-escalation/delivery.ts";
44
45
 
@@ -61,6 +62,7 @@ async function platformFaultEscalationSweep(ctx: { config: ScheduledJobConfig })
61
62
  escalated_at: string | Date | null;
62
63
  work_item_id: string | null;
63
64
  resolved_at: string | Date | null;
65
+ rearmed_at: string | Date | null;
64
66
  last_updated_at: string | Date | null;
65
67
  }>;
66
68
  const episodes: EscalationEpisode[] = openEpisodes.map((row) => ({
@@ -71,6 +73,7 @@ async function platformFaultEscalationSweep(ctx: { config: ScheduledJobConfig })
71
73
  escalatedAt: row.escalated_at ? new Date(row.escalated_at) : null,
72
74
  workItemId: row.work_item_id,
73
75
  resolvedAt: row.resolved_at ? new Date(row.resolved_at) : null,
76
+ rearmedAt: row.rearmed_at ? new Date(row.rearmed_at) : null,
74
77
  lastUpdatedAt: row.last_updated_at ? new Date(row.last_updated_at) : null,
75
78
  }));
76
79
 
@@ -204,6 +207,19 @@ async function platformFaultEscalationSweep(ctx: { config: ScheduledJobConfig })
204
207
  log.error(`[platform-fault-escalation] resolve for '${episode.kind}' failed: ${err instanceof Error ? err.message : String(err)}`);
205
208
  }
206
209
  }
210
+ // RE-ARM, NOT RESOLVE (2026-08-29). A kind with no declared capability
211
+ // probe went quiet: the alarm is armed for the next occurrence and NOTHING
212
+ // is claimed about the condition. No operator comment is written here on
213
+ // purpose — see rearmEpisode.
214
+ for (const { episode, quietMinutes, clearZeroVolumeFlag } of decision.rearm) {
215
+ try {
216
+ await rearmEpisode(episode, quietMinutes);
217
+ if (clearZeroVolumeFlag) await clearZeroVolumeHold(episode, zeroVolume.countByKind[episode.kind] ?? 0);
218
+ log.info(`[platform-fault-escalation] RE-ARMED '${episode.kind}' after ${Math.round(quietMinutes)} quiet min — no probe declared, so the condition is UNVERIFIED and no close was claimed`);
219
+ } catch (err) {
220
+ log.error(`[platform-fault-escalation] re-arm for '${episode.kind}' failed (retried next sweep): ${err instanceof Error ? err.message : String(err)}`);
221
+ }
222
+ }
207
223
 
208
224
  // Emitted every run, even when nothing fired — a sweep that only reports
209
225
  // when it escalated is indistinguishable from one that stopped running.