pi-mega-compact 0.19.2 → 0.19.3
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 +12 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +1 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-query.js +46 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex.js +1 -0
- package/dist/extensions/dashboard-server/routes.js +1 -1
- package/dist/extensions/dashboard-server/server.js +3 -1
- package/dist/src/config/vector-cortex.js +12 -0
- package/dist/src/config.js +1 -1
- package/dist/src/tieredRouter.js +8 -0
- package/dist/src/vector-cortex/migrations/router-generation-v2.js +252 -0
- package/dist/src/vector-cortex/topology/query.js +342 -0
- package/dist/vector-cortex/migrations/router-generation-v2.js +252 -0
- package/dist/vector-cortex/topology/query.js +342 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-CSaJZXru.js → AreaChart-DqiFSnDx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-CSaJZXru.js.map → AreaChart-DqiFSnDx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-CQouNj1g.js → BarChart-Cqg-PQMK.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-CQouNj1g.js.map → BarChart-Cqg-PQMK.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-m_eiCYoL.js → CacheTab-BPNs0xIF.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-m_eiCYoL.js.map → CacheTab-BPNs0xIF.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-DRyOwjYN.js → EventsTab-BQf7OGkf.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-DRyOwjYN.js.map → EventsTab-BQf7OGkf.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-Dj9k40TJ.js → HealthTab-B14TaSPN.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-Dj9k40TJ.js.map → HealthTab-B14TaSPN.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Bk9mhYfD.js → MaintenanceTab-D8lkRoa1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Bk9mhYfD.js.map → MaintenanceTab-D8lkRoa1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-C7ve49VG.js → MemoryMapTab-BOWtGfpx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-C7ve49VG.js.map → MemoryMapTab-BOWtGfpx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DzjhqZA9.js → MetricsTab-7XI9cWaX.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DzjhqZA9.js.map → MetricsTab-7XI9cWaX.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CFQdfIm_.js → OverviewTab-C9KaGYvh.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-CFQdfIm_.js.map → OverviewTab-C9KaGYvh.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-DyjwEFUr.js → ReposTab-BwESNhox.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-DyjwEFUr.js.map → ReposTab-BwESNhox.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-njLIy7ab.js → SessionsTab-DKH55Rda.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-njLIy7ab.js.map → SessionsTab-DKH55Rda.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-UHIs8-Lh.js → SetupTab-zDxf-nJd.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-UHIs8-Lh.js.map → SetupTab-zDxf-nJd.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-inWHjCxg.js → TimeSavedCard-C9isog4L.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-inWHjCxg.js.map → TimeSavedCard-C9isog4L.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CXJYugBu.js → TurnsTab-ciI6ydvj.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CXJYugBu.js.map → TurnsTab-ciI6ydvj.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-qTZ7NWng.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-qTZ7NWng.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-BJ4A2oi9.js → WikiTab-DcYHc734.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-BJ4A2oi9.js.map → WikiTab-DcYHc734.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-BWl-moyn.js → button-DF8lx4bt.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-BWl-moyn.js.map → button-DF8lx4bt.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-C58RVPOk.js → card-DIoDheSZ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-C58RVPOk.js.map → card-DIoDheSZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-vgeOQU3L.js → generateCategoricalChart-BFRzeYX3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-vgeOQU3L.js.map → generateCategoricalChart-BFRzeYX3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-lVrWQjpu.js → index-DSZON0Yi.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-lVrWQjpu.js.map → index-DSZON0Yi.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-CSeplFQT.js → switch-l4qo22mU.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-CSeplFQT.js.map → switch-l4qo22mU.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-CQRxgVRi.js → toggle-DPn6td1_.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-CQRxgVRi.js.map → toggle-DPn6td1_.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-BUz6IOI-.js → useSSE-BLmolqTJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-BUz6IOI-.js.map → useSSE-BLmolqTJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/api/vector-cortex.ts +9 -0
- package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +27 -0
- package/extensions/dashboard-client/src/types/vector-cortex.ts +12 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +17 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
- package/extensions/dashboard-server/routes-vector-cortex-query.ts +54 -0
- package/extensions/dashboard-server/routes-vector-cortex.ts +1 -0
- package/extensions/dashboard-server/routes.ts +1 -0
- package/extensions/dashboard-server/server.ts +2 -0
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +13 -0
- package/src/config.ts +1 -0
- package/src/tieredRouter.ts +9 -0
- package/src/vector-cortex/migrations/router-generation-v2.ts +317 -0
- package/src/vector-cortex/topology/query.ts +485 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DkS0Azx9.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-DkS0Azx9.js.map +0 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/topology/query.ts — topology query + router invalidation (VC3C).
|
|
3
|
+
*
|
|
4
|
+
* Owns `RouterKeyV2` (the structured router key) and `TopologyQueryV1` (the
|
|
5
|
+
* generation query/index surface that serves graphs and invalidates stale
|
|
6
|
+
* generations). This is the degradation seam of the sprint's failure triad:
|
|
7
|
+
*
|
|
8
|
+
* A = topology index at current generation (serve the active graph);
|
|
9
|
+
* B = fresh linear scan forced when a key targets a STALE generation;
|
|
10
|
+
* C = authority sequence scan forced when the derived store is unavailable.
|
|
11
|
+
*
|
|
12
|
+
* Key encoding is length-delimited with unsigned-byte (prefix) ordering and a
|
|
13
|
+
* FIXED five-field arity, so no field value can ambiguously prefix another and
|
|
14
|
+
* two sessions `a` / `aa` can never prefix-collide at the key level
|
|
15
|
+
* (M6-KEY-001). Invalidation matches the exact `(session,generation)` identity
|
|
16
|
+
* bytes — never a string-prefix — and a query whose key carries a generation
|
|
17
|
+
* older than the session's active one is rejected with `TOP_GENERATION_STALE`
|
|
18
|
+
* (M6-STALE-002), which forces mode B.
|
|
19
|
+
*
|
|
20
|
+
* Pure src/ logic over an injected `TopologyQueryHost` (the source of graphs +
|
|
21
|
+
* linear/authority scans) and an optional `TopologyQueryEmit`. Deterministic,
|
|
22
|
+
* pi-agnostic, no console, no network (PREVENT-PI-004), no `any`
|
|
23
|
+
* (PREVENT-011). The caller owns durability; every store write on the caller
|
|
24
|
+
* side is best-effort and never breaks the agent loop.
|
|
25
|
+
*/
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
27
|
+
/** Query rejected: the key's generation is stale relative to the active one. */
|
|
28
|
+
export const TOP_GENERATION_STALE = "TOP_GENERATION_STALE";
|
|
29
|
+
/** Query rejected: a key could not be decoded (malformed length-delimited bytes). */
|
|
30
|
+
export const TOP_KEY_DECODE_FAILED = "TOP_KEY_DECODE_FAILED";
|
|
31
|
+
/** Query rejected: the derived store is unavailable and no authority scan is possible. */
|
|
32
|
+
export const TOP_AUTHORITY_UNAVAILABLE = "TOP_AUTHORITY_UNAVAILABLE";
|
|
33
|
+
/** Frozen router key schema version carried in every encoded key. */
|
|
34
|
+
export const ROUTER_KEY_VERSION = 2;
|
|
35
|
+
/**
|
|
36
|
+
* Create the query surface. `emit` is optional and flag-gated (the caller passes
|
|
37
|
+
* a reporter produced by the same pattern VC3A/VC3B use — zero emissions when
|
|
38
|
+
* `MEGACOMPACT_VC3C=0`, byte-identical to the predecessor). Pure in-memory index
|
|
39
|
+
* over the injected host; durability of invalidation is the caller's job.
|
|
40
|
+
*/
|
|
41
|
+
export function createTopologyQuery(host, emit) {
|
|
42
|
+
// session -> Set of indexed generation strings (exact, byte-identity).
|
|
43
|
+
const generations = new Map();
|
|
44
|
+
const fire = (event, fields) => {
|
|
45
|
+
if (!emit)
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
emit(event, fields);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* non-fatal observability — never break the agent loop */
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const query = (key) => {
|
|
55
|
+
const active = host.activeGeneration(key.session);
|
|
56
|
+
// Mode C: derived store unavailable -> authority sequence scan.
|
|
57
|
+
if (!host.derivedAvailable(key.session)) {
|
|
58
|
+
const cDigest = host.authorityScan(key.session, key.sourceStart, key.sourceEnd);
|
|
59
|
+
if (cDigest === undefined) {
|
|
60
|
+
return { ok: false, code: TOP_AUTHORITY_UNAVAILABLE, mode: "C" };
|
|
61
|
+
}
|
|
62
|
+
fire("vector_cortex_topology_query_demoted", {
|
|
63
|
+
session: key.session,
|
|
64
|
+
generation: key.generation.toString(),
|
|
65
|
+
code: "DERIVED_UNAVAILABLE",
|
|
66
|
+
toMode: "C",
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
mode: "C",
|
|
71
|
+
session: key.session,
|
|
72
|
+
generation: key.generation,
|
|
73
|
+
algorithm: key.algorithm,
|
|
74
|
+
matchedKey: encodeRouterKeyV2(key),
|
|
75
|
+
digest: cDigest.digest,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// A key is stale when its generation is below the active one (or the active
|
|
79
|
+
// generation is unset and the requested generation is non-zero).
|
|
80
|
+
const stale = active === undefined
|
|
81
|
+
? key.generation !== 0n
|
|
82
|
+
: key.generation < active;
|
|
83
|
+
if (stale) {
|
|
84
|
+
// Mode B: fresh linear scan forced by a stale A key.
|
|
85
|
+
const bDigest = host.linearScan(key.session, key.sourceStart, key.sourceEnd);
|
|
86
|
+
fire("vector_cortex_topology_query_demoted", {
|
|
87
|
+
session: key.session,
|
|
88
|
+
generation: key.generation.toString(),
|
|
89
|
+
activeGeneration: active === undefined ? null : active.toString(),
|
|
90
|
+
code: "STALE_GENERATION",
|
|
91
|
+
toMode: "B",
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
mode: "B",
|
|
96
|
+
session: key.session,
|
|
97
|
+
generation: key.generation,
|
|
98
|
+
algorithm: key.algorithm,
|
|
99
|
+
matchedKey: encodeRouterKeyV2(key),
|
|
100
|
+
digest: bDigest.digest,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Mode A: topology index at the current generation. If the derived store is
|
|
104
|
+
// up but has not indexed this exact (generation, algorithm) yet, that is a
|
|
105
|
+
// mode-A miss (no graph to serve) — force the fresh linear scan rather than
|
|
106
|
+
// fabricate a graph or report a false staleness (Q03: the index cannot be
|
|
107
|
+
// trusted to hold every current generation; B recomputes from source).
|
|
108
|
+
const graph = host.graphAt(key.session, key.generation, key.algorithm);
|
|
109
|
+
if (graph === undefined) {
|
|
110
|
+
const bDigest = host.linearScan(key.session, key.sourceStart, key.sourceEnd);
|
|
111
|
+
fire("vector_cortex_topology_query_demoted", {
|
|
112
|
+
session: key.session,
|
|
113
|
+
generation: key.generation.toString(),
|
|
114
|
+
code: "GENERATION_MISS",
|
|
115
|
+
toMode: "B",
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
mode: "B",
|
|
120
|
+
session: key.session,
|
|
121
|
+
generation: key.generation,
|
|
122
|
+
algorithm: key.algorithm,
|
|
123
|
+
matchedKey: encodeRouterKeyV2(key),
|
|
124
|
+
digest: bDigest.digest,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
mode: "A",
|
|
130
|
+
session: key.session,
|
|
131
|
+
generation: key.generation,
|
|
132
|
+
algorithm: key.algorithm,
|
|
133
|
+
matchedKey: encodeRouterKeyV2(key),
|
|
134
|
+
digest: graph.digest,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
const invalidate = (session, generation) => {
|
|
138
|
+
const invalidationIdentity = invalidationKey(session, generation);
|
|
139
|
+
const set = generations.get(session);
|
|
140
|
+
if (set)
|
|
141
|
+
set.delete(invalidationIdentity);
|
|
142
|
+
fire("vector_cortex_router_generation_invalidated", {
|
|
143
|
+
session,
|
|
144
|
+
generation: generation.toString(),
|
|
145
|
+
key: invalidationIdentity,
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
const generationalCount = (session) => {
|
|
149
|
+
return generations.get(session)?.size ?? 0;
|
|
150
|
+
};
|
|
151
|
+
return { query, invalidate, generationalCount };
|
|
152
|
+
}
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Length-delimited, unsigned-byte-ordered key encoding (M6-KEY-001)
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
/**
|
|
157
|
+
* Encode one field: `u32BE(byteLength) || bytes`. Non-negative numbers are
|
|
158
|
+
* encoded as their unsigned big-endian byte form (leading zeros trimmed). This
|
|
159
|
+
* is the standard big-endian length-prefixed encoding: two fields compare in
|
|
160
|
+
* unsigned-byte order exactly as their values do, and no value is an ambiguous
|
|
161
|
+
* prefix of another.
|
|
162
|
+
*/
|
|
163
|
+
function encodeLengthDelimited(bytes) {
|
|
164
|
+
const out = new Uint8Array(4 + bytes.length);
|
|
165
|
+
const view = new DataView(out.buffer);
|
|
166
|
+
view.setUint32(0, bytes.length, false);
|
|
167
|
+
out.set(bytes, 4);
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
/** Big-endian unsigned bytes of a non-negative bigint (length-prefixed below). */
|
|
171
|
+
function bigintBytes(n) {
|
|
172
|
+
let v = n;
|
|
173
|
+
const out = [];
|
|
174
|
+
if (v === 0n)
|
|
175
|
+
return new Uint8Array([0]);
|
|
176
|
+
while (v > 0n) {
|
|
177
|
+
out.unshift(Number(v & 0xffn));
|
|
178
|
+
v >>= 8n;
|
|
179
|
+
}
|
|
180
|
+
return Uint8Array.from(out);
|
|
181
|
+
}
|
|
182
|
+
/** UTF-8 bytes of a string. */
|
|
183
|
+
function strBytes(s) {
|
|
184
|
+
return new TextEncoder().encode(s);
|
|
185
|
+
}
|
|
186
|
+
function concat(parts) {
|
|
187
|
+
let len = 0;
|
|
188
|
+
for (const p of parts)
|
|
189
|
+
len += p.length;
|
|
190
|
+
const out = new Uint8Array(len);
|
|
191
|
+
let off = 0;
|
|
192
|
+
for (const p of parts) {
|
|
193
|
+
out.set(p, off);
|
|
194
|
+
off += p.length;
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Encode a RouterKeyV2 into its canonical, length-delimited, unsigned-byte-ordered
|
|
200
|
+
* byte form. Version-prefixed (1 byte) + five length-delimited fields in fixed
|
|
201
|
+
* order (session, sourceStart, sourceEnd, generation, algorithm). Fixed arity +
|
|
202
|
+
* length-delimiting means no key is a strict prefix of another and sessions `a`
|
|
203
|
+
* and `aa` never collide (M6-KEY-001).
|
|
204
|
+
*/
|
|
205
|
+
export function encodeRouterKeyV2(key) {
|
|
206
|
+
const version = Uint8Array.of(ROUTER_KEY_VERSION);
|
|
207
|
+
const session = encodeLengthDelimited(strBytes(key.session));
|
|
208
|
+
const sourceStart = encodeLengthDelimited(bigintBytes(key.sourceStart));
|
|
209
|
+
const sourceEnd = encodeLengthDelimited(bigintBytes(key.sourceEnd));
|
|
210
|
+
const generation = encodeLengthDelimited(bigintBytes(key.generation));
|
|
211
|
+
const algorithm = encodeLengthDelimited(strBytes(key.algorithm));
|
|
212
|
+
return "rk2:" + Buffer.from(concat([version, session, sourceStart, sourceEnd, generation, algorithm])).toString("hex");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Decode a RouterKeyV2 from its canonical byte form. Returns
|
|
216
|
+
* TOP_KEY_DECODE_FAILED on malformed bytes (bad version, truncated length, or
|
|
217
|
+
* trailing garbage). Never parses by string prefix.
|
|
218
|
+
*/
|
|
219
|
+
export function decodeRouterKeyV2(encoded) {
|
|
220
|
+
if (!encoded.startsWith("rk2:"))
|
|
221
|
+
return { ok: false, code: "TOP_KEY_DECODE_FAILED" };
|
|
222
|
+
const hex = encoded.slice(4);
|
|
223
|
+
if (!/^[0-9a-f]*$/.test(hex) || hex.length % 2 !== 0) {
|
|
224
|
+
return { ok: false, code: "TOP_KEY_DECODE_FAILED" };
|
|
225
|
+
}
|
|
226
|
+
const buf = Buffer.from(hex, "hex");
|
|
227
|
+
const bytes = new Uint8Array(buf);
|
|
228
|
+
const reader = new Reader(bytes);
|
|
229
|
+
const version = reader.u8();
|
|
230
|
+
if (version !== ROUTER_KEY_VERSION)
|
|
231
|
+
return { ok: false, code: "TOP_KEY_DECODE_FAILED" };
|
|
232
|
+
const sessionBytes = reader.lenPrefixed();
|
|
233
|
+
const ssBytes = reader.lenPrefixed();
|
|
234
|
+
const seBytes = reader.lenPrefixed();
|
|
235
|
+
const genBytes = reader.lenPrefixed();
|
|
236
|
+
const algoBytes = reader.lenPrefixed();
|
|
237
|
+
if (reader.remaining() !== 0)
|
|
238
|
+
return { ok: false, code: "TOP_KEY_DECODE_FAILED" };
|
|
239
|
+
if (sessionBytes === null || ssBytes === null || seBytes === null || genBytes === null || algoBytes === null) {
|
|
240
|
+
return { ok: false, code: "TOP_KEY_DECODE_FAILED" };
|
|
241
|
+
}
|
|
242
|
+
const session = new TextDecoder().decode(sessionBytes);
|
|
243
|
+
const sourceStart = bytesToBigint(ssBytes);
|
|
244
|
+
const sourceEnd = bytesToBigint(seBytes);
|
|
245
|
+
const generation = bytesToBigint(genBytes);
|
|
246
|
+
const algorithm = new TextDecoder().decode(algoBytes);
|
|
247
|
+
return { ok: true, key: { session, sourceStart, sourceEnd, generation, algorithm } };
|
|
248
|
+
}
|
|
249
|
+
/** Sequential length-delimited byte reader. */
|
|
250
|
+
class Reader {
|
|
251
|
+
bytes;
|
|
252
|
+
off = 0;
|
|
253
|
+
constructor(bytes) {
|
|
254
|
+
this.bytes = bytes;
|
|
255
|
+
}
|
|
256
|
+
u8() {
|
|
257
|
+
const v = this.bytes[this.off];
|
|
258
|
+
this.off += 1;
|
|
259
|
+
return v;
|
|
260
|
+
}
|
|
261
|
+
lenPrefixed() {
|
|
262
|
+
if (this.off + 4 > this.bytes.length)
|
|
263
|
+
return null;
|
|
264
|
+
const view = new DataView(this.bytes.buffer, this.bytes.byteOffset + this.off, 4);
|
|
265
|
+
const len = view.getUint32(0, false);
|
|
266
|
+
this.off += 4;
|
|
267
|
+
if (this.off + len > this.bytes.length)
|
|
268
|
+
return null;
|
|
269
|
+
const out = this.bytes.slice(this.off, this.off + len);
|
|
270
|
+
this.off += len;
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
remaining() {
|
|
274
|
+
return this.bytes.length - this.off;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/** Non-negative bigint from its unsigned big-endian bytes. */
|
|
278
|
+
function bytesToBigint(bytes) {
|
|
279
|
+
let n = 0n;
|
|
280
|
+
for (const b of bytes)
|
|
281
|
+
n = (n << 8n) | BigInt(b);
|
|
282
|
+
return n;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Canonical invalidation identity for an exact (session,generation) pair, using
|
|
286
|
+
* the SAME length-delimited scheme so two pairs can only match by exact byte
|
|
287
|
+
* equality (never by string prefix — `a` never matches `aa`). Returned as hex
|
|
288
|
+
* for use as a registry key.
|
|
289
|
+
*/
|
|
290
|
+
export function invalidationKey(session, generation) {
|
|
291
|
+
const sessionF = encodeLengthDelimited(strBytes(session));
|
|
292
|
+
const genF = encodeLengthDelimited(bigintBytes(generation));
|
|
293
|
+
return "inv:" + Buffer.from(concat([sessionF, genF])).toString("hex");
|
|
294
|
+
}
|
|
295
|
+
/** One deterministic SHA-256 (hex) digest over the canonical key bytes. */
|
|
296
|
+
export function keyDigest(key) {
|
|
297
|
+
return `sha256:${createHash("sha256").update(Buffer.from(encodeRouterKeyV2(key).slice(4), "hex")).digest("hex")}`;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Shared invalidation-capability seam (narrow delegate, no routing rewrite).
|
|
301
|
+
* Records the EXACT (session,generation) identity and, when `enabled`, best-effort
|
|
302
|
+
* emits `vector_cortex_router_generation_invalidated`. Falsy `enabled` (flag OFF)
|
|
303
|
+
* is a strict no-op so the predecessor keying is byte-identical to pre-sprint.
|
|
304
|
+
* Exactness is enforced by `invalidationKey` (length-delimited, never a string
|
|
305
|
+
* prefix — `a` never matches `aa`), the same identity M6 validates.
|
|
306
|
+
*/
|
|
307
|
+
export function routerGenerationInvalidationSeam(key, enabled, emit) {
|
|
308
|
+
if (!enabled)
|
|
309
|
+
return;
|
|
310
|
+
const identity = invalidationKey(key.session, key.generation);
|
|
311
|
+
if (emit) {
|
|
312
|
+
try {
|
|
313
|
+
emit("vector_cortex_router_generation_invalidated", {
|
|
314
|
+
session: key.session,
|
|
315
|
+
generation: key.generation.toString(),
|
|
316
|
+
key: identity,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
/* non-fatal observability — never break the agent loop */
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** Registered TOP query conformance ID range (TOP-021..030). */
|
|
325
|
+
export const TOP_QUERY_IDS = [
|
|
326
|
+
"TOP-021",
|
|
327
|
+
"TOP-022",
|
|
328
|
+
"TOP-023",
|
|
329
|
+
"TOP-024",
|
|
330
|
+
"TOP-025",
|
|
331
|
+
"TOP-026",
|
|
332
|
+
"TOP-027",
|
|
333
|
+
"TOP-028",
|
|
334
|
+
"TOP-029",
|
|
335
|
+
"TOP-030",
|
|
336
|
+
];
|
|
337
|
+
/** Registered named TOP-query conformance IDs. */
|
|
338
|
+
export const TOP_QUERY_NAMED_IDS = [
|
|
339
|
+
"TOP-QUERY-003",
|
|
340
|
+
"M6-KEY-001",
|
|
341
|
+
"M6-STALE-002",
|
|
342
|
+
];
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/migrations/router-generation-v2.ts — M6 copy/validate/switch
|
|
3
|
+
* migration (VC3C).
|
|
4
|
+
*
|
|
5
|
+
* M6 versions the tiered router's query set: the predecessor routed on ad-hoc
|
|
6
|
+
* per-session keys (no generation identity); VC3C routes on the structured,
|
|
7
|
+
* length-delimited `RouterKeyV2`. M6 copies the old per-session query set into
|
|
8
|
+
* v2 `RouterGenV2Row`s BESIDE the legacy state, validates, and only then
|
|
9
|
+
* atomically switches the active version pointer — the same copy/validate/
|
|
10
|
+
* switch + resume contract as M4.
|
|
11
|
+
*
|
|
12
|
+
* - copy: resumable per (session, old key) — an interrupted run resumes
|
|
13
|
+
* without duplicate rows or active-pointer drift.
|
|
14
|
+
* - validate: every old query key has exactly one v2 row, every row's key is
|
|
15
|
+
* canonical + re-hashes, and — the VC3C-specific invariant — the new query
|
|
16
|
+
* set is partition-preserving: a row derived from an old key of session S
|
|
17
|
+
* MUST carry session S. A row whose session differs is rejected as
|
|
18
|
+
* cross-session eviction (`M6_CROSS_SESSION_EVICTION`): one session may
|
|
19
|
+
* never lose or steal another session's query identity.
|
|
20
|
+
* - switch: atomically flips the ACTIVE VERSION pointer from 1 (legacy) to 2
|
|
21
|
+
* (router-generation-v2) only after a verified copy; interruption keeps the
|
|
22
|
+
* old active pointer.
|
|
23
|
+
*
|
|
24
|
+
* Pure logic over an injected M6Host (deterministic + testable; no console).
|
|
25
|
+
* PREVENT-002/011/PI-004 honored.
|
|
26
|
+
*/
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
import { encodeRouterKeyV2, decodeRouterKeyV2, } from "../topology/query.js";
|
|
29
|
+
/** The active-version value the v2 router generation pointer is switched to. */
|
|
30
|
+
export const ROUTER_GEN_V2_VERSION = 2;
|
|
31
|
+
/** The legacy (predecessor) active-version value before any verified switch. */
|
|
32
|
+
export const ROUTER_GEN_LEGACY_VERSION = 1;
|
|
33
|
+
/** M6 failure codes. */
|
|
34
|
+
export const M6_FAIL = {
|
|
35
|
+
VERSION_MISMATCH: "M6_VERSION_MISMATCH",
|
|
36
|
+
COPY_PARTIAL: "M6_COPY_PARTIAL",
|
|
37
|
+
COUNT_MISMATCH: "M6_COUNT_MISMATCH",
|
|
38
|
+
DIGEST_MISMATCH: "M6_DIGEST_MISMATCH",
|
|
39
|
+
BAD_OLD_KEY: "M6_BAD_OLD_KEY",
|
|
40
|
+
CROSS_SESSION_EVICTION: "M6_CROSS_SESSION_EVICTION",
|
|
41
|
+
SWITCH_PRECONDITION: "M6_SWITCH_PRECONDITION",
|
|
42
|
+
};
|
|
43
|
+
/** Derive one v2 row from an old key (authoritative bytes). */
|
|
44
|
+
export function deriveRouterGenRow(host, session, oldKey) {
|
|
45
|
+
const parsed = host.parseOldKey(session, oldKey);
|
|
46
|
+
if (!parsed.ok)
|
|
47
|
+
return { ok: false, session, oldKey };
|
|
48
|
+
const key = parsed.key;
|
|
49
|
+
// Cross-session guard at COPY time: the structured key's session must equal
|
|
50
|
+
// the slot session. A key that claims a different session is never copied.
|
|
51
|
+
if (key.session !== session)
|
|
52
|
+
return { ok: false, session, oldKey };
|
|
53
|
+
const encoded = encodeRouterKeyV2(key);
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
row: {
|
|
57
|
+
session,
|
|
58
|
+
generation: key.generation,
|
|
59
|
+
sourceStart: key.sourceStart,
|
|
60
|
+
sourceEnd: key.sourceEnd,
|
|
61
|
+
algorithm: key.algorithm,
|
|
62
|
+
key: encoded,
|
|
63
|
+
digest: sha256Hex(Buffer.from(encoded.slice(4), "hex")),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function sha256Hex(bytes) {
|
|
68
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Copy the full old query set into v2 rows (resumable): rows already persisted
|
|
72
|
+
* with a matching canonical key + digest are left untouched (no duplicates);
|
|
73
|
+
* absent or corrupt (wrong session / wrong key / stale digest / bad old key)
|
|
74
|
+
* slots are (re)written from the authoritative derivation. Returns the delta
|
|
75
|
+
* written this call. The active pointer stays at the legacy version until a
|
|
76
|
+
* verified switch.
|
|
77
|
+
*/
|
|
78
|
+
export function m6Copy(host) {
|
|
79
|
+
const existing = new Map(host.existingV2().map((r) => [`${r.session}::${r.key}`, r]));
|
|
80
|
+
const wanted = [];
|
|
81
|
+
for (const session of host.sessions()) {
|
|
82
|
+
for (const oldKey of host.oldKeysOf(session)) {
|
|
83
|
+
const derived = deriveRouterGenRow(host, session, oldKey);
|
|
84
|
+
if (!derived.ok)
|
|
85
|
+
continue; // bad old key surfaced by verify, not lost silently
|
|
86
|
+
const stored = existing.get(`${session}::${derived.row.key}`);
|
|
87
|
+
if (stored !== undefined && rowMatches(stored, derived.row))
|
|
88
|
+
continue; // healthy
|
|
89
|
+
wanted.push(derived.row);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (wanted.length > 0)
|
|
93
|
+
host.putV2(wanted);
|
|
94
|
+
return { written: wanted, all: [...host.existingV2()] };
|
|
95
|
+
}
|
|
96
|
+
/** True when a persisted row already matches the authoritative derivation. */
|
|
97
|
+
function rowMatches(stored, fresh) {
|
|
98
|
+
return (stored.session === fresh.session &&
|
|
99
|
+
stored.generation === fresh.generation &&
|
|
100
|
+
stored.sourceStart === fresh.sourceStart &&
|
|
101
|
+
stored.sourceEnd === fresh.sourceEnd &&
|
|
102
|
+
stored.algorithm === fresh.algorithm &&
|
|
103
|
+
stored.key === fresh.key &&
|
|
104
|
+
stored.digest === fresh.digest);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Verify the migrated query set: every old query key across every session has a
|
|
108
|
+
* v2 row (exact-once), every row's key is canonical + re-hashes, and the new set
|
|
109
|
+
* is partition-preserving — every row's session is exactly the session of the
|
|
110
|
+
* old key it was derived from (rejects cross-session eviction). Never mutates.
|
|
111
|
+
*
|
|
112
|
+
* Comparison of old/new query sets is exact-by-identity (structured key bytes),
|
|
113
|
+
* never by string prefix.
|
|
114
|
+
*/
|
|
115
|
+
export function m6Verify(host) {
|
|
116
|
+
const codes = [];
|
|
117
|
+
const stored = host.existingV2();
|
|
118
|
+
// Expected slot inventory: (session, canonical key) for every derivable old key.
|
|
119
|
+
const expectedSlots = [];
|
|
120
|
+
const badOldKeys = [];
|
|
121
|
+
for (const session of host.sessions()) {
|
|
122
|
+
for (const oldKey of host.oldKeysOf(session)) {
|
|
123
|
+
const parsed = host.parseOldKey(session, oldKey);
|
|
124
|
+
if (!parsed.ok) {
|
|
125
|
+
badOldKeys.push({ session, oldKey });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (parsed.key.session !== session) {
|
|
129
|
+
// A structured key that claims a different session is a cross-session
|
|
130
|
+
// eviction attempt — never migrated, never evicted.
|
|
131
|
+
codes.push(M6_FAIL.CROSS_SESSION_EVICTION);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
expectedSlots.push({ session, oldKey });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (badOldKeys.length > 0)
|
|
138
|
+
codes.push(M6_FAIL.BAD_OLD_KEY);
|
|
139
|
+
// Map stored rows by their (session,key) identity.
|
|
140
|
+
const byIdentity = new Map();
|
|
141
|
+
const duplicateCheck = new Map();
|
|
142
|
+
for (const r of stored) {
|
|
143
|
+
const ident = `${r.session}::${r.key}`;
|
|
144
|
+
byIdentity.set(ident, r);
|
|
145
|
+
duplicateCheck.set(ident, (duplicateCheck.get(ident) ?? 0) + 1);
|
|
146
|
+
}
|
|
147
|
+
// Each expected slot must be present exactly once, with a healthy row.
|
|
148
|
+
for (const slot of expectedSlots) {
|
|
149
|
+
const parsed = host.parseOldKey(slot.session, slot.oldKey);
|
|
150
|
+
if (!parsed.ok)
|
|
151
|
+
continue;
|
|
152
|
+
const expectedKey = encodeRouterKeyV2(parsed.key);
|
|
153
|
+
const ident = `${slot.session}::${expectedKey}`;
|
|
154
|
+
const count = duplicateCheck.get(ident) ?? 0;
|
|
155
|
+
if (count === 0)
|
|
156
|
+
codes.push(M6_FAIL.COPY_PARTIAL);
|
|
157
|
+
if (count > 1)
|
|
158
|
+
codes.push(M6_FAIL.COUNT_MISMATCH);
|
|
159
|
+
}
|
|
160
|
+
// Row health: digest re-hashes + session partition-preserving.
|
|
161
|
+
for (const r of stored) {
|
|
162
|
+
if (!r.key.startsWith("rk2:"))
|
|
163
|
+
codes.push(M6_FAIL.DIGEST_MISMATCH);
|
|
164
|
+
const expectedDigest = sha256Hex(Buffer.from(r.key.slice(4), "hex"));
|
|
165
|
+
if (r.digest !== expectedDigest)
|
|
166
|
+
codes.push(M6_FAIL.DIGEST_MISMATCH);
|
|
167
|
+
// The row's own structured key must decode and agree with its declared fields.
|
|
168
|
+
const dec = decodeRow(r);
|
|
169
|
+
if (!dec)
|
|
170
|
+
codes.push(M6_FAIL.DIGEST_MISMATCH);
|
|
171
|
+
}
|
|
172
|
+
// No stored row may be an orphan (no corresponding old key anywhere).
|
|
173
|
+
const expectedIdentities = new Set();
|
|
174
|
+
for (const slot of expectedSlots) {
|
|
175
|
+
const parsed = host.parseOldKey(slot.session, slot.oldKey);
|
|
176
|
+
if (!parsed.ok)
|
|
177
|
+
continue;
|
|
178
|
+
expectedIdentities.add(`${slot.session}::${encodeRouterKeyV2(parsed.key)}`);
|
|
179
|
+
}
|
|
180
|
+
for (const ident of byIdentity.keys()) {
|
|
181
|
+
if (!expectedIdentities.has(ident))
|
|
182
|
+
codes.push(M6_FAIL.COUNT_MISMATCH);
|
|
183
|
+
}
|
|
184
|
+
const ok = codes.length === 0;
|
|
185
|
+
return { ok, codes: dedupe(codes) };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Self-consistent decode of a stored row: its canonical key decodes to a
|
|
189
|
+
* RouterKeyV2 whose five fields equal the row's declared fields. A row whose key
|
|
190
|
+
* doesn't round-trip against its own declared session/generation/range/algorithm
|
|
191
|
+
* is corrupt (cannot be trusted as the query set's identity).
|
|
192
|
+
*/
|
|
193
|
+
function decodeRow(r) {
|
|
194
|
+
if (!r.key.startsWith("rk2:"))
|
|
195
|
+
return false;
|
|
196
|
+
const hex = r.key.slice(4);
|
|
197
|
+
if (!/^[0-9a-f]+$/.test(hex) || hex.length % 2 !== 0)
|
|
198
|
+
return false;
|
|
199
|
+
const dec = decodeRouterKeyV2(r.key);
|
|
200
|
+
if (!dec.ok)
|
|
201
|
+
return false;
|
|
202
|
+
return (dec.key.session === r.session &&
|
|
203
|
+
dec.key.generation === r.generation &&
|
|
204
|
+
dec.key.sourceStart === r.sourceStart &&
|
|
205
|
+
dec.key.sourceEnd === r.sourceEnd &&
|
|
206
|
+
dec.key.algorithm === r.algorithm);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Switch the active version to 2. Only call after `m6Verify` reports ok; a
|
|
210
|
+
* crash/return before switch leaves the legacy pointer active (interruption
|
|
211
|
+
* keeps old authority, resumable idempotently).
|
|
212
|
+
*/
|
|
213
|
+
export function m6Switch(host) {
|
|
214
|
+
if (host.activeVersion() !== ROUTER_GEN_LEGACY_VERSION)
|
|
215
|
+
return;
|
|
216
|
+
host.switchToV2();
|
|
217
|
+
}
|
|
218
|
+
/** Full M6 lifecycle: copy -> verify -> switch; returns the verify result. */
|
|
219
|
+
export function migrateRouterGenerationV2(host) {
|
|
220
|
+
m6Copy(host);
|
|
221
|
+
const v = m6Verify(host);
|
|
222
|
+
if (v.ok)
|
|
223
|
+
m6Switch(host);
|
|
224
|
+
return v;
|
|
225
|
+
}
|
|
226
|
+
/** Registered M6 conformance ID range (M6-001..012). */
|
|
227
|
+
export const M6_IDS = [
|
|
228
|
+
"M6-001",
|
|
229
|
+
"M6-002",
|
|
230
|
+
"M6-003",
|
|
231
|
+
"M6-004",
|
|
232
|
+
"M6-005",
|
|
233
|
+
"M6-006",
|
|
234
|
+
"M6-007",
|
|
235
|
+
"M6-008",
|
|
236
|
+
"M6-009",
|
|
237
|
+
"M6-010",
|
|
238
|
+
"M6-011",
|
|
239
|
+
"M6-012",
|
|
240
|
+
];
|
|
241
|
+
/** Registered named M6 conformance IDs. */
|
|
242
|
+
export const M6_NAMED_IDS = [
|
|
243
|
+
"M6-KEY-001",
|
|
244
|
+
"M6-STALE-002",
|
|
245
|
+
];
|
|
246
|
+
function dedupe(codes) {
|
|
247
|
+
const out = [];
|
|
248
|
+
for (const c of codes)
|
|
249
|
+
if (!out.includes(c))
|
|
250
|
+
out.push(c);
|
|
251
|
+
return out;
|
|
252
|
+
}
|