@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
140
|
-
expect(decision.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
363
|
-
expect(decision.resolve
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|