pi-mega-compact 0.16.2 → 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/httpEmbedder.js +136 -1
- 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/httpEmbedder.ts +134 -1
- 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,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex-safety.ts — VC0C live safety envelope (VC0C, task 4 + 5).
|
|
3
|
+
*
|
|
4
|
+
* Owns the live triad selection BEFORE provider invocation: select mode A
|
|
5
|
+
* (optimized/learned), then independent mode B (deterministic local spool
|
|
6
|
+
* deriving directly from authority), then unchanged mode C (continuity, not
|
|
7
|
+
* semantic completeness — C uses only the exact current transcript and reports
|
|
8
|
+
* that limitation). A manual reset clears cooldown but NEVER evidence.
|
|
9
|
+
*
|
|
10
|
+
* This adapter composes the resilience seams (`resilience/breaker.ts`,
|
|
11
|
+
* `resilience/spool.ts`, `resilience/emit.ts`) into the health/reset API the
|
|
12
|
+
* dashboard owns (`GET /api/vector-cortex/health`, admin
|
|
13
|
+
* `POST /api/vector-cortex/breakers/reset`). It is the pi-runtime-adjacent shell;
|
|
14
|
+
* all durable state is owned by the src/ resilience modules. Non-fatal: any
|
|
15
|
+
* breaker/spool noise degrades to mode C (unchanged host transcript), and the
|
|
16
|
+
* agent loop is never broken.
|
|
17
|
+
*
|
|
18
|
+
* Local-only (PREVENT-PI-004); no `any` (PREVENT-011); no console.log — events
|
|
19
|
+
* go through the injected resilience reporter / Logger.
|
|
20
|
+
*
|
|
21
|
+
* LIVENESS HONESTY (VC0C-Q01/Q06): the breaker composed here is PER-PROCESS /
|
|
22
|
+
* IN-MEMORY and EPHEMERAL — `createVectorCortexSafety` builds a fresh instance on
|
|
23
|
+
* every dashboard request (GET /health, POST /reset). There is NO persistent
|
|
24
|
+
* breaker runtime and NO live producer wiring (the real circuit-breaker that runs
|
|
25
|
+
* around the actual compaction loop is DEFERRED to the producer-wiring sprint,
|
|
26
|
+
* carried VC0B-I08 -> VC0D). Consequently GET /health always reflects a brand-new
|
|
27
|
+
* CLOSED_A instance and POST /reset mutates an instance that vanishes when the
|
|
28
|
+
* request returns: the endpoints are ILLUSTRATIVE until VC0D wires a persistent
|
|
29
|
+
* instance. The health payload therefore carries `stateSource:"ephemeral"` so the
|
|
30
|
+
* dashboard/README never present it as a live breaker. Frontier-frozen / spool-lag
|
|
31
|
+
* are owned by the DURABLE spool but have no live per-session handle here, so they
|
|
32
|
+
* are surfaced as static/non-live until VC0D (with authorityOutage mirrored from
|
|
33
|
+
* the host context when supplied).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { join } from "node:path";
|
|
37
|
+
import { createResilienceReporter, type ResilienceReporter } from "../../src/vector-cortex/resilience/emit.js";
|
|
38
|
+
import { createBreaker } from "../../src/vector-cortex/resilience/breaker.js";
|
|
39
|
+
import { createSpool, type Spool } from "../../src/vector-cortex/resilience/spool.js";
|
|
40
|
+
import type { BreakerRecord, Mode, TriadResult } from "../../src/vector-cortex/resilience/types.js";
|
|
41
|
+
import { BREAKER_WINDOW_MS, VC0C_ENABLED } from "../../src/config/vector-cortex.js";
|
|
42
|
+
|
|
43
|
+
/** Daily/health context the safety adapter needs from its host. */
|
|
44
|
+
export interface VectorCortexSafetyContext {
|
|
45
|
+
/** State directory hosting the durable breaker/spool state. */
|
|
46
|
+
readonly stateDir: string;
|
|
47
|
+
/** Monotonic clock for windows/cooldowns (fake-clock testable). */
|
|
48
|
+
readonly now?: () => number;
|
|
49
|
+
/** Wall-clock supplier for record timestamps (default ISO now). */
|
|
50
|
+
readonly wallNow?: () => string;
|
|
51
|
+
/** True while the mode-A authority (SQLite ledger) is in OUTAGE. */
|
|
52
|
+
readonly authorityOutage?: () => boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Health-card aggregate (dashboard contract). */
|
|
56
|
+
export interface VectorCortexHealthSummary {
|
|
57
|
+
readonly enabled: boolean;
|
|
58
|
+
readonly mode: Mode;
|
|
59
|
+
readonly state: string;
|
|
60
|
+
readonly subsystem: string;
|
|
61
|
+
readonly sinceMs: number;
|
|
62
|
+
readonly reason?: string;
|
|
63
|
+
readonly windowMs: number;
|
|
64
|
+
readonly probeCount: number;
|
|
65
|
+
readonly backoffDelayMs: number;
|
|
66
|
+
readonly frontierFrozen: boolean;
|
|
67
|
+
readonly authorityOutage: boolean;
|
|
68
|
+
readonly spoolLag: number;
|
|
69
|
+
readonly attempts: number;
|
|
70
|
+
readonly failures: number;
|
|
71
|
+
readonly p95Ms: number;
|
|
72
|
+
readonly failureRate: number;
|
|
73
|
+
readonly updatedAt: string;
|
|
74
|
+
/** "ephemeral" => per-process/in-memory breaker; "live" => persistent (VC0D). */
|
|
75
|
+
readonly stateSource: "ephemeral" | "live";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TriadProvider<T> {
|
|
79
|
+
readonly session: string;
|
|
80
|
+
readonly subsystem: string;
|
|
81
|
+
readonly inputDigest: string;
|
|
82
|
+
/** Mode A — optimized/learned. */
|
|
83
|
+
readonly runA: () => T;
|
|
84
|
+
/** Mode B — deterministic local spool, derives directly from authority. */
|
|
85
|
+
readonly runB: () => T;
|
|
86
|
+
/** Mode C — unchanged current transcript (continuity, NOT completeness). */
|
|
87
|
+
readonly runC: () => T;
|
|
88
|
+
readonly validate: (v: T) => boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface VectorCortexSafety {
|
|
92
|
+
/**
|
|
93
|
+
* Select A, then independent B, then unchanged C BEFORE provider invocation.
|
|
94
|
+
* Demotes on live failure; the selected mode's output is the only thing served
|
|
95
|
+
* (probe output is never served).
|
|
96
|
+
*/
|
|
97
|
+
select<T>(provider: TriadProvider<T>): TriadResult<T>;
|
|
98
|
+
/** Reader-only aggregate for GET /api/vector-cortex/health. */
|
|
99
|
+
health(): VectorCortexHealthSummary;
|
|
100
|
+
/** The mode-B durable spool (append before provider; drain on recovery). */
|
|
101
|
+
readonly spool: Spool;
|
|
102
|
+
/** Admin reset: clears cooldown, never evidence. Returns the new record. */
|
|
103
|
+
reset(subsystem: string): BreakerRecord;
|
|
104
|
+
/** Manual halt with a required reason (authority/digest/causal corruption). */
|
|
105
|
+
halt(reason: string): BreakerRecord;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Build the live safety envelope, wiring the emit seam into the breaker. */
|
|
109
|
+
export function createVectorCortexSafety(
|
|
110
|
+
ctx: VectorCortexSafetyContext,
|
|
111
|
+
emit?: (event: string, fields: Record<string, unknown>) => void,
|
|
112
|
+
): VectorCortexSafety {
|
|
113
|
+
const reporter: ResilienceReporter = createResilienceReporter(emit);
|
|
114
|
+
const wallNow = ctx.wallNow ?? (() => new Date().toISOString());
|
|
115
|
+
const breaker = createBreaker({ now: ctx.now, reporter, wallNow });
|
|
116
|
+
const spool = createSpool({
|
|
117
|
+
dir: join(ctx.stateDir, "spool"),
|
|
118
|
+
authorityOutage: ctx.authorityOutage,
|
|
119
|
+
// Wire the resilience emit seam (incl. vector_cortex_frontier_frozen) into
|
|
120
|
+
// the mode-B spool so a real authority-frontier freeze is observable.
|
|
121
|
+
reporter,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
spool,
|
|
126
|
+
select<T>(provider: TriadProvider<T>): TriadResult<T> {
|
|
127
|
+
const { subsystem, inputDigest, runA, runB, runC, validate } = provider;
|
|
128
|
+
// Thin delegation to the breaker: it selects the highest healthy mode
|
|
129
|
+
// (A, else independent B, else unchanged C) BEFORE provider invocation and
|
|
130
|
+
// records the attempt/trip. Across invocations A-failure demotes to B and
|
|
131
|
+
// B-failure demotes to C (continuity — unchanged transcript).
|
|
132
|
+
return breaker.execute(
|
|
133
|
+
subsystem,
|
|
134
|
+
inputDigest,
|
|
135
|
+
{ A: runA, B: runB, C: runC },
|
|
136
|
+
validate,
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
health(): VectorCortexHealthSummary {
|
|
141
|
+
const rec = breaker.snapshot("provider");
|
|
142
|
+
const enabled = VC0C_ENABLED();
|
|
143
|
+
// LIVENESS HONESTY (VC0C-Q01/Q06): this breaker is created fresh per request
|
|
144
|
+
// and is ephemeral — declare it explicitly so the dashboard never presents it
|
|
145
|
+
// as a live breaker. `authorityOutage` is mirrored from the host context when
|
|
146
|
+
// supplied (otherwise false). `frontierFrozen` (rec.frozenFrontier) and
|
|
147
|
+
// `spoolLag` are owned by the DURABLE spool, which has no live per-session
|
|
148
|
+
// handle here — they stay static/no-op until VC0D wires a persistent breaker
|
|
149
|
+
// instance; stateSource:"ephemeral" is the signal that they are not live.
|
|
150
|
+
return {
|
|
151
|
+
enabled,
|
|
152
|
+
mode: rec.state === "MANUAL_HALT" ? "C" : rec.state === "CLOSED_A" ? "A" : "B",
|
|
153
|
+
state: rec.state,
|
|
154
|
+
subsystem: "provider",
|
|
155
|
+
sinceMs: rec.transitionedAtMs,
|
|
156
|
+
reason: rec.manualReason,
|
|
157
|
+
windowMs: BREAKER_WINDOW_MS,
|
|
158
|
+
probeCount: rec.probeCount,
|
|
159
|
+
backoffDelayMs: rec.retryDelayMs,
|
|
160
|
+
frontierFrozen: rec.frozenFrontier,
|
|
161
|
+
authorityOutage: ctx.authorityOutage?.() ?? false,
|
|
162
|
+
spoolLag: 0,
|
|
163
|
+
attempts: rec.attempts,
|
|
164
|
+
failures: rec.failures,
|
|
165
|
+
p95Ms: rec.p95Ms,
|
|
166
|
+
failureRate: rec.failureRate,
|
|
167
|
+
updatedAt: wallNow(),
|
|
168
|
+
stateSource: "ephemeral",
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
reset(subsystem: string): BreakerRecord {
|
|
173
|
+
return breaker.reset(subsystem);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
halt(reason: string): BreakerRecord {
|
|
177
|
+
return breaker.manualHalt(reason);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"clean": "rm -rf dist",
|
|
48
48
|
"prebuild": "npm run clean",
|
|
49
49
|
"build": "tsc -p tsconfig.json",
|
|
50
|
+
"postbuild": "node scripts/vector-cortex-publish-acceptance.mjs",
|
|
50
51
|
"build:dashboard": "cd extensions/dashboard-client && npx vite build",
|
|
51
52
|
"test:dashboard-smoke": "node scripts/dashboard-tab-smoke.mjs",
|
|
52
53
|
"lint": "tsc --noEmit && node scripts/guardrails-scan.mjs && node scripts/semantic-scan.mjs",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config/vector-cortex.ts — vector-cortex feature flags + breaker/triad constants.
|
|
3
|
+
*
|
|
4
|
+
* Every sprint ships one positive `MEGACOMPACT_<SPRINT>` flag, default ON and
|
|
5
|
+
* `=0`/`_DISABLED` off. Flag-OFF is byte-identical to the predecessor sprint's
|
|
6
|
+
* behavior (for VC0A: mode C — observer absent, zero evaluation writes).
|
|
7
|
+
*
|
|
8
|
+
* The breaker/triad constants (TRIAD_RESILIENCE.md) live here so VC0C consumes
|
|
9
|
+
* them without re-declaring the ownership boundary. Pi-agnostic, dependency-free.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Positive sprint flag: `=0` or `_DISABLED=true` disables (default ON). */
|
|
13
|
+
function sprintFlag(name: string): boolean {
|
|
14
|
+
const v = process.env[name];
|
|
15
|
+
if (v === "0" || v === "false") return false;
|
|
16
|
+
const disabled = process.env[name + "_DISABLED"];
|
|
17
|
+
if (disabled === "true" || disabled === "1") return false;
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** VC0A — baseline observability (MetricEventV1 / AnnotationV1). Default ON. */
|
|
22
|
+
export const VC0A_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC0A");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* VC0B — replay correctness (ReplayCutV2 / ReplayReportV2, M3 effective-cut-v2).
|
|
26
|
+
* Default ON. `MEGACOMPACT_VC0B=0` disables and is byte-identical to the
|
|
27
|
+
* predecessor (legacy capped-replay behavior preserved; the v2 cut/replay is
|
|
28
|
+
* only consulted on the vector-cortex path).
|
|
29
|
+
*/
|
|
30
|
+
export const VC0B_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC0B");
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* VC1A — canonical byte events (EventV2 / EventCodec).
|
|
34
|
+
* Default ON. `MEGACOMPACT_VC1A=0` disables and is byte-identical to the
|
|
35
|
+
* predecessor (mode C: ledger absent, current transcript codec unchanged).
|
|
36
|
+
* The single real consumer is the ledger emit seam (`ledger/emit.ts`): flag OFF
|
|
37
|
+
* gates zero observability writes.
|
|
38
|
+
*/
|
|
39
|
+
export const VC1A_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC1A");
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* VC1B — occurrence ledger + tool identity + compat journal (LedgerReader/
|
|
43
|
+
* Writer/Admin, CompatJournalV1, M2 occurrence-v2 migration).
|
|
44
|
+
* Default ON. `MEGACOMPACT_VC1B=0` disables and is byte-identical to the
|
|
45
|
+
* predecessor (mode C: the neutral occurrence ledger is not written, no
|
|
46
|
+
* journal rows, zero `vector_cortex_occurrence_appended` emissions). The real
|
|
47
|
+
* consumers are the ledger write integration seam and the compat-journal
|
|
48
|
+
* switch seam.
|
|
49
|
+
*/
|
|
50
|
+
export const VC1B_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC1B");
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* VC0C — live safety envelope (TriadResult / Breaker / KillDecision + durable
|
|
54
|
+
* spool). Default ON. `MEGACOMPACT_VC0C=0` disables and is byte-identical to
|
|
55
|
+
* the predecessor (mode C: selected before provider invocation, unchanged host
|
|
56
|
+
* transcript, breaker/spool idle and emitting nothing). The single real
|
|
57
|
+
* consumer is the resilience emit seam + the safety adapter's triad selection.
|
|
58
|
+
*/
|
|
59
|
+
export const VC0C_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC0C");
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Breaker state machine constants (TRIAD_RESILIENCE.md §breaker).
|
|
63
|
+
// Rolled numbers for one 60s window; VC0C consumes these at its breaker seam.
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/** Rolling eligibility window (milliseconds). */
|
|
67
|
+
export const BREAKER_WINDOW_MS = 60_000;
|
|
68
|
+
/** Minimum attempts before a breaker may trip or promote. */
|
|
69
|
+
export const BREAKER_MIN_ATTEMPTS = 20;
|
|
70
|
+
/** Performance trip: ≥ this many failures in window, or ≥ this fraction. */
|
|
71
|
+
export const BREAKER_PERF_FAILURES = 5;
|
|
72
|
+
export const BREAKER_PERF_FAILURE_RATE = 0.1;
|
|
73
|
+
/** Correctness trip trips on the first correctness failure. */
|
|
74
|
+
export const BREAKER_CORRECTNESS_FAILURES = 1;
|
|
75
|
+
/** Cooldown before an open breaker may probe (milliseconds). */
|
|
76
|
+
export const BREAKER_COOLDOWN_MS = 30_000;
|
|
77
|
+
/** Consecutive successful probes required to advance a state. */
|
|
78
|
+
export const BREAKER_PROBE_COUNT = 3;
|
|
79
|
+
/** Exponential retry base: 30s * 2^attempt, capped, ±10% jitter. */
|
|
80
|
+
export const BREAKER_RETRY_BASE_MS = 30_000;
|
|
81
|
+
export const BREAKER_RETRY_CAP_MS = 15 * 60_000;
|
|
82
|
+
export const BREAKER_RETRY_JITTER = 0.1;
|
|
83
|
+
/** Promotion hysteresis: failure rate must be < this and p95 within budget. */
|
|
84
|
+
export const BREAKER_HYSTERESIS_FAILURE_RATE = 0.02;
|
|
85
|
+
export const BREAKER_HYSTERESIS_BUDGET_P95_MS = 50;
|
|
86
|
+
/** Minimum healthy residence before a further promotion (milliseconds). */
|
|
87
|
+
export const BREAKER_MIN_HEALTHY_RESIDENCE_MS = 5 * 60_000;
|
package/src/config.ts
CHANGED
|
@@ -143,3 +143,30 @@ export const RAG_HYDE_ENABLED = (): boolean =>
|
|
|
143
143
|
|
|
144
144
|
/** Spec 1: vbrainstorm visual design migration for the dashboard. */
|
|
145
145
|
export const NEW_UI = (): boolean => ragEnabled("MEGACOMPACT_NEW_UI");
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Vector-cortex flags + breaker constants (VC0A+). Positive sprint flags,
|
|
149
|
+
// default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
|
|
150
|
+
// so root consumers share one source of truth.
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
VC0A_ENABLED,
|
|
155
|
+
VC0B_ENABLED,
|
|
156
|
+
VC1A_ENABLED,
|
|
157
|
+
VC0C_ENABLED,
|
|
158
|
+
VC1B_ENABLED,
|
|
159
|
+
BREAKER_WINDOW_MS,
|
|
160
|
+
BREAKER_MIN_ATTEMPTS,
|
|
161
|
+
BREAKER_PERF_FAILURES,
|
|
162
|
+
BREAKER_PERF_FAILURE_RATE,
|
|
163
|
+
BREAKER_CORRECTNESS_FAILURES,
|
|
164
|
+
BREAKER_COOLDOWN_MS,
|
|
165
|
+
BREAKER_PROBE_COUNT,
|
|
166
|
+
BREAKER_RETRY_BASE_MS,
|
|
167
|
+
BREAKER_RETRY_CAP_MS,
|
|
168
|
+
BREAKER_RETRY_JITTER,
|
|
169
|
+
BREAKER_HYSTERESIS_FAILURE_RATE,
|
|
170
|
+
BREAKER_HYSTERESIS_BUDGET_P95_MS,
|
|
171
|
+
BREAKER_MIN_HEALTHY_RESIDENCE_MS,
|
|
172
|
+
} from "./config/vector-cortex.js";
|
package/src/httpEmbedder.ts
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import type { Embedder, Vector } from "./embedder.js";
|
|
29
|
-
import { l2Normalize } from "./embedder.js";
|
|
29
|
+
import { l2Normalize, TrigramEmbedder } from "./embedder.js";
|
|
30
|
+
import { Logger } from "./log.js";
|
|
30
31
|
import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: localhost-only user-spawned embedding server (BYO backend, never remote)
|
|
31
32
|
import { isIP } from "node:net"; // guardrails-allow PREVENT-PI-004: localhost-only loopback address validation in BYO embedding server
|
|
32
33
|
|
|
@@ -40,6 +41,94 @@ export interface HttpEmbedderOptions {
|
|
|
40
41
|
dim?: number;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// ── Oversized-input chunking + graceful fallback (BowTiedDevil 500 report) ────
|
|
45
|
+
//
|
|
46
|
+
// The embedding server enforces a physical batch size (llama.cpp n_ctx_slot,
|
|
47
|
+
// commonly 2048 tokens); a single over-large prompt is rejected with HTTP 500
|
|
48
|
+
// ("input is too large to process"). Previously embed() sent the WHOLE region
|
|
49
|
+
// as one prompt and threw on the 500, propagating an unhandled rejection up
|
|
50
|
+
// through VectorStore.addCheckpoint → engine and crashing the checkpoint write.
|
|
51
|
+
//
|
|
52
|
+
// Two layered defenses now live inside embed():
|
|
53
|
+
// 1. CHUNKING — text whose estimated token count exceeds the configured batch
|
|
54
|
+
// limit is split into <=limit chunks (paragraph/sentence/word boundaries,
|
|
55
|
+
// never mid-word when avoidable), each chunk embedded, and the per-chunk
|
|
56
|
+
// vectors mean-pooled (weighted by chunk tokens) + L2-renormalized. This
|
|
57
|
+
// changes the vector for oversized docs — which previously crashed — and
|
|
58
|
+
// is the standard pooling approximation for long inputs.
|
|
59
|
+
// 2. FALLBACK — if the server is unreachable / returns an error / returns an
|
|
60
|
+
// unparseable body, embed() catches and falls back to the local
|
|
61
|
+
// TrigramEmbedder (logging one structured warn, never the text), so a
|
|
62
|
+
// server outage degrades semantic dedup instead of crashing the agent loop
|
|
63
|
+
// (repo non-fatal-store / fails-closed convention).
|
|
64
|
+
//
|
|
65
|
+
// Token count is ESTIMATED, not tokenized: chars / MEGACOMPACT_EMBEDDING_CHARS_PER_TOKEN
|
|
66
|
+
// (default 4, a conservative English/code average). Both knobs are env-tunable.
|
|
67
|
+
|
|
68
|
+
/** Default physical batch the typical local server accepts (llama.cpp n_ctx_slot). */
|
|
69
|
+
const DEFAULT_BATCH_TOKENS = 2048;
|
|
70
|
+
/** Conservative chars-per-token average for English + code. */
|
|
71
|
+
const DEFAULT_CHARS_PER_TOKEN = 4;
|
|
72
|
+
|
|
73
|
+
function envInt(name: string, fallback: number): number {
|
|
74
|
+
const raw = process.env[name];
|
|
75
|
+
if (raw === undefined) return fallback;
|
|
76
|
+
const n = Number(raw);
|
|
77
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Estimated token count of `text` (chars / charsPerToken, rounded up). Exported for tests. */
|
|
81
|
+
export function estimateTokens(text: string, charsPerToken: number): number {
|
|
82
|
+
return Math.ceil(text.length / charsPerToken);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Split `text` into chunks each estimated <= maxTokens. Prefers paragraph, then
|
|
87
|
+
* sentence, then word boundaries; falls back to a hard slice only when a single
|
|
88
|
+
* word/run still exceeds the limit (never splits a word unless unavoidable).
|
|
89
|
+
*/
|
|
90
|
+
export function chunkText(text: string, maxTokens: number, charsPerToken: number): string[] {
|
|
91
|
+
const maxChars = maxTokens * charsPerToken;
|
|
92
|
+
if (text.length <= maxChars) return [text];
|
|
93
|
+
const chunks: string[] = [];
|
|
94
|
+
let start = 0;
|
|
95
|
+
while (start < text.length) {
|
|
96
|
+
let end = Math.min(start + maxChars, text.length);
|
|
97
|
+
if (end < text.length) {
|
|
98
|
+
// Back off to the last nice boundary within the window: paragraph, then
|
|
99
|
+
// sentence, then whitespace. Require the boundary in the back half so we
|
|
100
|
+
// don't pathologically emit tiny chunks.
|
|
101
|
+
const windowStart = start + Math.floor(maxChars / 2);
|
|
102
|
+
const para = text.lastIndexOf("\n\n", end);
|
|
103
|
+
const sentence = text.lastIndexOf(". ", end);
|
|
104
|
+
const space = text.lastIndexOf(" ", end);
|
|
105
|
+
if (para >= windowStart) end = para + 2;
|
|
106
|
+
else if (sentence >= windowStart) end = sentence + 2;
|
|
107
|
+
else if (space >= windowStart) end = space + 1;
|
|
108
|
+
// else: no good boundary in the back half — hard-slice at maxChars.
|
|
109
|
+
}
|
|
110
|
+
const piece = text.slice(start, end);
|
|
111
|
+
if (piece.trim().length > 0) chunks.push(piece);
|
|
112
|
+
start = end;
|
|
113
|
+
}
|
|
114
|
+
return chunks;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Weighted mean-pool of equal-dim vectors (weights = chunk token estimates), then L2-renormalize. */
|
|
118
|
+
export function meanPool(vectors: readonly Vector[], weights: readonly number[]): Vector {
|
|
119
|
+
const dim = vectors[0]?.length ?? 0;
|
|
120
|
+
const acc = new Array<number>(dim).fill(0);
|
|
121
|
+
let total = 0;
|
|
122
|
+
for (let i = 0; i < vectors.length; i++) {
|
|
123
|
+
const w = weights[i] ?? 0;
|
|
124
|
+
total += w;
|
|
125
|
+
const v = vectors[i];
|
|
126
|
+
for (let d = 0; d < dim; d++) acc[d] += v[d] * w;
|
|
127
|
+
}
|
|
128
|
+
if (total > 0) for (let d = 0; d < dim; d++) acc[d] /= total;
|
|
129
|
+
return l2Normalize(acc);
|
|
130
|
+
}
|
|
131
|
+
|
|
43
132
|
// Inline worker script: resolves a hostname via dns.lookup in a child process
|
|
44
133
|
// (dns.lookup is callback-async; the child has its own event loop). Used to
|
|
45
134
|
// verify that a hostname in the embedding URL resolves to loopback ONLY.
|
|
@@ -217,12 +306,20 @@ export class HttpEmbedder implements Embedder {
|
|
|
217
306
|
private readonly apiKey?: string;
|
|
218
307
|
private readonly headers: Record<string, string>;
|
|
219
308
|
private resolvedDim: number;
|
|
309
|
+
private readonly batchTokens: number;
|
|
310
|
+
private readonly charsPerToken: number;
|
|
311
|
+
private readonly fallback: TrigramEmbedder;
|
|
312
|
+
private readonly logger: Logger;
|
|
220
313
|
|
|
221
314
|
constructor(opts: HttpEmbedderOptions) {
|
|
222
315
|
this.url = opts.url;
|
|
223
316
|
this.apiKey = opts.apiKey;
|
|
224
317
|
this.headers = opts.headers ?? {};
|
|
225
318
|
this.resolvedDim = opts.dim ?? 0; // resolved after the first embed
|
|
319
|
+
this.batchTokens = envInt("MEGACOMPACT_EMBEDDING_BATCH_TOKENS", DEFAULT_BATCH_TOKENS);
|
|
320
|
+
this.charsPerToken = envInt("MEGACOMPACT_EMBEDDING_CHARS_PER_TOKEN", DEFAULT_CHARS_PER_TOKEN);
|
|
321
|
+
this.fallback = new TrigramEmbedder();
|
|
322
|
+
this.logger = new Logger();
|
|
226
323
|
}
|
|
227
324
|
|
|
228
325
|
get dim(): number {
|
|
@@ -235,7 +332,43 @@ export class HttpEmbedder implements Embedder {
|
|
|
235
332
|
return new URL("/api/chat", this.url).href;
|
|
236
333
|
}
|
|
237
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Embed `text`. Oversized inputs are chunked (each chunk <= the configured
|
|
337
|
+
* server batch) and mean-pooled; a server failure falls back to the local
|
|
338
|
+
* TrigramEmbedder. NEVER throws — a misbehaving server must degrade semantic
|
|
339
|
+
* dedup, not crash the checkpoint write path (VectorStore.addCheckpoint has
|
|
340
|
+
* no try/catch around embed()).
|
|
341
|
+
*/
|
|
238
342
|
embed(text: string): Vector {
|
|
343
|
+
const est = estimateTokens(text, this.charsPerToken);
|
|
344
|
+
const chunks =
|
|
345
|
+
est > this.batchTokens ? chunkText(text, this.batchTokens, this.charsPerToken) : [text];
|
|
346
|
+
try {
|
|
347
|
+
if (chunks.length === 1) {
|
|
348
|
+
return this.embedOne(chunks[0]);
|
|
349
|
+
}
|
|
350
|
+
const vectors: Vector[] = [];
|
|
351
|
+
const weights: number[] = [];
|
|
352
|
+
for (const c of chunks) {
|
|
353
|
+
vectors.push(this.embedOne(c));
|
|
354
|
+
weights.push(estimateTokens(c, this.charsPerToken));
|
|
355
|
+
}
|
|
356
|
+
return meanPool(vectors, weights);
|
|
357
|
+
} catch (e) {
|
|
358
|
+
// Graceful fallback — degrade to the local embedder rather than crash.
|
|
359
|
+
// Never log the text itself (privacy / PREVENT-PI-004).
|
|
360
|
+
this.logger.warn("embedder_http_fallback", {
|
|
361
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
362
|
+
chunks: chunks.length,
|
|
363
|
+
chars: text.length,
|
|
364
|
+
estTokens: est,
|
|
365
|
+
});
|
|
366
|
+
return this.fallback.embed(text);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Single request/response round-trip for one chunk. Throws on any failure. */
|
|
371
|
+
private embedOne(text: string): Vector {
|
|
239
372
|
const ollama = isOllamaEndpoint(this.url);
|
|
240
373
|
const body = ollama
|
|
241
374
|
? JSON.stringify({ model: process.env.MEGACOMPACT_OLLAMA_MODEL || "nomic-embed-text", prompt: text })
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/eval/annotations.ts — redaction metadata + JSONL serialization.
|
|
3
|
+
*
|
|
4
|
+
* Payload bytes, prompts, and exact ledger text are NEVER serialized. Before a
|
|
5
|
+
* JSONL line is written, each such field is replaced by a SHA-256 digest + byte
|
|
6
|
+
* count + redaction kind (EVAL-REDACT-002: prompt bytes never appear in JSONL).
|
|
7
|
+
* The redaction journal is itself the AnnotationV1 output.
|
|
8
|
+
*
|
|
9
|
+
* Uses node:crypto for the digest (pure local math, PREVENT-PI-004 safe).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import type { AnnotationV1 } from "./types.js";
|
|
14
|
+
|
|
15
|
+
export type RedactKind = "payload" | "prompt" | "ledger";
|
|
16
|
+
|
|
17
|
+
/** A raw content field awaiting redaction before serialization. */
|
|
18
|
+
export interface RawContent {
|
|
19
|
+
/** Field name in the original annotation. */
|
|
20
|
+
readonly field: string;
|
|
21
|
+
/** Kind of content — determines the redaction reason. */
|
|
22
|
+
readonly kind: RedactKind;
|
|
23
|
+
/** Original bytes; digested, never serialized. */
|
|
24
|
+
readonly bytes: Uint8Array;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Canonical serialization of an annotation: digest/count metadata only. */
|
|
28
|
+
export interface SerializedAnnotation {
|
|
29
|
+
readonly jsonl: string;
|
|
30
|
+
readonly annotation: AnnotationV1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** SHA-256 over bytes, unpadded base64 (conformance canonical binary form). */
|
|
34
|
+
export function digestBytes(bytes: Uint8Array): string {
|
|
35
|
+
return createHash("sha256").update(bytes).digest("base64");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Replace every raw content field with its digest/count metadata. */
|
|
39
|
+
export function redactAnnotation(
|
|
40
|
+
itemId: string,
|
|
41
|
+
contents: ReadonlyArray<RawContent>,
|
|
42
|
+
): AnnotationV1 {
|
|
43
|
+
const redactions = contents.map((c) => ({
|
|
44
|
+
field: c.field,
|
|
45
|
+
digest: digestBytes(c.bytes),
|
|
46
|
+
bytes: c.bytes.byteLength,
|
|
47
|
+
kind: c.kind,
|
|
48
|
+
}));
|
|
49
|
+
return {
|
|
50
|
+
itemId,
|
|
51
|
+
redactions,
|
|
52
|
+
redactedCount: redactions.length,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Serialize one set of raw contents to a single redacted JSONL line. Returns
|
|
58
|
+
* the annotation with its digest metadata and the JSONL text; the raw bytes
|
|
59
|
+
* never appear in the returned string.
|
|
60
|
+
*/
|
|
61
|
+
export function serializeRedactedJsonl(
|
|
62
|
+
itemId: string,
|
|
63
|
+
contents: ReadonlyArray<RawContent>,
|
|
64
|
+
): SerializedAnnotation {
|
|
65
|
+
const annotation = redactAnnotation(itemId, contents);
|
|
66
|
+
return {
|
|
67
|
+
annotation,
|
|
68
|
+
jsonl: `${JSON.stringify(annotation)}\n`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/eval/metrics.ts — canonical metric ordering + fixed histogram.
|
|
3
|
+
*
|
|
4
|
+
* 1. Stable sort rows by `(session, seq, event)` — session string byte order,
|
|
5
|
+
* then integer seq ascending, then event-name byte order as tiebreak
|
|
6
|
+
* (EVAL-ORDER-003: equal seq rows use event-name order).
|
|
7
|
+
* 2. Bucket latency values at exactly 1/5/10/25/50/100/250ms with inclusive
|
|
8
|
+
* boundaries (EVAL-BUCKET-001); values past 250ms kept in a separate
|
|
9
|
+
* overflow bucket. Histogram totals are permutation-stable.
|
|
10
|
+
*
|
|
11
|
+
* Pure/no side effects (PREVENT-PI-004).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { LATENCY_BUCKETS } from "./types.js";
|
|
15
|
+
import type { MetricEventV1, EvalReject } from "./types.js";
|
|
16
|
+
|
|
17
|
+
/** Inclusive-boundary bucket edges, with a sentinel past the largest edge. */
|
|
18
|
+
const EDGES: readonly number[] = LATENCY_BUCKETS;
|
|
19
|
+
const OVERFLOW_INDEX = EDGES.length;
|
|
20
|
+
|
|
21
|
+
/** Histogram cell counts aligned to EDGES, plus a trailing overflow cell. */
|
|
22
|
+
export function emptyHistogram(): number[] {
|
|
23
|
+
return new Array<number>(EDGES.length + 1).fill(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Per-session histogram of `value` (when the sample is in `unit`), plus a
|
|
28
|
+
* non-latency count kept separate so totals are permutation-stable.
|
|
29
|
+
*/
|
|
30
|
+
export interface LatencyHistogram {
|
|
31
|
+
/** Cells: one per bucket edge, then overflow. Sum == counted latency samples. */
|
|
32
|
+
cells: number[];
|
|
33
|
+
/** Bucket label for each cell (overflow label is "250+" passed via >250). */
|
|
34
|
+
edges: readonly number[];
|
|
35
|
+
/** Number of latency samples that landed past the largest edge. */
|
|
36
|
+
overflow: number;
|
|
37
|
+
/** Total latency samples bucketed (cells sum). */
|
|
38
|
+
total: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface MetricsResult {
|
|
42
|
+
/** Rows in canonical `(session, seq, event)` order. */
|
|
43
|
+
readonly rows: readonly MetricEventV1[];
|
|
44
|
+
/** Per-session latency histograms (mode A observed). */
|
|
45
|
+
readonly histogram: LatencyHistogram;
|
|
46
|
+
/** Rejections encountered while validating streaming input. */
|
|
47
|
+
readonly rejects: readonly EvalReject[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True when a sample carries a latency (`unit === "ms"`) datum. */
|
|
51
|
+
export function isLatencySample(m: MetricEventV1): boolean {
|
|
52
|
+
return m.unit === "ms";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Bucket a latency ms value into a cell index (0..EDGES.length, last=overflow). */
|
|
56
|
+
export function bucketIndex(value: number): number {
|
|
57
|
+
if (!Number.isFinite(value) || value < 0) return OVERFLOW_INDEX;
|
|
58
|
+
for (let i = 0; i < EDGES.length; i++) {
|
|
59
|
+
if (value <= EDGES[i]) return i; // inclusive upper boundary
|
|
60
|
+
}
|
|
61
|
+
return OVERFLOW_INDEX;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Bucket latency sample values across all rows into the fixed histogram.
|
|
66
|
+
* Overflow is kept separate (counted in `overflow` and the trailing cell).
|
|
67
|
+
*/
|
|
68
|
+
export function bucketHistogram(rows: readonly MetricEventV1[]): LatencyHistogram {
|
|
69
|
+
const cells = emptyHistogram();
|
|
70
|
+
let overflow = 0;
|
|
71
|
+
for (const m of rows) {
|
|
72
|
+
if (!isLatencySample(m)) continue;
|
|
73
|
+
const idx = bucketIndex(m.value);
|
|
74
|
+
cells[idx] += 1;
|
|
75
|
+
if (idx === EDGES.length) overflow += 1;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
cells,
|
|
79
|
+
edges: [...EDGES],
|
|
80
|
+
overflow,
|
|
81
|
+
total: cells.reduce((a, b) => a + b, 0),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Stable sort MetricEventV1 rows by `(session, seq, event)`:
|
|
87
|
+
* - session: UTF-8 / JS string byte order;
|
|
88
|
+
* - seq: ascending numeric;
|
|
89
|
+
* - event: ascending string byte order (tiebreak for equal seq).
|
|
90
|
+
* Equal content stays separate occurrences (never deduplicated here).
|
|
91
|
+
*/
|
|
92
|
+
export function sortCanonical(rows: readonly MetricEventV1[]): MetricEventV1[] {
|
|
93
|
+
const out = [...rows];
|
|
94
|
+
out.sort((a, b) => {
|
|
95
|
+
const s = a.session < b.session ? -1 : a.session > b.session ? 1 : 0;
|
|
96
|
+
if (s !== 0) return s;
|
|
97
|
+
if (a.seq !== b.seq) return a.seq - b.seq;
|
|
98
|
+
return a.event < b.event ? -1 : a.event > b.event ? 1 : 0;
|
|
99
|
+
});
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Assisted sort plus histogram, returning both under one result.
|
|
105
|
+
* The observer (mode A) calls this before JSONL serialization.
|
|
106
|
+
*/
|
|
107
|
+
export function buildMetrics(rows: readonly MetricEventV1[]): MetricsResult {
|
|
108
|
+
return {
|
|
109
|
+
rows: sortCanonical(rows),
|
|
110
|
+
histogram: bucketHistogram(rows),
|
|
111
|
+
rejects: [],
|
|
112
|
+
};
|
|
113
|
+
}
|