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,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/migrations/effective-cut-v2.ts — M3 copy/validate/switch
|
|
3
|
+
* migration (VC0B).
|
|
4
|
+
*
|
|
5
|
+
* Freezes the v2 effective replay cut: `min(boundarySafeSeq, committedSeq,
|
|
6
|
+
* capturedHighWater)` with pair retreat and anchor floor. The migration copies
|
|
7
|
+
* the candidate into the staged slot, validates the STAGED pointer against the
|
|
8
|
+
* minima and pair/anchor invariants (plus copy-match), and ONLY THEN atomically
|
|
9
|
+
* switches the active pointer. The three phases are exposed separately so a
|
|
10
|
+
* crash between validate and switch retains the OLD pointer and a restart
|
|
11
|
+
* resumes idempotently (the unique failure-injection contract).
|
|
12
|
+
*
|
|
13
|
+
* This is an internal developer seam: no dashboard/API change is necessary. It
|
|
14
|
+
* operates against the EXISTING host state via an injected M3Host interface —
|
|
15
|
+
* the future v2 compat-journal is owned by VC1B and is not introduced here.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { computeEffectiveCutV2 } from "../replay/cut.js";
|
|
19
|
+
import type { ReplayToolPair, ReplayRetreatCode } from "../replay/types.js";
|
|
20
|
+
|
|
21
|
+
/** Migration failure codes (registered in M3-001..010 as conformance rows). */
|
|
22
|
+
export const M3_FAIL = {
|
|
23
|
+
MISSING_HOST: "M3_HOST_MISSING",
|
|
24
|
+
INVALID_MINIMA: "M3_MINIMA_VIOLATED",
|
|
25
|
+
PAIR_SPLIT: "M3_PAIR_SPLIT",
|
|
26
|
+
ANCHOR_CROSSED: "M3_ANCHOR_CROSSED",
|
|
27
|
+
COPY_MISMATCH: "M3_COPY_MISMATCH",
|
|
28
|
+
} as const;
|
|
29
|
+
export type M3FailureCode = (typeof M3_FAIL)[keyof typeof M3_FAIL];
|
|
30
|
+
|
|
31
|
+
/** Live seq inputs that parameterize the effective cut. */
|
|
32
|
+
export interface M3CutInput {
|
|
33
|
+
readonly requestedSeq: bigint;
|
|
34
|
+
readonly boundarySafeSeq: bigint;
|
|
35
|
+
readonly committedSeq: bigint;
|
|
36
|
+
readonly capturedHighWater: bigint;
|
|
37
|
+
readonly anchorFloor: bigint;
|
|
38
|
+
readonly pairs: readonly ReplayToolPair[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Host state the migration reads/writes. copy/validate are read-only over host;
|
|
43
|
+
* switch is the only mutating step (atomically activates the staged pointer).
|
|
44
|
+
*/
|
|
45
|
+
export interface M3Host {
|
|
46
|
+
/** Actively used cut pointer (legacy prior). Never changes before switch. */
|
|
47
|
+
readonly oldPointer: bigint | null;
|
|
48
|
+
/** Persist the staged (new) cut pointer WITHOUT activating it. */
|
|
49
|
+
writeStaged(pointer: bigint): void;
|
|
50
|
+
/** The currently staged (new) pointer, persisted across restarts. */
|
|
51
|
+
stagedPointer(): bigint | null;
|
|
52
|
+
/** Contiguous durable authority high-water (min source). */
|
|
53
|
+
committedSeq(): bigint;
|
|
54
|
+
/** Atomic switch: activate the staged pointer and freeze the prior one. */
|
|
55
|
+
switchPointer(pointer: bigint): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface M3ValidateResult {
|
|
59
|
+
readonly ok: boolean;
|
|
60
|
+
readonly codes: readonly M3FailureCode[];
|
|
61
|
+
readonly retreats: readonly { code: ReplayRetreatCode; fromSeq: bigint; toSeq: bigint }[];
|
|
62
|
+
readonly effectiveSeq: bigint;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Pure effective-cut computation (min-of-three + retreat + floor). */
|
|
66
|
+
export function m3Compute(input: M3CutInput): bigint {
|
|
67
|
+
return computeEffectiveCutV2(input).cut.effectiveSeq;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Phase 1 — copy: compute the candidate effective cut and stage it via the host
|
|
72
|
+
* (without activating). Returns the staged seq.
|
|
73
|
+
*/
|
|
74
|
+
export function m3Copy(host: M3Host, input: M3CutInput): bigint {
|
|
75
|
+
const effective = m3Compute(input);
|
|
76
|
+
host.writeStaged(effective);
|
|
77
|
+
return effective;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Phase 2 — validate: the host's STAGED pointer must equal the freshly computed
|
|
82
|
+
* effective cut (copy-match), respect the minima, not split a pair, and never
|
|
83
|
+
* fall below the anchor floor. Failure codes:
|
|
84
|
+
* M3_HOST_MISSING — no staged pointer at all
|
|
85
|
+
* M3_COPY_MISMATCH — staged != freshly computed effective
|
|
86
|
+
* M3_MINIMA_VIOLATED — effective exceeds a source minimum (or requested)
|
|
87
|
+
* M3_PAIR_SPLIT — effective splits a tool call/result pair
|
|
88
|
+
* M3_ANCHOR_CROSSED — effective is below the anchor floor
|
|
89
|
+
*/
|
|
90
|
+
export function m3Validate(host: M3Host, input: M3CutInput): M3ValidateResult {
|
|
91
|
+
const codes: M3FailureCode[] = [];
|
|
92
|
+
const { cut, retreats } = computeEffectiveCutV2(input);
|
|
93
|
+
const expected = cut.effectiveSeq;
|
|
94
|
+
|
|
95
|
+
const staged = host.stagedPointer();
|
|
96
|
+
if (staged === null) {
|
|
97
|
+
codes.push(M3_FAIL.MISSING_HOST);
|
|
98
|
+
} else {
|
|
99
|
+
if (staged !== expected) codes.push(M3_FAIL.COPY_MISMATCH);
|
|
100
|
+
if (
|
|
101
|
+
staged > input.boundarySafeSeq ||
|
|
102
|
+
staged > host.committedSeq() ||
|
|
103
|
+
staged > input.capturedHighWater ||
|
|
104
|
+
staged > input.requestedSeq
|
|
105
|
+
) {
|
|
106
|
+
codes.push(M3_FAIL.INVALID_MINIMA);
|
|
107
|
+
}
|
|
108
|
+
for (const p of input.pairs) {
|
|
109
|
+
if (p.callSeq <= staged && staged < p.resultSeq) {
|
|
110
|
+
codes.push(M3_FAIL.PAIR_SPLIT);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (staged < input.anchorFloor) codes.push(M3_FAIL.ANCHOR_CROSSED);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
ok: codes.length === 0,
|
|
119
|
+
codes,
|
|
120
|
+
retreats,
|
|
121
|
+
effectiveSeq: expected,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Phase 3 — switch: atomic activation of the staged pointer. Only call after
|
|
127
|
+
* m3Validate reports ok; the host performs the durable pointer flip.
|
|
128
|
+
*/
|
|
129
|
+
export function m3Switch(host: M3Host, input: M3CutInput): bigint {
|
|
130
|
+
const effective = m3Compute(input);
|
|
131
|
+
host.switchPointer(effective);
|
|
132
|
+
return effective;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Run the full copy-validate-switch migration against a host. On validation
|
|
137
|
+
* failure the switch is NOT performed and the host retains its previous pointer
|
|
138
|
+
* (crash-safety / copy-validate-switch).
|
|
139
|
+
*/
|
|
140
|
+
export function migrateEffectiveCutV2(
|
|
141
|
+
host: M3Host,
|
|
142
|
+
input: M3CutInput,
|
|
143
|
+
): { ok: boolean; codes: readonly M3FailureCode[]; effectiveSeq: bigint } {
|
|
144
|
+
m3Copy(host, input);
|
|
145
|
+
const v = m3Validate(host, input);
|
|
146
|
+
if (!v.ok) return { ok: false, codes: v.codes, effectiveSeq: v.effectiveSeq };
|
|
147
|
+
m3Switch(host, input);
|
|
148
|
+
return { ok: true, codes: [], effectiveSeq: v.effectiveSeq };
|
|
149
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/migrations/occurrence-v2.ts — M2 copy/validate/switch migration
|
|
3
|
+
* for the occurrence-v2 ledger + compat journal (VC1B).
|
|
4
|
+
*
|
|
5
|
+
* M2 converts the neutral v2 ledger into a lossless legacy export so an OLD
|
|
6
|
+
* binary may keep reading after a downgrade. It follows the compat-journal
|
|
7
|
+
* copy/validate/switch contract: COPY the v2 rows + journal records into the
|
|
8
|
+
* staged legacy export, VALIDATE the staged export (seq monotonicity + digest
|
|
9
|
+
* parity + unrepresentable rows listed), and ONLY THEN atomically SWITCH
|
|
10
|
+
* authority to the export. Each phase is exposed separately so a crash between
|
|
11
|
+
* validate and switch retains the OLD authority and a restart resumes
|
|
12
|
+
* idempotently (the unique failure-injection contract — terminate after
|
|
13
|
+
* `validated` and before `switched`, restart switches once without duplicate
|
|
14
|
+
* rows).
|
|
15
|
+
*
|
|
16
|
+
* Runs against an injected M2Host so the migration is deterministic and
|
|
17
|
+
* testable. Never mutates v2; the export is a NEW copy (silent direct downgrade
|
|
18
|
+
* is rejected). Registered conformance rows: M2-001..015 + MIG-DOWN-001.
|
|
19
|
+
* PREVENT-002/011/PI-004 honored; no console.log.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
23
|
+
import { MIG_DOWN_FAIL, verifyLegacyDigest } from "../ledger/compat-journal.js";
|
|
24
|
+
|
|
25
|
+
/** M2 failure codes (downgrade export). */
|
|
26
|
+
export const M2_FAIL = {
|
|
27
|
+
...MIG_DOWN_FAIL,
|
|
28
|
+
COPY_MISSING: "M2_COPY_MISSING",
|
|
29
|
+
UNREPRESENTABLE_UNLISTED: "M2_UNREPRESENTABLE_UNLISTED",
|
|
30
|
+
} as const;
|
|
31
|
+
export type M2MigrationCode = (typeof M2_FAIL)[keyof typeof M2_FAIL];
|
|
32
|
+
|
|
33
|
+
/** A legacy export row derived from a v2 occurrence (or an unrepresentable marker). */
|
|
34
|
+
export interface LegacyExportRow {
|
|
35
|
+
readonly session: string;
|
|
36
|
+
readonly seq: bigint;
|
|
37
|
+
readonly eventId: string;
|
|
38
|
+
readonly digest: string;
|
|
39
|
+
readonly kind: string;
|
|
40
|
+
readonly legacyProjection: string | null;
|
|
41
|
+
readonly unrepresentable: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The host the migration reads from and writes to. copy/validate are read-only
|
|
46
|
+
* over v2; switch is the only mutating step (atomically activating the legacy
|
|
47
|
+
* export as the openable authority). All phase transitions delegate to the
|
|
48
|
+
* compat journal's lifecycle so validate/switch advance the same singleton
|
|
49
|
+
* state machine (prepared→copied→validated→switched).
|
|
50
|
+
*/
|
|
51
|
+
export interface M2Host {
|
|
52
|
+
readonly db: DatabaseSync;
|
|
53
|
+
/** Durable journal phase (prepared/copied/validated/switched). */
|
|
54
|
+
readonly phase: () => string;
|
|
55
|
+
/** Whether the journal is active (any v2 append journaled). */
|
|
56
|
+
readonly journalActive: () => boolean;
|
|
57
|
+
/** All journaled occurrences (v2 source of truth for the export). */
|
|
58
|
+
readonly journalRows: () => readonly LegacyExportRow[];
|
|
59
|
+
/** Stage the legacy export rows (idempotent; never activates). */
|
|
60
|
+
readonly writeStagedLegacy: (rows: readonly LegacyExportRow[]) => void;
|
|
61
|
+
/** The staged legacy export (null when not yet staged). */
|
|
62
|
+
readonly stagedLegacy: () => readonly LegacyExportRow[] | null;
|
|
63
|
+
/** Validate the staged export against the journal (advances to `validated`). */
|
|
64
|
+
readonly validateStaged: () => { ok: boolean; codes: readonly string[] };
|
|
65
|
+
/** Atomically switch authority to the legacy export (advances to `switched`). */
|
|
66
|
+
readonly switchLegacy: () => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The freshly computed legacy export from the v2 journal (expected bytes). */
|
|
70
|
+
export function computeLegacyExport(host: M2Host): readonly LegacyExportRow[] {
|
|
71
|
+
return host.journalRows();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Phase 1 — copy: stage the legacy export without activating. Returns rows. */
|
|
75
|
+
export function m2Copy(host: M2Host): readonly LegacyExportRow[] {
|
|
76
|
+
const rows = computeLegacyExport(host);
|
|
77
|
+
host.writeStagedLegacy(rows);
|
|
78
|
+
return rows;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Phase 2 — validate: the staged export must equal the freshly computed one
|
|
83
|
+
* (copy-match), the journal must be active and have reached the `copied`
|
|
84
|
+
* phase, seq must be monotonic per session, digests must be well-formed, and
|
|
85
|
+
* every unrepresentable row must be listed. Advances to `validated` only when
|
|
86
|
+
* every check passes (so a crash here leaves authority unchanged).
|
|
87
|
+
*/
|
|
88
|
+
export function m2Validate(host: M2Host): M2ValidateResult {
|
|
89
|
+
const codes: M2MigrationCode[] = [];
|
|
90
|
+
const staged = host.stagedLegacy();
|
|
91
|
+
const expected = computeLegacyExport(host);
|
|
92
|
+
|
|
93
|
+
if (staged === null) {
|
|
94
|
+
codes.push(M2_FAIL.COPY_MISSING);
|
|
95
|
+
} else {
|
|
96
|
+
const sameRows =
|
|
97
|
+
staged.length === expected.length &&
|
|
98
|
+
staged.every(
|
|
99
|
+
(r, i) =>
|
|
100
|
+
r.session === expected[i]!.session &&
|
|
101
|
+
r.seq === expected[i]!.seq &&
|
|
102
|
+
r.eventId === expected[i]!.eventId &&
|
|
103
|
+
r.unrepresentable === expected[i]!.unrepresentable,
|
|
104
|
+
);
|
|
105
|
+
if (!sameRows) codes.push(M2_FAIL.COPY_MISSING);
|
|
106
|
+
// Seq monotonic per session (no regression).
|
|
107
|
+
const prev = new Map<string, bigint>();
|
|
108
|
+
for (const r of staged) {
|
|
109
|
+
const last = prev.get(r.session);
|
|
110
|
+
if (last !== undefined && r.seq <= last) codes.push(M2_FAIL.SEQ_REGRESSION);
|
|
111
|
+
prev.set(r.session, r.seq);
|
|
112
|
+
}
|
|
113
|
+
// Digests on representable rows must verify: recompute sha256 over the stored
|
|
114
|
+
// source and compare to the recorded digest (Q03 — real parity).
|
|
115
|
+
for (const r of staged) {
|
|
116
|
+
if (!r.unrepresentable && !verifyLegacyDigest(r.digest, r.legacyProjection)) {
|
|
117
|
+
codes.push(M2_FAIL.DIGEST_MISMATCH);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Every unrepresentable row is explicitly flagged (MIG-DOWN-003).
|
|
121
|
+
for (const r of expected) {
|
|
122
|
+
if (r.unrepresentable) {
|
|
123
|
+
const flag = staged.find(
|
|
124
|
+
(s) => s.session === r.session && s.seq === r.seq && s.unrepresentable,
|
|
125
|
+
);
|
|
126
|
+
if (!flag) codes.push(M2_FAIL.UNREPRESENTABLE_UNLISTED);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!host.journalActive()) codes.push(M2_FAIL.NOT_ACTIVE);
|
|
132
|
+
if (host.phase() !== "copied") codes.push(M2_FAIL.PHASE_UNREACHED);
|
|
133
|
+
|
|
134
|
+
if (codes.length === 0) {
|
|
135
|
+
const v = host.validateStaged();
|
|
136
|
+
if (!v.ok) {
|
|
137
|
+
// Q05: narrow the host's string codes through a runtime type guard instead
|
|
138
|
+
// of an unchecked cast — unknown codes are dropped, never mis-typed.
|
|
139
|
+
for (const c of v.codes) if (isM2MigrationCode(c)) codes.push(c);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const ok = codes.length === 0;
|
|
143
|
+
return { ok, codes: dedupe(codes) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** M2 downgrade-export validation result. */
|
|
147
|
+
export interface M2ValidateResult {
|
|
148
|
+
readonly ok: boolean;
|
|
149
|
+
readonly codes: readonly M2MigrationCode[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Phase 3 — switch: atomic activation of the legacy export as the openable
|
|
154
|
+
* authority. Only call after m2Validate reports ok; the journal advances to
|
|
155
|
+
* `switched` and v2 becomes read-only-archival. Idempotent on resume.
|
|
156
|
+
*/
|
|
157
|
+
export function m2Switch(host: M2Host): void {
|
|
158
|
+
host.switchLegacy();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Registered M2 conformance ID range (M2-001..015). */
|
|
162
|
+
export const M2_IDS = [
|
|
163
|
+
"M2-001",
|
|
164
|
+
"M2-002",
|
|
165
|
+
"M2-003",
|
|
166
|
+
"M2-004",
|
|
167
|
+
"M2-005",
|
|
168
|
+
"M2-006",
|
|
169
|
+
"M2-007",
|
|
170
|
+
"M2-008",
|
|
171
|
+
"M2-009",
|
|
172
|
+
"M2-010",
|
|
173
|
+
"M2-011",
|
|
174
|
+
"M2-012",
|
|
175
|
+
"M2-013",
|
|
176
|
+
"M2-014",
|
|
177
|
+
"M2-015",
|
|
178
|
+
] as const;
|
|
179
|
+
|
|
180
|
+
/** Registered MIG-DOWN conformance ID (MIG-DOWN-001). */
|
|
181
|
+
export const MIG_DOWN_IDS = ["MIG-DOWN-001"] as const;
|
|
182
|
+
|
|
183
|
+
/** Runtime narrowing: is `code` a known M2 migration failure code? */
|
|
184
|
+
function isM2MigrationCode(code: string): code is M2MigrationCode {
|
|
185
|
+
return (Object.values(M2_FAIL) as string[]).includes(code);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function dedupe(codes: M2MigrationCode[]): M2MigrationCode[] {
|
|
189
|
+
const out: M2MigrationCode[] = [];
|
|
190
|
+
for (const c of codes) if (!out.includes(c)) out.push(c);
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/replay/cut.ts — ReplayCutV2 effective-cut calculator (VC0B).
|
|
3
|
+
*
|
|
4
|
+
* M3 effective-cut rule: effective = min(boundarySafeSeq, committedSeq,
|
|
5
|
+
* capturedHighWater), capped by requestedSeq, then retreated to the call
|
|
6
|
+
* boundary whenever the candidate intersects a tool call/result pair, then
|
|
7
|
+
* clamped to the recent-anchor floor. On a tie among the min sources we choose
|
|
8
|
+
* the LOWER source order and record CUT_LOWEST_SOURCE_ORDER. This is the
|
|
9
|
+
* successor to the legacy capped-replay effective cut — the defect at
|
|
10
|
+
* docs/vector-cortex/SPRINT_PLAN.md M3 ("capped replay effective cut must
|
|
11
|
+
* respect boundary/commit/capture minima") is fixed here.
|
|
12
|
+
*
|
|
13
|
+
* Pure/deterministic — no network, no side effects (PREVENT-PI-004).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
ReplayCutV2,
|
|
18
|
+
ReplayRetreatCode,
|
|
19
|
+
ReplayRetreatRecord,
|
|
20
|
+
ReplayToolPair,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
export interface EffectiveCutInput {
|
|
24
|
+
/** Caller's desired cap; effective never exceeds this. */
|
|
25
|
+
readonly requestedSeq: bigint;
|
|
26
|
+
/** Largest pair-safe cap (already boundary-safe). */
|
|
27
|
+
readonly boundarySafeSeq: bigint;
|
|
28
|
+
/** Contiguous durable authority high-water. */
|
|
29
|
+
readonly committedSeq: bigint;
|
|
30
|
+
/** Derived high-water captured/frozen at replay start. */
|
|
31
|
+
readonly capturedHighWater: bigint;
|
|
32
|
+
/** Recent-anchor floor (inclusive lower bound the cut never crosses). */
|
|
33
|
+
readonly anchorFloor: bigint;
|
|
34
|
+
/** Tool call/result pairs (callSeq < resultSeq) to reason over. */
|
|
35
|
+
readonly pairs: readonly ReplayToolPair[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface EffectiveCutOutput {
|
|
39
|
+
readonly cut: ReplayCutV2;
|
|
40
|
+
readonly retreats: readonly ReplayRetreatRecord[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const MIN_SOURCES = ["boundary", "committed", "captured"] as const;
|
|
44
|
+
type MinSource = (typeof MIN_SOURCES)[number];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* min-of-three, capped by requestedSeq, with deterministic lower-source
|
|
48
|
+
* tie-breaking. Returns the min value and every source that tied for it.
|
|
49
|
+
* On a tie the LOWER source order wins; `sources[0]` is the winning source.
|
|
50
|
+
*/
|
|
51
|
+
function minOfCapped(
|
|
52
|
+
requestedSeq: bigint,
|
|
53
|
+
boundarySafeSeq: bigint,
|
|
54
|
+
committedSeq: bigint,
|
|
55
|
+
capturedHighWater: bigint,
|
|
56
|
+
): { value: bigint; sources: MinSource[] } {
|
|
57
|
+
const values: [MinSource, bigint][] = [
|
|
58
|
+
["boundary", boundarySafeSeq],
|
|
59
|
+
["committed", committedSeq],
|
|
60
|
+
["captured", capturedHighWater],
|
|
61
|
+
];
|
|
62
|
+
const minVal = values.reduce<bigint>((a, [, v]) => (v < a ? v : a), requestedSeq);
|
|
63
|
+
const floors = values.filter(([, v]) => v === minVal).map(([s]) => s);
|
|
64
|
+
// If a source value exceeds requestedSeq it is capped away; the min is
|
|
65
|
+
// positioned at requestedSeq only when every source is above the request.
|
|
66
|
+
const value = floors.length > 0 ? minVal : requestedSeq;
|
|
67
|
+
return { value, sources: floors.length > 0 ? floors : ["boundary"] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** True when a cut at seq `e` does NOT split any tool call/result pair. */
|
|
71
|
+
export function cutIsPairSafe(e: bigint, pairs: readonly ReplayToolPair[]): boolean {
|
|
72
|
+
for (const p of pairs) {
|
|
73
|
+
// Cutting at e keeps events with seq <= e. A pair is split when the call
|
|
74
|
+
// (callSeq) is kept but the result (resultSeq) is dropped.
|
|
75
|
+
if (p.callSeq <= e && e < p.resultSeq) return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Retreat `e` upward-locked toward the call boundary of any pair it splits.
|
|
82
|
+
* Returns the largest pair-safe e <= start, never below `floor`.
|
|
83
|
+
*
|
|
84
|
+
* `splitResolved` is true when a CUT_TOOL_PAIR_SPLIT retreat occurred; when a
|
|
85
|
+
* retreat would cross the floor the cut clamps AT the floor and the reason
|
|
86
|
+
* becomes CUT_ANCHOR_FLOOR (the floor is assumed pair-safe for legal inputs).
|
|
87
|
+
*/
|
|
88
|
+
function retreatAgainstPairs(
|
|
89
|
+
start: bigint,
|
|
90
|
+
pairs: readonly ReplayToolPair[],
|
|
91
|
+
floor: bigint,
|
|
92
|
+
): { effective: bigint; split: boolean; floorClamped: boolean } {
|
|
93
|
+
let e = start;
|
|
94
|
+
let split = false;
|
|
95
|
+
let floorClamped = false;
|
|
96
|
+
for (;;) {
|
|
97
|
+
let hit = false;
|
|
98
|
+
for (const p of pairs) {
|
|
99
|
+
if (p.callSeq <= e && e < p.resultSeq) {
|
|
100
|
+
hit = true;
|
|
101
|
+
const target = p.callSeq - 1n;
|
|
102
|
+
if (target < floor) {
|
|
103
|
+
// No-progress guard (VC0B-I12): the retreat target lies below the
|
|
104
|
+
// floor. If the floor itself still splits a pair we must not loop
|
|
105
|
+
// back onto it (that hangs the synchronous agent loop) — clamp AT
|
|
106
|
+
// the floor once and stop. The floor is taken as the legal cut.
|
|
107
|
+
e = floor;
|
|
108
|
+
floorClamped = true;
|
|
109
|
+
} else {
|
|
110
|
+
e = target;
|
|
111
|
+
}
|
|
112
|
+
split = true;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Terminate when no pair splits e, OR when we have already clamped to the
|
|
117
|
+
// floor (the floor is an absolute lower bound; we never retreat past it
|
|
118
|
+
// even if it is not itself pair-safe).
|
|
119
|
+
if (!hit || floorClamped) return { effective: e, split, floorClamped };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Compute the v2 effective cut. Pure with respect to the provided inputs.
|
|
125
|
+
*/
|
|
126
|
+
export function computeEffectiveCutV2(input: EffectiveCutInput): EffectiveCutOutput {
|
|
127
|
+
const { requestedSeq, boundarySafeSeq, committedSeq, capturedHighWater, anchorFloor, pairs } = input;
|
|
128
|
+
|
|
129
|
+
const retreats: ReplayRetreatRecord[] = [];
|
|
130
|
+
|
|
131
|
+
// Step 1 — min-of-three (capped by requestedSeq) with lower-source tie-break.
|
|
132
|
+
const min = minOfCapped(requestedSeq, boundarySafeSeq, committedSeq, capturedHighWater);
|
|
133
|
+
let effective: bigint = min.value;
|
|
134
|
+
// Tie among the min sources: resolved to the LOWER source order — record it.
|
|
135
|
+
if (min.sources.length > 1) {
|
|
136
|
+
retreats.push({
|
|
137
|
+
code: "CUT_LOWEST_SOURCE_ORDER",
|
|
138
|
+
fromSeq: effective,
|
|
139
|
+
toSeq: effective,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Step 2 — pair retreat (never below the anchor floor).
|
|
144
|
+
const retreat = retreatAgainstPairs(effective, pairs, anchorFloor);
|
|
145
|
+
if (retreat.split && retreat.floorClamped) {
|
|
146
|
+
retreats.push({ code: "CUT_ANCHOR_FLOOR", fromSeq: effective, toSeq: retreat.effective });
|
|
147
|
+
} else if (retreat.split) {
|
|
148
|
+
retreats.push({ code: "CUT_TOOL_PAIR_SPLIT", fromSeq: effective, toSeq: retreat.effective });
|
|
149
|
+
}
|
|
150
|
+
effective = retreat.effective;
|
|
151
|
+
|
|
152
|
+
// Step 3 — final anchor-floor clamp (safety; legal floors are already >=).
|
|
153
|
+
if (effective < anchorFloor) {
|
|
154
|
+
retreats.push({ code: "CUT_ANCHOR_FLOOR", fromSeq: effective, toSeq: anchorFloor });
|
|
155
|
+
effective = anchorFloor;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const cut: ReplayCutV2 = {
|
|
159
|
+
requestedSeq,
|
|
160
|
+
boundarySafeSeq,
|
|
161
|
+
committedSeq,
|
|
162
|
+
capturedHighWater,
|
|
163
|
+
effectiveSeq: effective,
|
|
164
|
+
};
|
|
165
|
+
return { cut, retreats };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Re-export for callers that want just the code type. */
|
|
169
|
+
export type { ReplayRetreatCode };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/replay/emit.ts — VC0B replay emit seam (VC0B).
|
|
3
|
+
*
|
|
4
|
+
* The single structured-event surface for the replay/effective-cut path. It is
|
|
5
|
+
* the replay analogue of the VC0A eval observer (`src/vector-cortex/eval/
|
|
6
|
+
* observer.ts`): the same non-fatal, structured-JSON `ts`+`event` contract
|
|
7
|
+
* (see `src/log.ts` LogEntry), delivered through an injected emit callback so it
|
|
8
|
+
* stays pi-agnostic, deterministically testable, and network-free
|
|
9
|
+
* (PREVENT-PI-004). A future VC0C breaker/dashboard consumes these lines.
|
|
10
|
+
*
|
|
11
|
+
* This is deliberately MINIMAL — it is NOT a second metrics pipeline. It carries
|
|
12
|
+
* only the two events the replay/effective-cut path emits:
|
|
13
|
+
* - vector_cortex_replay_cut_retreat (a pair/anchor/lowest-source retreat)
|
|
14
|
+
* - vector_cortex_replay_highwater_frozen (mode C: host transcript unchanged)
|
|
15
|
+
*
|
|
16
|
+
* Flag gating: this is VC0B's single real consumer of `VC0B_ENABLED`. When the
|
|
17
|
+
* flag is OFF, the reporter emits NOTHING (zero observability writes) — mirroring
|
|
18
|
+
* the VC0A mode-C pattern where flag-off produces zero evaluation writes. The
|
|
19
|
+
* ReplayReportV2 data (cut/effectiveSeq/capturedHighWater/mode) is returned by
|
|
20
|
+
* `runReplayV2` regardless; only the emitted observability is gated. The flag is
|
|
21
|
+
* read per-call so a live toggle takes effect immediately.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { VC0B_ENABLED } from "../../config/vector-cortex.js";
|
|
25
|
+
|
|
26
|
+
export type ReplayEventName =
|
|
27
|
+
| "vector_cortex_replay_cut_retreat"
|
|
28
|
+
| "vector_cortex_replay_highwater_frozen";
|
|
29
|
+
|
|
30
|
+
/** Injected emit callback — same (event, fields) shape as the eval observer. */
|
|
31
|
+
export type ReplayEmitter = (event: string, fields: Record<string, unknown>) => void;
|
|
32
|
+
|
|
33
|
+
export interface ReplayCutRetreatFields {
|
|
34
|
+
readonly session: string;
|
|
35
|
+
readonly code: string;
|
|
36
|
+
readonly fromSeq: string;
|
|
37
|
+
readonly toSeq: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ReplayHighWaterFrozenFields {
|
|
41
|
+
readonly session: string;
|
|
42
|
+
readonly committedSeq: string;
|
|
43
|
+
readonly frozenHighWater: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Typed, best-effort reporter bound to the two replay event names. */
|
|
47
|
+
export interface ReplayReporter {
|
|
48
|
+
readonly cutRetreat: (fields: ReplayCutRetreatFields) => void;
|
|
49
|
+
readonly highWaterFrozen: (fields: ReplayHighWaterFrozenFields) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function safe(fn: (event: ReplayEventName, fields: Record<string, unknown>) => void): (
|
|
53
|
+
event: ReplayEventName,
|
|
54
|
+
fields: Record<string, unknown>,
|
|
55
|
+
) => void {
|
|
56
|
+
return (event, fields) => {
|
|
57
|
+
try {
|
|
58
|
+
fn(event, fields);
|
|
59
|
+
} catch {
|
|
60
|
+
/* non-fatal observability — never break the agent loop */
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build a typed reporter over an injected emit callback. Observability emission
|
|
67
|
+
* is gated on `VC0B_ENABLED()` — flag OFF means zero replay observability writes
|
|
68
|
+
* (VC0A mode-C parity). `emit` is optional: absent (or flag off) degrades to a
|
|
69
|
+
* no-op. The flag is read per-call so a live toggle takes effect immediately.
|
|
70
|
+
*/
|
|
71
|
+
export function createReplayReporter(emit?: ReplayEmitter): ReplayReporter {
|
|
72
|
+
const fire = safe((event, fields) => {
|
|
73
|
+
if (VC0B_ENABLED()) emit?.(event, fields);
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
cutRetreat(fields) {
|
|
77
|
+
fire("vector_cortex_replay_cut_retreat", fields as unknown as Record<string, unknown>);
|
|
78
|
+
},
|
|
79
|
+
highWaterFrozen(fields) {
|
|
80
|
+
fire("vector_cortex_replay_highwater_frozen", fields as unknown as Record<string, unknown>);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Export the event names for consumers that match on them. */
|
|
86
|
+
export const REPLAY_EVENTS: readonly ReplayEventName[] = [
|
|
87
|
+
"vector_cortex_replay_cut_retreat",
|
|
88
|
+
"vector_cortex_replay_highwater_frozen",
|
|
89
|
+
] as const;
|