pi-mega-compact 0.16.3 → 0.17.0
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/config/vector-cortex.js +81 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints/registry-ext.js +30 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +4 -14
- package/dist/extensions/dashboard-server/api-contracts/vector-cortex.js +12 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +10 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex.js +292 -0
- package/dist/extensions/dashboard-server/routes.js +1 -0
- package/dist/extensions/dashboard-server/server.js +9 -1
- package/dist/extensions/mega-events/context-handler.js +11 -0
- package/dist/extensions/mega-runtime/vector-cortex-ledger.js +131 -0
- package/dist/extensions/mega-runtime/vector-cortex-safety.js +100 -0
- package/dist/src/config/vector-cortex.js +81 -0
- package/dist/src/config.js +6 -0
- package/dist/src/vector-cortex/eval/annotations.js +41 -0
- package/dist/src/vector-cortex/eval/metrics.js +86 -0
- package/dist/src/vector-cortex/eval/observer.js +61 -0
- package/dist/src/vector-cortex/eval/persist.js +67 -0
- package/dist/src/vector-cortex/eval/reader.js +29 -0
- package/dist/src/vector-cortex/eval/types.js +33 -0
- package/dist/src/vector-cortex/ledger/adapter.js +55 -0
- package/dist/src/vector-cortex/ledger/compat-journal.js +206 -0
- package/dist/src/vector-cortex/ledger/emit.js +53 -0
- package/dist/src/vector-cortex/ledger/event-codec.js +72 -0
- package/dist/src/vector-cortex/ledger/event-codecB.js +76 -0
- package/dist/src/vector-cortex/ledger/sqlite.js +200 -0
- package/dist/src/vector-cortex/ledger/store.js +184 -0
- package/dist/src/vector-cortex/ledger/types.js +52 -0
- package/dist/src/vector-cortex/ledger/validator.js +117 -0
- package/dist/src/vector-cortex/migrations/effective-cut-v2.js +103 -0
- package/dist/src/vector-cortex/migrations/occurrence-v2.js +139 -0
- package/dist/src/vector-cortex/replay/cut.js +123 -0
- package/dist/src/vector-cortex/replay/emit.js +58 -0
- package/dist/src/vector-cortex/replay/replay.js +260 -0
- package/dist/src/vector-cortex/replay/replayB.js +73 -0
- package/dist/src/vector-cortex/replay/types.js +52 -0
- package/dist/src/vector-cortex/resilience/breaker-core.js +359 -0
- package/dist/src/vector-cortex/resilience/breaker.js +12 -0
- package/dist/src/vector-cortex/resilience/emit.js +56 -0
- package/dist/src/vector-cortex/resilience/spool-core.js +347 -0
- package/dist/src/vector-cortex/resilience/spool.js +12 -0
- package/dist/src/vector-cortex/resilience/types.js +32 -0
- package/dist/vector-cortex/eval/annotations.js +41 -0
- package/dist/vector-cortex/eval/metrics.js +86 -0
- package/dist/vector-cortex/eval/observer.js +61 -0
- package/dist/vector-cortex/eval/persist.js +67 -0
- package/dist/vector-cortex/eval/reader.js +29 -0
- package/dist/vector-cortex/eval/types.js +33 -0
- package/dist/vector-cortex/ledger/adapter.js +55 -0
- package/dist/vector-cortex/ledger/compat-journal.js +206 -0
- package/dist/vector-cortex/ledger/emit.js +53 -0
- package/dist/vector-cortex/ledger/event-codec.js +72 -0
- package/dist/vector-cortex/ledger/event-codecB.js +76 -0
- package/dist/vector-cortex/ledger/sqlite.js +200 -0
- package/dist/vector-cortex/ledger/store.js +184 -0
- package/dist/vector-cortex/ledger/types.js +52 -0
- package/dist/vector-cortex/ledger/validator.js +117 -0
- package/dist/vector-cortex/migrations/effective-cut-v2.js +103 -0
- package/dist/vector-cortex/migrations/occurrence-v2.js +139 -0
- package/dist/vector-cortex/replay/cut.js +123 -0
- package/dist/vector-cortex/replay/emit.js +58 -0
- package/dist/vector-cortex/replay/replay.js +260 -0
- package/dist/vector-cortex/replay/replayB.js +73 -0
- package/dist/vector-cortex/replay/types.js +52 -0
- package/dist/vector-cortex/resilience/breaker-core.js +359 -0
- package/dist/vector-cortex/resilience/breaker.js +12 -0
- package/dist/vector-cortex/resilience/emit.js +56 -0
- package/dist/vector-cortex/resilience/spool-core.js +347 -0
- package/dist/vector-cortex/resilience/spool.js +12 -0
- package/dist/vector-cortex/resilience/types.js +32 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-Y_X0Bbjd.js → AreaChart-CAnWomhw.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-Y_X0Bbjd.js.map → AreaChart-CAnWomhw.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-B9XDb-Ob.js → BarChart-lZ2-s71k.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-B9XDb-Ob.js.map → BarChart-lZ2-s71k.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-BUGVb1G1.js → CacheTab-BL2Thh66.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-BUGVb1G1.js.map → CacheTab-BL2Thh66.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-dOqLYTsy.js → EventsTab-DEh2Z9fp.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-dOqLYTsy.js.map → EventsTab-DEh2Z9fp.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-c2vezKg1.js → HealthTab-DTwKclxk.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-c2vezKg1.js.map → HealthTab-DTwKclxk.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-bQYHIcdx.js → MaintenanceTab-DDfBEdbX.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-bQYHIcdx.js.map → MaintenanceTab-DDfBEdbX.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-2L8pH6y2.js → MemoryMapTab-Dgfmg-0Q.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-2L8pH6y2.js.map → MemoryMapTab-Dgfmg-0Q.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CzUx1jpo.js → MetricsTab-DOcMm0Tx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CzUx1jpo.js.map → MetricsTab-DOcMm0Tx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-eCNoaclv.js → OverviewTab-DHT5aoHg.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-eCNoaclv.js.map → OverviewTab-DHT5aoHg.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-tb0IW3NF.js → ReposTab-CGAmtR_c.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-tb0IW3NF.js.map → ReposTab-CGAmtR_c.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C-IoLw5h.js → SessionsTab-BIgx6iKT.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C-IoLw5h.js.map → SessionsTab-BIgx6iKT.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-BvmFi8gf.js → SetupTab-ADghKuEI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-BvmFi8gf.js.map → SetupTab-ADghKuEI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DBH-cOlA.js → TimeSavedCard-C33UiiOM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-DBH-cOlA.js.map → TimeSavedCard-C33UiiOM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DBMgmvnX.js → TurnsTab-DdbL9wPN.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-DBMgmvnX.js.map → TurnsTab-DdbL9wPN.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-Cl1pbEdR.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-Cl1pbEdR.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-1LcQrXk3.js → WikiTab-Ch00OFKs.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-1LcQrXk3.js.map → WikiTab-Ch00OFKs.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-DtEsZTUE.js → button-ByeEdo2g.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-DtEsZTUE.js.map → button-ByeEdo2g.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-DcfTmw-4.js → card-DesoLXvc.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-DcfTmw-4.js.map → card-DesoLXvc.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BsIxUDyw.js → generateCategoricalChart-CCAB7Iz4.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BsIxUDyw.js.map → generateCategoricalChart-CCAB7Iz4.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-B93cewiX.js → index-B6cgyAPr.js} +34 -29
- package/extensions/dashboard-client/dist/assets/index-B6cgyAPr.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/index-CfvziyBi.css +1 -0
- package/extensions/dashboard-client/dist/assets/{switch-Bowd7jCD.js → switch-DIZC-oe-.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-Bowd7jCD.js.map → switch-DIZC-oe-.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-Bo7kJZ4u.js → toggle-CIoWaH2e.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-Bo7kJZ4u.js.map → toggle-CIoWaH2e.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-zNhb0c3u.js → useSSE-Bv4943l1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-zNhb0c3u.js.map → useSSE-Bv4943l1.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/App.tsx +2 -0
- package/extensions/dashboard-client/src/api/vector-cortex.ts +54 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +272 -0
- package/extensions/dashboard-client/src/tabs/registry.ts +4 -1
- package/extensions/dashboard-client/src/types/vector-cortex.ts +84 -0
- package/extensions/dashboard-server/api-contracts/endpoints/registry-ext.ts +67 -0
- package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +4 -21
- package/extensions/dashboard-server/api-contracts/index.ts +3 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +136 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +35 -0
- package/extensions/dashboard-server/routes-vector-cortex.ts +332 -0
- package/extensions/dashboard-server/routes.ts +6 -0
- package/extensions/dashboard-server/server.ts +8 -0
- package/extensions/mega-events/context-handler.ts +16 -0
- package/extensions/mega-runtime/vector-cortex-ledger.ts +165 -0
- package/extensions/mega-runtime/vector-cortex-safety.ts +181 -0
- package/package.json +2 -1
- package/src/config/vector-cortex.ts +87 -0
- package/src/config.ts +27 -0
- package/src/vector-cortex/eval/annotations.ts +70 -0
- package/src/vector-cortex/eval/metrics.ts +113 -0
- package/src/vector-cortex/eval/observer.ts +86 -0
- package/src/vector-cortex/eval/persist.ts +73 -0
- package/src/vector-cortex/eval/reader.ts +45 -0
- package/src/vector-cortex/eval/types.ts +89 -0
- package/src/vector-cortex/ledger/adapter.ts +71 -0
- package/src/vector-cortex/ledger/compat-journal.ts +234 -0
- package/src/vector-cortex/ledger/emit.ts +86 -0
- package/src/vector-cortex/ledger/event-codec.ts +79 -0
- package/src/vector-cortex/ledger/event-codecB.ts +87 -0
- package/src/vector-cortex/ledger/sqlite.ts +265 -0
- package/src/vector-cortex/ledger/store.ts +380 -0
- package/src/vector-cortex/ledger/types.ts +141 -0
- package/src/vector-cortex/ledger/validator.ts +124 -0
- package/src/vector-cortex/migrations/effective-cut-v2.ts +149 -0
- package/src/vector-cortex/migrations/occurrence-v2.ts +192 -0
- package/src/vector-cortex/replay/cut.ts +169 -0
- package/src/vector-cortex/replay/emit.ts +89 -0
- package/src/vector-cortex/replay/replay.ts +308 -0
- package/src/vector-cortex/replay/replayB.ts +97 -0
- package/src/vector-cortex/replay/types.ts +145 -0
- package/src/vector-cortex/resilience/breaker-core.ts +444 -0
- package/src/vector-cortex/resilience/breaker.ts +22 -0
- package/src/vector-cortex/resilience/emit.ts +99 -0
- package/src/vector-cortex/resilience/spool-core.ts +411 -0
- package/src/vector-cortex/resilience/spool.ts +22 -0
- package/src/vector-cortex/resilience/types.ts +158 -0
- package/extensions/dashboard-client/dist/assets/index-B93cewiX.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-C_tCbOI0.css +0 -1
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/resilience/breaker-core.ts — VC0C breaker state machine (VC0C).
|
|
3
|
+
*
|
|
4
|
+
* Implementation file behind the `breaker.ts` factory shell. See breaker.ts for
|
|
5
|
+
* the public surface; this file owns the TRIAD_RESILIENCE §breaker state machine.
|
|
6
|
+
*
|
|
7
|
+
* Implements TRIAD_RESILIENCE.md §breaker state machine over the shared
|
|
8
|
+
* constants in src/config/vector-cortex.ts (consumed, NOT redefined — resolves
|
|
9
|
+
* the standing "dead BREAKER_* constants" issue):
|
|
10
|
+
*
|
|
11
|
+
* States: CLOSED_A, OPEN_B, OPEN_C, PROBE_B, PROBE_A, MANUAL_HALT.
|
|
12
|
+
* Rolling window BREAKER_WINDOW_MS; min BREAKER_MIN_ATTEMPTS attempts.
|
|
13
|
+
* Performance trip at BREAKER_PERF_FAILURES failures or failure-rate >=
|
|
14
|
+
* BREAKER_PERF_FAILURE_RATE; correctness trip on the FIRST correctness failure.
|
|
15
|
+
* Cooldown BREAKER_COOLDOWN_MS; BREAKER_PROBE_COUNT consecutive probes succeed
|
|
16
|
+
* to advance; exponential retry BREAKER_RETRY_BASE_MS * 2^attempt capped at
|
|
17
|
+
* BREAKER_RETRY_CAP_MS with deterministic +-10% jitter derived from the
|
|
18
|
+
* subsystem digest; promotion hysteresis needs failure rate <
|
|
19
|
+
* BREAKER_HYSTERESIS_FAILURE_RATE and p95 <= BREAKER_HYSTERESIS_BUDGET_P95_MS;
|
|
20
|
+
* BREAKER_MIN_HEALTHY_RESIDENCE_MS healthy residence before another promotion.
|
|
21
|
+
*
|
|
22
|
+
* Clock: windows/cooldowns use MONOTONIC time (`now()`, injectable and
|
|
23
|
+
* defaulting to performance.now()); wall time appears only in `updatedAt`
|
|
24
|
+
* (records). Backward/forward wall jumps never alter eligibility. Restart
|
|
25
|
+
* reconstructs state from appended breaker events (`onEvent`). Expired cooldown
|
|
26
|
+
* may PROBE, never directly promote. Probe output is never served. Manual halt
|
|
27
|
+
* requires a reason + explicit admin `reset` that clears cooldown but never
|
|
28
|
+
* evidence.
|
|
29
|
+
*
|
|
30
|
+
* Pure in-memory + event-serializable state; no storage, no network
|
|
31
|
+
* (PREVENT-PI-004), no `any` (PREVENT-011).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
BREAKER_WINDOW_MS,
|
|
36
|
+
BREAKER_MIN_ATTEMPTS,
|
|
37
|
+
BREAKER_PERF_FAILURES,
|
|
38
|
+
BREAKER_PERF_FAILURE_RATE,
|
|
39
|
+
BREAKER_CORRECTNESS_FAILURES,
|
|
40
|
+
BREAKER_COOLDOWN_MS,
|
|
41
|
+
BREAKER_PROBE_COUNT,
|
|
42
|
+
BREAKER_RETRY_BASE_MS,
|
|
43
|
+
BREAKER_RETRY_CAP_MS,
|
|
44
|
+
BREAKER_RETRY_JITTER,
|
|
45
|
+
BREAKER_HYSTERESIS_FAILURE_RATE,
|
|
46
|
+
BREAKER_HYSTERESIS_BUDGET_P95_MS,
|
|
47
|
+
BREAKER_MIN_HEALTHY_RESIDENCE_MS,
|
|
48
|
+
} from "../../config/vector-cortex.js";
|
|
49
|
+
import type {
|
|
50
|
+
Breaker,
|
|
51
|
+
BreakerRecord,
|
|
52
|
+
BreakerState,
|
|
53
|
+
BreakerTripKind,
|
|
54
|
+
Mode,
|
|
55
|
+
TriadResult,
|
|
56
|
+
} from "./types.js";
|
|
57
|
+
import type { ResilienceReporter } from "./emit.js";
|
|
58
|
+
|
|
59
|
+
/** Monotonic clock supplier (fake-clock testable). */
|
|
60
|
+
export type MonotonicClock = () => number;
|
|
61
|
+
/** Wall-clock ISO supplier for record timestamps (irrelevant to eligibility). */
|
|
62
|
+
export type WallClock = () => string;
|
|
63
|
+
|
|
64
|
+
export interface BreakerOptions {
|
|
65
|
+
/** Monotonic clock, default performance.now(). */
|
|
66
|
+
readonly now?: MonotonicClock;
|
|
67
|
+
/** Wall-clock ISO, default new Date().toISOString(). */
|
|
68
|
+
readonly wallNow?: WallClock;
|
|
69
|
+
/** Resilience emit seam (flag-gated). */
|
|
70
|
+
readonly reporter?: ResilienceReporter;
|
|
71
|
+
/**
|
|
72
|
+
* Append-only breaker event sink for restart reconstruction. Each transition
|
|
73
|
+
* appends a canonical event; `createBreaker` may replay them to rebuild state.
|
|
74
|
+
*/
|
|
75
|
+
readonly onEvent?: (event: Record<string, unknown>) => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface SubsystemState {
|
|
79
|
+
state: BreakerState;
|
|
80
|
+
transitionedAtMs: number;
|
|
81
|
+
attempts: number[];
|
|
82
|
+
failures: number[];
|
|
83
|
+
latencies: number[];
|
|
84
|
+
cooldownUntilMs?: number;
|
|
85
|
+
probeCount: number;
|
|
86
|
+
retryAttempt: number;
|
|
87
|
+
frozenFrontier: boolean;
|
|
88
|
+
manualReason?: string;
|
|
89
|
+
tripKind: BreakerTripKind;
|
|
90
|
+
/** Monotonic start of the current healthy-residence window (OPEN_B only). */
|
|
91
|
+
healthyResidenceSinceMs?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Deterministic +-jitter fraction for a subsystem (0..1) from its digest. */
|
|
95
|
+
function jitterSeed(subsystem: string): number {
|
|
96
|
+
let h = 2166136261;
|
|
97
|
+
for (let i = 0; i < subsystem.length; i++) {
|
|
98
|
+
h ^= subsystem.charCodeAt(i);
|
|
99
|
+
h = Math.imul(h, 16777619);
|
|
100
|
+
}
|
|
101
|
+
return (h >>> 0) / 4294967295; // 0..1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Iterative backoff delay with +-BREAKER_RETRY_JITTER deterministic jitter. */
|
|
105
|
+
export function breakerRetryDelay(subsystem: string, attempt: number): number {
|
|
106
|
+
const exp = Math.min(
|
|
107
|
+
BREAKER_RETRY_BASE_MS * 2 ** attempt,
|
|
108
|
+
BREAKER_RETRY_CAP_MS,
|
|
109
|
+
);
|
|
110
|
+
const frac = jitterSeed(subsystem); // 0..1
|
|
111
|
+
const factor = 1 + (frac * 2 - 1) * BREAKER_RETRY_JITTER; // 0.9..1.1
|
|
112
|
+
return Math.round(exp * factor);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function percentile(sorted: number[], p: number): number {
|
|
116
|
+
if (sorted.length === 0) return 0;
|
|
117
|
+
const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1));
|
|
118
|
+
return sorted[idx] as number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ConcreteBreaker extends Breaker {
|
|
122
|
+
/** Live per-subsystem record. */
|
|
123
|
+
snapshot(subsystem: string): BreakerRecord;
|
|
124
|
+
/** Admin reset: clears cooldown (never evidence), or unwires MANUAL_HALT. */
|
|
125
|
+
reset(subsystem: string): BreakerRecord;
|
|
126
|
+
/** Whether the breaker is healthy enough that mode A may be attempted. */
|
|
127
|
+
modeFor(subsystem: string): Mode;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createBreaker(opts: BreakerOptions = {}): ConcreteBreaker {
|
|
131
|
+
const now: MonotonicClock = opts.now ?? (() => performance.now());
|
|
132
|
+
const wallNow: WallClock = opts.wallNow ?? (() => new Date().toISOString());
|
|
133
|
+
const reporter = opts.reporter;
|
|
134
|
+
const onEvent = opts.onEvent;
|
|
135
|
+
const subs = new Map<string, SubsystemState>();
|
|
136
|
+
|
|
137
|
+
function get(subsystem: string): SubsystemState {
|
|
138
|
+
let s = subs.get(subsystem);
|
|
139
|
+
if (!s) {
|
|
140
|
+
s = {
|
|
141
|
+
state: "CLOSED_A",
|
|
142
|
+
transitionedAtMs: now(),
|
|
143
|
+
attempts: [],
|
|
144
|
+
failures: [],
|
|
145
|
+
latencies: [],
|
|
146
|
+
retryAttempt: 0,
|
|
147
|
+
probeCount: 0,
|
|
148
|
+
frozenFrontier: false,
|
|
149
|
+
tripKind: "correctness",
|
|
150
|
+
};
|
|
151
|
+
subs.set(subsystem, s);
|
|
152
|
+
}
|
|
153
|
+
return s;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function pruneWindow(s: SubsystemState, at: number): void {
|
|
157
|
+
const cutoff = at - BREAKER_WINDOW_MS;
|
|
158
|
+
s.attempts = s.attempts.filter((t) => t > cutoff);
|
|
159
|
+
s.failures = s.failures.filter((t) => t > cutoff);
|
|
160
|
+
s.latencies = s.latencies.slice(-s.attempts.length);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Emit a transition with the minimal event surface + optional append. */
|
|
164
|
+
function transition(s: SubsystemState, to: BreakerState, event: string, fields: Record<string, unknown>): void {
|
|
165
|
+
s.state = to;
|
|
166
|
+
s.transitionedAtMs = now();
|
|
167
|
+
if (onEvent) {
|
|
168
|
+
try {
|
|
169
|
+
onEvent({ type: event, to, ...fields, atMs: s.transitionedAtMs });
|
|
170
|
+
} catch {
|
|
171
|
+
/* non-fatal */
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (reporter) {
|
|
175
|
+
if (event === "breaker_opened") {
|
|
176
|
+
reporter.breakerOpened({
|
|
177
|
+
subsystem: String(fields.subsystem ?? ""),
|
|
178
|
+
fromState: String(fields.fromState ?? ""),
|
|
179
|
+
toState: to,
|
|
180
|
+
code: String(fields.code ?? ""),
|
|
181
|
+
attempts: Number(fields.attempts ?? 0),
|
|
182
|
+
failures: Number(fields.failures ?? 0),
|
|
183
|
+
});
|
|
184
|
+
} else if (event === "probe_promoted") {
|
|
185
|
+
reporter.probePromoted({
|
|
186
|
+
subsystem: String(fields.subsystem ?? ""),
|
|
187
|
+
fromState: String(fields.fromState ?? ""),
|
|
188
|
+
toState: to,
|
|
189
|
+
probeCount: Number(fields.probeCount ?? 0),
|
|
190
|
+
retryAttempt: Number(fields.retryAttempt ?? 0),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function record(subsystem: string): BreakerRecord {
|
|
197
|
+
const s = get(subsystem);
|
|
198
|
+
const at = now();
|
|
199
|
+
pruneWindow(s, at);
|
|
200
|
+
const failureRate = s.attempts.length === 0 ? 0 : s.failures.length / s.attempts.length;
|
|
201
|
+
const sortedLat = [...s.latencies].sort((a, b) => a - b);
|
|
202
|
+
const p95 = percentile(sortedLat, 95);
|
|
203
|
+
const r: BreakerRecord = {
|
|
204
|
+
subsystem,
|
|
205
|
+
state: s.state,
|
|
206
|
+
windowStartMs: s.attempts.length > 0 ? Math.min(...s.attempts) : at,
|
|
207
|
+
attempts: s.attempts.length,
|
|
208
|
+
failures: s.failures.length,
|
|
209
|
+
tripKind: s.tripKind,
|
|
210
|
+
transitionedAtMs: s.transitionedAtMs,
|
|
211
|
+
cooldownUntilMs: s.cooldownUntilMs,
|
|
212
|
+
probeCount: s.probeCount,
|
|
213
|
+
retryAttempt: s.retryAttempt,
|
|
214
|
+
retryDelayMs: breakerRetryDelay(subsystem, s.retryAttempt),
|
|
215
|
+
frozenFrontier: s.frozenFrontier,
|
|
216
|
+
manualReason: s.manualReason,
|
|
217
|
+
updatedAt: wallNow(),
|
|
218
|
+
p95Ms: p95,
|
|
219
|
+
failureRate,
|
|
220
|
+
};
|
|
221
|
+
return r;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Enter a cooldown + the given open state; returns the open-state name. */
|
|
225
|
+
function openTo(s: SubsystemState, subsystem: string, to: "OPEN_B" | "OPEN_C" | "MANUAL_HALT", kind: BreakerTripKind, code: string): BreakerState {
|
|
226
|
+
const from = s.state;
|
|
227
|
+
s.tripKind = kind;
|
|
228
|
+
if (to === "MANUAL_HALT") {
|
|
229
|
+
s.cooldownUntilMs = undefined;
|
|
230
|
+
s.probeCount = 0;
|
|
231
|
+
transition(s, "MANUAL_HALT", "breaker_opened", { subsystem, fromState: from, code, attempts: s.attempts.length, failures: s.failures.length });
|
|
232
|
+
return "MANUAL_HALT";
|
|
233
|
+
}
|
|
234
|
+
s.cooldownUntilMs = now() + BREAKER_COOLDOWN_MS;
|
|
235
|
+
s.probeCount = 0;
|
|
236
|
+
if (to === "OPEN_B") s.healthyResidenceSinceMs = now(); // start healthy residence
|
|
237
|
+
transition(s, to, "breaker_opened", { subsystem, fromState: from, code, attempts: s.attempts.length, failures: s.failures.length });
|
|
238
|
+
return to;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** After cooldown, an open state may PROBE (never directly promote). */
|
|
242
|
+
function maybeProbe(s: SubsystemState, subsystem: string, openState: "OPEN_B" | "OPEN_C"): void {
|
|
243
|
+
const at = now();
|
|
244
|
+
if (s.state !== openState) return;
|
|
245
|
+
if (s.cooldownUntilMs === undefined || at < s.cooldownUntilMs) return;
|
|
246
|
+
// Promotion back toward A additionally requires the minimum healthy
|
|
247
|
+
// residence in OPEN_B (invariant: promotion never precedes the 5min
|
|
248
|
+
// residence, so we may only PROBE here, never directly promote).
|
|
249
|
+
if (openState === "OPEN_B") {
|
|
250
|
+
const since = s.healthyResidenceSinceMs ?? at;
|
|
251
|
+
if (at - since < BREAKER_MIN_HEALTHY_RESIDENCE_MS) return;
|
|
252
|
+
}
|
|
253
|
+
s.cooldownUntilMs = undefined;
|
|
254
|
+
const to = openState === "OPEN_C" ? "PROBE_B" : "PROBE_A";
|
|
255
|
+
transition(s, to, "probe_promoted", { subsystem, fromState: openState, probeCount: 0, retryAttempt: s.retryAttempt });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Promotion hysteresis: promotion requires window failure rate < 2% and p95
|
|
260
|
+
* latency within budget (TRIAD_RESILIENCE). Without it, a recovery probe that
|
|
261
|
+
* passes but with a poor window must NOT promote.
|
|
262
|
+
*/
|
|
263
|
+
function canPromote(s: SubsystemState): boolean {
|
|
264
|
+
const failureRate = s.attempts.length === 0 ? 0 : s.failures.length / s.attempts.length;
|
|
265
|
+
if (failureRate >= BREAKER_HYSTERESIS_FAILURE_RATE) return false;
|
|
266
|
+
const sortedLat = [...s.latencies].sort((a, b) => a - b);
|
|
267
|
+
return percentile(sortedLat, 95) <= BREAKER_HYSTERESIS_BUDGET_P95_MS;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** A successful probe: advance the count, and promote once N AND hysteresis hold. */
|
|
271
|
+
function advanceProbe(s: SubsystemState, subsystem: string): void {
|
|
272
|
+
if (s.state !== "PROBE_B" && s.state !== "PROBE_A") return;
|
|
273
|
+
s.probeCount += 1;
|
|
274
|
+
if (s.probeCount >= BREAKER_PROBE_COUNT) {
|
|
275
|
+
if (!canPromote(s)) {
|
|
276
|
+
// Probe succeeded but hysteresis not met — stay probing, no promotion.
|
|
277
|
+
s.probeCount = 0;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const from = s.state;
|
|
281
|
+
s.probeCount = 0;
|
|
282
|
+
const to = from === "PROBE_B" ? "OPEN_B" : "CLOSED_A";
|
|
283
|
+
transition(s, to, "probe_promoted", { subsystem, fromState: from, probeCount: BREAKER_PROBE_COUNT, retryAttempt: s.retryAttempt });
|
|
284
|
+
if (to === "OPEN_B") {
|
|
285
|
+
s.cooldownUntilMs = now() + BREAKER_COOLDOWN_MS;
|
|
286
|
+
s.healthyResidenceSinceMs = now();
|
|
287
|
+
}
|
|
288
|
+
s.retryAttempt = 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const breaker: ConcreteBreaker = {
|
|
293
|
+
modeFor(subsystem) {
|
|
294
|
+
const s = get(subsystem);
|
|
295
|
+
const at = now();
|
|
296
|
+
pruneWindow(s, at);
|
|
297
|
+
if (s.state === "MANUAL_HALT") return "C";
|
|
298
|
+
if (s.state === "CLOSED_A") return "A";
|
|
299
|
+
// OPEN_B / OPEN_C / PROBE_* — the primary is broken; fall back to B, or C
|
|
300
|
+
// once B itself has tripped.
|
|
301
|
+
if (s.state === "OPEN_C" || s.state === "PROBE_B") return "C";
|
|
302
|
+
return "B";
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
execute<T>(
|
|
306
|
+
subsystem: string,
|
|
307
|
+
inputDigest: string,
|
|
308
|
+
run: Record<Mode, () => T>,
|
|
309
|
+
validate: (v: T) => boolean,
|
|
310
|
+
): TriadResult<T> {
|
|
311
|
+
const s = get(subsystem);
|
|
312
|
+
const at = now();
|
|
313
|
+
pruneWindow(s, at);
|
|
314
|
+
const mode: Mode = breaker.modeFor(subsystem);
|
|
315
|
+
s.attempts.push(at);
|
|
316
|
+
let ok = false;
|
|
317
|
+
let value: T | undefined;
|
|
318
|
+
let errorCode = "";
|
|
319
|
+
try {
|
|
320
|
+
value = run[mode]();
|
|
321
|
+
ok = validate(value);
|
|
322
|
+
if (!ok) errorCode = "TRI_OUTPUT_INVALID";
|
|
323
|
+
} catch {
|
|
324
|
+
errorCode = "TRI_EXEC_THREW";
|
|
325
|
+
ok = false;
|
|
326
|
+
}
|
|
327
|
+
const latency = now() - at;
|
|
328
|
+
s.latencies.push(latency);
|
|
329
|
+
|
|
330
|
+
if (ok) {
|
|
331
|
+
const openState: "OPEN_B" | "OPEN_C" =
|
|
332
|
+
s.state === "OPEN_C" ? "OPEN_C" : "OPEN_B";
|
|
333
|
+
maybeProbe(s, subsystem, openState);
|
|
334
|
+
advanceProbe(s, subsystem);
|
|
335
|
+
return {
|
|
336
|
+
ok: true,
|
|
337
|
+
value: value as T,
|
|
338
|
+
mode,
|
|
339
|
+
inputDigest,
|
|
340
|
+
outputDigest: String(((value as { digest?: unknown })?.digest) ?? ""),
|
|
341
|
+
algorithmVersion: "vc0c-triad-1",
|
|
342
|
+
latencyMs: latency,
|
|
343
|
+
breaker: record(subsystem),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
s.failures.push(at);
|
|
348
|
+
// Probe failure: return to the OPEN state it was probing FROM and increment
|
|
349
|
+
// backoff (TRIAD_RESILIENCE §transitions: "Any probe failure returns to its
|
|
350
|
+
// open state and increments backoff"). A probe failure is NOT a fresh trip —
|
|
351
|
+
// it re-enters the originating open state/cooldown and never serves output.
|
|
352
|
+
// This governs both PROBE_A (probing OPEN_B, mode B) and PROBE_B (probing
|
|
353
|
+
// OPEN_C, mode C); without it a PROBE_A failure would mis-trip to OPEN_C and
|
|
354
|
+
// a PROBE_B failure would short-circuit with no revert and no backoff bump.
|
|
355
|
+
if (s.state === "PROBE_A" || s.state === "PROBE_B") {
|
|
356
|
+
const from: "OPEN_B" | "OPEN_C" = s.state === "PROBE_A" ? "OPEN_B" : "OPEN_C";
|
|
357
|
+
openTo(s, subsystem, from, "performance", errorCode);
|
|
358
|
+
s.retryAttempt += 1;
|
|
359
|
+
return { ok: false, mode, code: errorCode, retryable: true, breaker: record(subsystem) };
|
|
360
|
+
}
|
|
361
|
+
// Trip rules for FRESH (non-probe) failures.
|
|
362
|
+
if (mode === "C") {
|
|
363
|
+
// C failures are continuity-level; they do not advance the breaker but
|
|
364
|
+
// surface as retryable C failures (unchanged transcript).
|
|
365
|
+
return { ok: false, mode, code: errorCode, retryable: true, breaker: record(subsystem) };
|
|
366
|
+
}
|
|
367
|
+
if (mode === "B") {
|
|
368
|
+
// Fresh B failure -> OPEN_C. tripKind mirrors the A path: only a semantic
|
|
369
|
+
// correctness signal (TRI_OUTPUT_INVALID) is "correctness"; a plain
|
|
370
|
+
// TRI_EXEC_THREW perf throw is "performance". (Not `failures.length >=
|
|
371
|
+
// BREAKER_CORRECTNESS_FAILURES` — that is ALWAYS true right after
|
|
372
|
+
// recording this failure, which would tautologically mislabel every
|
|
373
|
+
// B-throw as a correctness trip.)
|
|
374
|
+
openTo(s, subsystem, "OPEN_C", errorCode === "TRI_OUTPUT_INVALID" ? "correctness" : "performance", errorCode);
|
|
375
|
+
s.retryAttempt += 1;
|
|
376
|
+
return { ok: false, mode, code: errorCode, retryable: true, breaker: record(subsystem) };
|
|
377
|
+
}
|
|
378
|
+
// mode === "A" failure: correctness (output validation failed) is
|
|
379
|
+
// ZERO-TOLERANCE and trips on its first occurrence regardless of attempt
|
|
380
|
+
// count (TRIAD_RESILIENCE: "correctness trip on first failure"). The
|
|
381
|
+
// 20-attempt minimum gates only the PERFORMANCE rate trip. A first, isolated
|
|
382
|
+
// failure is a performance-rate signal (rate 1.0), so it needs the window.
|
|
383
|
+
const failureRate = s.failures.length / s.attempts.length;
|
|
384
|
+
const perf = s.failures.length >= BREAKER_PERF_FAILURES || failureRate >= BREAKER_PERF_FAILURE_RATE;
|
|
385
|
+
const correctness =
|
|
386
|
+
errorCode === "TRI_OUTPUT_INVALID" ||
|
|
387
|
+
(s.failures.length >= BREAKER_CORRECTNESS_FAILURES && failureRate < BREAKER_PERF_FAILURE_RATE);
|
|
388
|
+
const trips = correctness || (s.attempts.length >= BREAKER_MIN_ATTEMPTS && perf);
|
|
389
|
+
if (trips) {
|
|
390
|
+
openTo(s, subsystem, "OPEN_B", correctness ? "correctness" : "performance", errorCode);
|
|
391
|
+
s.retryAttempt += 1;
|
|
392
|
+
}
|
|
393
|
+
return { ok: false, mode, code: errorCode, retryable: true, breaker: record(subsystem) };
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
recordProbe(subsystem: string): BreakerRecord {
|
|
397
|
+
const s = get(subsystem);
|
|
398
|
+
const openState: "OPEN_B" | "OPEN_C" =
|
|
399
|
+
s.state === "OPEN_C" ? "OPEN_C" : "OPEN_B";
|
|
400
|
+
maybeProbe(s, subsystem, openState);
|
|
401
|
+
advanceProbe(s, subsystem);
|
|
402
|
+
return record(subsystem);
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
manualHalt(reason: string): BreakerRecord {
|
|
406
|
+
if (!reason) return record("provider");
|
|
407
|
+
// Authority/digest/causal corruption degrades EVERY tracked subsystem to
|
|
408
|
+
// MANUAL_HALT (the corruption is global, not per-subsystem). Returns the
|
|
409
|
+
// provider's record. A manual halt requires a reason and an explicit
|
|
410
|
+
// admin reset to clear — evidence is never discarded.
|
|
411
|
+
const targets = subs.size === 0 ? ["provider"] : [...subs.keys()];
|
|
412
|
+
let last: BreakerRecord = record("provider");
|
|
413
|
+
for (const subsystem of targets) {
|
|
414
|
+
const s = get(subsystem);
|
|
415
|
+
s.manualReason = reason;
|
|
416
|
+
openTo(s, subsystem, "MANUAL_HALT", "manual", "TRI_MANUAL_HALT");
|
|
417
|
+
last = record(subsystem);
|
|
418
|
+
}
|
|
419
|
+
return last;
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
snapshot(subsystem) {
|
|
423
|
+
return record(subsystem);
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
reset(subsystem) {
|
|
427
|
+
const s = get(subsystem);
|
|
428
|
+
const at = now();
|
|
429
|
+
pruneWindow(s, at);
|
|
430
|
+
// Manual reset clears cooldown / probe state but NEVER evidence (attempts
|
|
431
|
+
// and failures are retained). A MANUAL_HALT unwires to a probe of B.
|
|
432
|
+
s.cooldownUntilMs = undefined;
|
|
433
|
+
s.probeCount = 0;
|
|
434
|
+
s.manualReason = undefined;
|
|
435
|
+
if (s.state === "MANUAL_HALT") {
|
|
436
|
+
s.state = "OPEN_B";
|
|
437
|
+
s.transitionedAtMs = now();
|
|
438
|
+
}
|
|
439
|
+
return record(subsystem);
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
return breaker;
|
|
444
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/resilience/breaker.ts — VC0C breaker factory shell (VC0C).
|
|
3
|
+
*
|
|
4
|
+
* Thin barrel over the state-machine implementation in `breaker-core.ts`
|
|
5
|
+
* (delegate-shell + impl-file split keeps the frequently-touched file small).
|
|
6
|
+
* Owns the public `createBreaker` factory and the monotonic-clock/options types,
|
|
7
|
+
* all re-exported for the safety adapter and tests.
|
|
8
|
+
*
|
|
9
|
+
* Full semantics live in breaker-core.ts (TRIAD_RESILIENCE §breaker). Local-only,
|
|
10
|
+
* no network (PREVENT-PI-004), no `any` (PREVENT-011).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
createBreaker,
|
|
15
|
+
breakerRetryDelay,
|
|
16
|
+
} from "./breaker-core.js";
|
|
17
|
+
export type {
|
|
18
|
+
MonotonicClock,
|
|
19
|
+
WallClock,
|
|
20
|
+
BreakerOptions,
|
|
21
|
+
ConcreteBreaker,
|
|
22
|
+
} from "./breaker-core.js";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/resilience/emit.ts — VC0C resilience emit seam (VC0C).
|
|
3
|
+
*
|
|
4
|
+
* The single structured-event surface for the live safety envelope: the breaker
|
|
5
|
+
* opening/probing and frontier-freeze observability that a live provider path (or
|
|
6
|
+
* dashboard) consumes. It mirrors the VC0B replay emit seam (`replay/emit.ts`)
|
|
7
|
+
* and the VC0A eval observer: the same injected `(event, fields)` callback, the
|
|
8
|
+
* same non-fatal `safe()` best-effort wrapper, and the same per-call
|
|
9
|
+
* `VC0C_ENABLED()` gate. Absent emitter (or flag OFF) degrades to a no-op with
|
|
10
|
+
* byte-identical predecessor behavior (PREVENT-PI-004, structured `ts`+`event`).
|
|
11
|
+
*
|
|
12
|
+
* Carries exactly the three events the sprint names:
|
|
13
|
+
* - vector_cortex_breaker_opened (a breaker tripped to an OPEN state)
|
|
14
|
+
* - vector_cortex_probe_promoted (recovery probes advanced a state)
|
|
15
|
+
* - vector_cortex_frontier_frozen (authority outage froze derived frontiers)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { VC0C_ENABLED } from "../../config/vector-cortex.js";
|
|
19
|
+
|
|
20
|
+
export type ResilienceEventName =
|
|
21
|
+
| "vector_cortex_breaker_opened"
|
|
22
|
+
| "vector_cortex_probe_promoted"
|
|
23
|
+
| "vector_cortex_frontier_frozen";
|
|
24
|
+
|
|
25
|
+
/** Injected emit callback — same (event, fields) shape as the other seams. */
|
|
26
|
+
export type ResilienceEmitter = (event: string, fields: Record<string, unknown>) => void;
|
|
27
|
+
|
|
28
|
+
export interface BreakerOpenedFields {
|
|
29
|
+
readonly subsystem: string;
|
|
30
|
+
readonly fromState: string;
|
|
31
|
+
readonly toState: string;
|
|
32
|
+
readonly code: string;
|
|
33
|
+
readonly attempts: number;
|
|
34
|
+
readonly failures: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ProbePromotedFields {
|
|
38
|
+
readonly subsystem: string;
|
|
39
|
+
readonly fromState: string;
|
|
40
|
+
readonly toState: string;
|
|
41
|
+
readonly probeCount: number;
|
|
42
|
+
readonly retryAttempt: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FrontierFrozenFields {
|
|
46
|
+
readonly session: string;
|
|
47
|
+
readonly committedSeq: string;
|
|
48
|
+
readonly frozenHighWater: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Typed, best-effort reporter bound to the three resilience event names. */
|
|
52
|
+
export interface ResilienceReporter {
|
|
53
|
+
readonly breakerOpened: (fields: BreakerOpenedFields) => void;
|
|
54
|
+
readonly probePromoted: (fields: ProbePromotedFields) => void;
|
|
55
|
+
readonly frontierFrozen: (fields: FrontierFrozenFields) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function safe(fn: (event: ResilienceEventName, fields: Record<string, unknown>) => void): (
|
|
59
|
+
event: ResilienceEventName,
|
|
60
|
+
fields: Record<string, unknown>,
|
|
61
|
+
) => void {
|
|
62
|
+
return (event, fields) => {
|
|
63
|
+
try {
|
|
64
|
+
fn(event, fields);
|
|
65
|
+
} catch {
|
|
66
|
+
/* non-fatal observability — never break the agent loop */
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build a typed reporter over an injected emit callback. Emission is gated on
|
|
73
|
+
* `VC0C_ENABLED()` — flag OFF (or absent emitter) means zero resilience
|
|
74
|
+
* observability writes (mode-C predecessor parity). The flag is read per-call so
|
|
75
|
+
* a live toggle takes effect immediately.
|
|
76
|
+
*/
|
|
77
|
+
export function createResilienceReporter(emit?: ResilienceEmitter): ResilienceReporter {
|
|
78
|
+
const fire = safe((event, fields) => {
|
|
79
|
+
if (VC0C_ENABLED()) emit?.(event, fields);
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
breakerOpened(fields) {
|
|
83
|
+
fire("vector_cortex_breaker_opened", fields as unknown as Record<string, unknown>);
|
|
84
|
+
},
|
|
85
|
+
probePromoted(fields) {
|
|
86
|
+
fire("vector_cortex_probe_promoted", fields as unknown as Record<string, unknown>);
|
|
87
|
+
},
|
|
88
|
+
frontierFrozen(fields) {
|
|
89
|
+
fire("vector_cortex_frontier_frozen", fields as unknown as Record<string, unknown>);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Export the event names for consumers that match on them. */
|
|
95
|
+
export const RESILIENCE_EVENTS: readonly ResilienceEventName[] = [
|
|
96
|
+
"vector_cortex_breaker_opened",
|
|
97
|
+
"vector_cortex_probe_promoted",
|
|
98
|
+
"vector_cortex_frontier_frozen",
|
|
99
|
+
] as const;
|