pi-mega-compact 0.19.3 → 0.20.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 +12 -0
- package/dist/config.js +1 -1
- package/dist/extensions/dashboard-server/routes-cache.test/_helpers.js +2 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +1 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-helpers.js +100 -0
- package/dist/extensions/dashboard-server/routes-vector-cortex-shards.js +48 -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 +39 -16
- package/dist/extensions/dashboard-server/tailscale.js +32 -0
- package/dist/src/config/vector-cortex.js +12 -0
- package/dist/src/config.js +1 -1
- package/dist/src/vector-cortex/shards/exact.js +134 -0
- package/dist/src/vector-cortex/shards/manifest.js +237 -0
- package/dist/src/vector-cortex/shards/semantic.js +137 -0
- package/dist/src/vector-cortex/shards/types.js +51 -0
- package/dist/vector-cortex/shards/exact.js +134 -0
- package/dist/vector-cortex/shards/manifest.js +237 -0
- package/dist/vector-cortex/shards/semantic.js +137 -0
- package/dist/vector-cortex/shards/types.js +51 -0
- package/extensions/dashboard-client/dist/assets/{AreaChart-DqiFSnDx.js → AreaChart-n4z5w9Ox.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-DqiFSnDx.js.map → AreaChart-n4z5w9Ox.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{BarChart-Cqg-PQMK.js → BarChart-BLGjWWBr.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{BarChart-Cqg-PQMK.js.map → BarChart-BLGjWWBr.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-BPNs0xIF.js → CacheTab-CAnrKKVK.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-BPNs0xIF.js.map → CacheTab-CAnrKKVK.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-BQf7OGkf.js → EventsTab-D-GL7Ngq.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-BQf7OGkf.js.map → EventsTab-D-GL7Ngq.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-B14TaSPN.js → HealthTab-C4x5qQ8s.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-B14TaSPN.js.map → HealthTab-C4x5qQ8s.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-D8lkRoa1.js → MaintenanceTab-BSPs0eq5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-D8lkRoa1.js.map → MaintenanceTab-BSPs0eq5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BOWtGfpx.js → MemoryMapTab-BWEUtTSu.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-BOWtGfpx.js.map → MemoryMapTab-BWEUtTSu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-7XI9cWaX.js → MetricsTab-Dhm1c3Cx.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-7XI9cWaX.js.map → MetricsTab-Dhm1c3Cx.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-C9KaGYvh.js → OverviewTab-Cm6cySiQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-C9KaGYvh.js.map → OverviewTab-Cm6cySiQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-BwESNhox.js → ReposTab-DxLN1N4K.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-BwESNhox.js.map → ReposTab-DxLN1N4K.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DKH55Rda.js → SessionsTab-CZuI4ISW.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-DKH55Rda.js.map → SessionsTab-CZuI4ISW.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-zDxf-nJd.js → SetupTab-C_0VOomm.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-zDxf-nJd.js.map → SetupTab-C_0VOomm.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-C9isog4L.js → TimeSavedCard-BhbaRLmi.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-C9isog4L.js.map → TimeSavedCard-BhbaRLmi.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-ciI6ydvj.js → TurnsTab-CXJmbjje.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-ciI6ydvj.js.map → TurnsTab-CXJmbjje.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BGe9hu3r.js +2 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-BGe9hu3r.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{WikiTab-DcYHc734.js → WikiTab-C5uvMCzp.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-DcYHc734.js.map → WikiTab-C5uvMCzp.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-DF8lx4bt.js → button-Cl_fxJcg.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-DF8lx4bt.js.map → button-Cl_fxJcg.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-DIoDheSZ.js → card-BH8Le5YM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-DIoDheSZ.js.map → card-BH8Le5YM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BFRzeYX3.js → generateCategoricalChart-sZ6ZNtBi.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-BFRzeYX3.js.map → generateCategoricalChart-sZ6ZNtBi.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DSZON0Yi.js → index-CX9zv8cJ.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-DSZON0Yi.js.map → index-CX9zv8cJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{switch-l4qo22mU.js → switch-CXJhcjhM.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-l4qo22mU.js.map → switch-CXJhcjhM.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-DPn6td1_.js → toggle-CoLtIc6y.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-DPn6td1_.js.map → toggle-CoLtIc6y.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-BLmolqTJ.js → useSSE-fSXho606.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-BLmolqTJ.js.map → useSSE-fSXho606.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 +35 -0
- package/extensions/dashboard-client/src/types/vector-cortex.ts +13 -0
- package/extensions/dashboard-server/api-contracts/vector-cortex.ts +24 -0
- package/extensions/dashboard-server/routes-cache.test/_helpers.ts +2 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
- package/extensions/dashboard-server/routes-vector-cortex-helpers.ts +110 -0
- package/extensions/dashboard-server/routes-vector-cortex-shards.ts +57 -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 +44 -17
- package/extensions/dashboard-server/tailscale.ts +32 -0
- package/package.json +1 -1
- package/src/config/vector-cortex.ts +13 -0
- package/src/config.ts +1 -0
- package/src/vector-cortex/shards/exact.ts +141 -0
- package/src/vector-cortex/shards/manifest.ts +276 -0
- package/src/vector-cortex/shards/semantic.ts +156 -0
- package/src/vector-cortex/shards/types.ts +212 -0
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-qTZ7NWng.js +0 -2
- package/extensions/dashboard-client/dist/assets/VectorCortexTab-qTZ7NWng.js.map +0 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes-vector-cortex-helpers.ts — shared spawn-and-fetch harness for the
|
|
3
|
+
* /api/vector-cortex/* route test files.
|
|
4
|
+
*
|
|
5
|
+
* The dashboard-server.js main block fires when the entry argv includes
|
|
6
|
+
* "dashboard-server", so tests under dist/extensions/dashboard-server/ MUST
|
|
7
|
+
* spawn the server as a child process rather than import launchDashboardServer.
|
|
8
|
+
* Extracted from routes-vector-cortex.test.ts so per-sprint route test files
|
|
9
|
+
* can share the harness without each re-implementing it.
|
|
10
|
+
*
|
|
11
|
+
* Reader-only: no payload bytes are ever returned by the routes under test.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { rmSync, readFileSync, existsSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
/** Locate the repo root (the directory holding `conformance/vector-cortex`). */
|
|
20
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
export function repoRoot(from: string = HERE): string {
|
|
22
|
+
let dir = from;
|
|
23
|
+
for (let i = 0; i < 8; i++) {
|
|
24
|
+
if (existsSync(join(dir, "conformance", "vector-cortex"))) return dir;
|
|
25
|
+
const next = dirname(dir);
|
|
26
|
+
if (next === dir) break;
|
|
27
|
+
dir = next;
|
|
28
|
+
}
|
|
29
|
+
throw new Error("conformance corpus not found above " + from);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const REPO_ROOT = repoRoot();
|
|
33
|
+
|
|
34
|
+
/** SHA-256 of the REAL committed ModelManifestV1 — the health card's digest. */
|
|
35
|
+
export function realManifestDigest(): string {
|
|
36
|
+
// guardrails-allow PREVENT-PI-004: local committed asset filesystem read (loopback)
|
|
37
|
+
return createHash("sha256")
|
|
38
|
+
.update(readFileSync(join(REPO_ROOT, "assets", "vector-cortex", "encoder-v1", "manifest.json")))
|
|
39
|
+
.digest("hex");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const SERVER_ENTRY = new URL("./server.js", import.meta.url).pathname;
|
|
43
|
+
|
|
44
|
+
export function waitFor(
|
|
45
|
+
cond: () => boolean | Promise<boolean>,
|
|
46
|
+
timeoutMs = 6000,
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const tick = async () => {
|
|
51
|
+
if (await cond()) return resolve();
|
|
52
|
+
if (Date.now() - start > timeoutMs) return reject(new Error("timeout"));
|
|
53
|
+
setTimeout(tick, 50);
|
|
54
|
+
};
|
|
55
|
+
tick();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function withServer<T>(
|
|
60
|
+
port: string,
|
|
61
|
+
dir: string,
|
|
62
|
+
fn: (port: number) => Promise<T>,
|
|
63
|
+
): Promise<T> {
|
|
64
|
+
process.env.MEGACOMPACT_DASHBOARD_PORT = port;
|
|
65
|
+
// Pin loopback so the spawn-and-fetch harness stays hermetic even on a
|
|
66
|
+
// machine with tailscale0 (the production auto-bind would otherwise grab the
|
|
67
|
+
// tailnet IP and the localhost probe would connection-refuse). Tests that
|
|
68
|
+
// need a different host can set MEGACOMPACT_DASHBOARD_HOST before calling.
|
|
69
|
+
process.env.MEGACOMPACT_DASHBOARD_HOST = "127.0.0.1";
|
|
70
|
+
const child = spawn(process.execPath, [SERVER_ENTRY, dir], {
|
|
71
|
+
stdio: "ignore",
|
|
72
|
+
env: {
|
|
73
|
+
...process.env,
|
|
74
|
+
MEGACOMPACT_INDEX_DIR: dir,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
await waitFor(async () => {
|
|
79
|
+
try {
|
|
80
|
+
const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
|
|
81
|
+
const res = await fetch(`http://localhost:${raw.port}/api/version`);
|
|
82
|
+
return res.ok;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const raw = JSON.parse(readFileSync(join(dir, "port.pid"), "utf-8"));
|
|
88
|
+
return await fn(raw.port);
|
|
89
|
+
} finally {
|
|
90
|
+
child.kill("SIGTERM");
|
|
91
|
+
delete process.env.MEGACOMPACT_DASHBOARD_PORT;
|
|
92
|
+
delete process.env.MEGACOMPACT_DASHBOARD_HOST;
|
|
93
|
+
rmSync(dir, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Seed redacted metric rows directly on disk so the server's reader sees them. */
|
|
98
|
+
export async function seedEval(dir: string): Promise<void> {
|
|
99
|
+
const { appendEvalRow } = await import(
|
|
100
|
+
"../../src/vector-cortex/eval/persist.js"
|
|
101
|
+
);
|
|
102
|
+
// Buckets (inclusive): 1,5,10,25,50,100,250. 1ms→cell0, 12ms→cell3
|
|
103
|
+
// (<=25), 250ms→cell6, 300ms→overflow.
|
|
104
|
+
appendEvalRow(dir, [
|
|
105
|
+
{ session: "s1", seq: 1, event: "encode", value: 1, unit: "ms", mode: "A" },
|
|
106
|
+
{ session: "s1", seq: 2, event: "encode", value: 250, unit: "ms", mode: "A" },
|
|
107
|
+
{ session: "s1", seq: 3, event: "encode", value: 300, unit: "ms", mode: "A" },
|
|
108
|
+
{ session: "s1", seq: 4, event: "encode", value: 12, unit: "ms", mode: "A" },
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-server/routes-vector-cortex-shards.ts — VC4A dual-tier shard
|
|
3
|
+
* dashboard route.
|
|
4
|
+
*
|
|
5
|
+
* Reader-only GET /api/vector-cortex/shards returning a COUNT/BYTE aggregate
|
|
6
|
+
* (semantic shards, exact shards, protected byte total) — never shard payloads,
|
|
7
|
+
* verbatim exact bytes, or prompt text. Flag-gated on MEGACOMPACT_VC4A:
|
|
8
|
+
* `enabled:false` when off (byte-identical to the pre-VC4A predecessor).
|
|
9
|
+
*
|
|
10
|
+
* The VC4A shard partition is PURE IN-MEMORY logic in this sprint (it has no
|
|
11
|
+
* durable manifest store yet), so the aggregate reports the current status
|
|
12
|
+
* truthfully: `enabled` reflects the flag, and the count/byte fields are zero
|
|
13
|
+
* until a future sprint persists a staged manifest. This route is the seam that
|
|
14
|
+
* sprint will populate. Non-fatal: a missing state dir degrades to
|
|
15
|
+
* `enabled:false`.
|
|
16
|
+
*
|
|
17
|
+
* Guardrails: PREVENT-PI-004 (local filesystem read only / in-process state),
|
|
18
|
+
* PREVENT-011 (no `any`), reader-only aggregate (counts/bytes only).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
22
|
+
import type { RouteContext } from "./routes-core.js";
|
|
23
|
+
import { VC4A_ENABLED } from "../../src/config.js";
|
|
24
|
+
import { sendJson } from "./routes-vector-cortex-shared.js";
|
|
25
|
+
import type { VectorCortexShardsView } from "./api-contracts/vector-cortex.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reader-only GET /api/vector-cortex/shards (VC4A).
|
|
29
|
+
*/
|
|
30
|
+
export function handleVectorCortexShards(
|
|
31
|
+
req: IncomingMessage,
|
|
32
|
+
res: ServerResponse,
|
|
33
|
+
_ctx: RouteContext,
|
|
34
|
+
): boolean {
|
|
35
|
+
const url = req.url ?? "";
|
|
36
|
+
const path = url.split("?")[0] ?? url;
|
|
37
|
+
if (path !== "/api/vector-cortex/shards") return false;
|
|
38
|
+
if (req.method !== "GET") {
|
|
39
|
+
// Reader-only path: cannot be "off" without a GET; it is genuinely read-only.
|
|
40
|
+
sendJson(res, 405, { error: "method_not_allowed" });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const enabled = VC4A_ENABLED();
|
|
45
|
+
const body: VectorCortexShardsView = {
|
|
46
|
+
enabled,
|
|
47
|
+
// No durable shard manifest is staged yet in this sprint (pure in-memory
|
|
48
|
+
// partition), so the aggregates are truthfully zero.
|
|
49
|
+
semanticCount: 0,
|
|
50
|
+
exactCount: 0,
|
|
51
|
+
byteTotal: 0,
|
|
52
|
+
protectedByteTotal: 0,
|
|
53
|
+
updatedAt: new Date().toISOString(),
|
|
54
|
+
};
|
|
55
|
+
sendJson(res, 200, body);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
@@ -19,3 +19,4 @@ export { handleVectorCortexHealth, handleVectorCortexBreakersReset } from "./rou
|
|
|
19
19
|
export { handleVectorCortexLedger } from "./routes-vector-cortex-ledger.js";
|
|
20
20
|
export { handleVectorCortexTopology } from "./routes-vector-cortex-topology.js";
|
|
21
21
|
export { handleVectorCortexQuery } from "./routes-vector-cortex-query.js";
|
|
22
|
+
export { handleVectorCortexShards } from "./routes-vector-cortex-shards.js";
|
|
@@ -25,6 +25,7 @@ import { createRequire } from "node:module";
|
|
|
25
25
|
|
|
26
26
|
import { NEW_UI } from "../../src/config.js";
|
|
27
27
|
import { log, setLogPath, setDashboardServerVersion } from "./state.js";
|
|
28
|
+
import { detectTailscaleIP } from "./tailscale.js";
|
|
28
29
|
import {
|
|
29
30
|
buildRouteContext,
|
|
30
31
|
handleIndex,
|
|
@@ -62,6 +63,7 @@ import {
|
|
|
62
63
|
handleVectorCortexLedger,
|
|
63
64
|
handleVectorCortexTopology,
|
|
64
65
|
handleVectorCortexQuery,
|
|
66
|
+
handleVectorCortexShards,
|
|
65
67
|
handleStatic,
|
|
66
68
|
} from "./routes.js";
|
|
67
69
|
|
|
@@ -231,15 +233,31 @@ export async function launchDashboardServer(
|
|
|
231
233
|
detectCrossRepoDrift,
|
|
232
234
|
});
|
|
233
235
|
|
|
236
|
+
// Bind host resolution: explicit MEGACOMPACT_DASHBOARD_HOST override wins;
|
|
237
|
+
// otherwise auto-bind to the Tailscale interface (tailnet access); fall back
|
|
238
|
+
// to loopback when Tailscale is absent. 0.0.0.0 means "all interfaces" and
|
|
239
|
+
// opts into permissive CORS below.
|
|
240
|
+
const host =
|
|
241
|
+
process.env.MEGACOMPACT_DASHBOARD_HOST ??
|
|
242
|
+
detectTailscaleIP() ??
|
|
243
|
+
"127.0.0.1";
|
|
244
|
+
const allInterfaces = host === "0.0.0.0";
|
|
245
|
+
|
|
246
|
+
// guardrails-allow PREVENT-PI-004: optional, user-triggered /dashboard server (tailnet-local via Tailscale mesh or loopback); no remote outbound calls.
|
|
234
247
|
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
235
|
-
//
|
|
236
|
-
//
|
|
248
|
+
// CORS for local access — restricted to loopback/same-origin tailnet
|
|
249
|
+
// origins (the dashboard server binds to localhost or the tailscale IP).
|
|
250
|
+
// With MEGACOMPACT_DASHBOARD_HOST=0.0.0.0 the user opted into
|
|
251
|
+
// all-interfaces binding, so we allow any origin.
|
|
237
252
|
const origin = req.headers.origin;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
253
|
+
const originAllowed =
|
|
254
|
+
allInterfaces ||
|
|
255
|
+
(typeof origin === "string" &&
|
|
256
|
+
new RegExp(
|
|
257
|
+
`^http://(localhost|127\\.0\\.0\\.1|::1|${host.replace(/\./g, "\\.")})(:\\d+)?$`,
|
|
258
|
+
).test(origin));
|
|
259
|
+
if (originAllowed) {
|
|
260
|
+
res.setHeader("Access-Control-Allow-Origin", origin ?? "*");
|
|
243
261
|
res.setHeader("Vary", "Origin");
|
|
244
262
|
}
|
|
245
263
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS");
|
|
@@ -287,6 +305,7 @@ export async function launchDashboardServer(
|
|
|
287
305
|
if (handleVectorCortexLedger(req, res, ctx)) return;
|
|
288
306
|
if (handleVectorCortexTopology(req, res, ctx)) return;
|
|
289
307
|
if (handleVectorCortexQuery(req, res, ctx)) return;
|
|
308
|
+
if (handleVectorCortexShards(req, res, ctx)) return;
|
|
290
309
|
handleStatic(req, res, ctx);
|
|
291
310
|
});
|
|
292
311
|
|
|
@@ -310,20 +329,28 @@ export async function launchDashboardServer(
|
|
|
310
329
|
}
|
|
311
330
|
});
|
|
312
331
|
|
|
313
|
-
server.listen(port,
|
|
314
|
-
|
|
315
|
-
|
|
332
|
+
server.listen(port, host, () => {
|
|
333
|
+
// When bound to the loopback, keep the legacy localhost URL so the
|
|
334
|
+
// user's browser (which resolves localhost → ::1 or 127.0.0.1) works.
|
|
335
|
+
// When bound to a tailnet/tailscale IP, surface that address instead.
|
|
336
|
+
const url =
|
|
337
|
+
host === "127.0.0.1"
|
|
338
|
+
? `http://localhost:${port}`
|
|
339
|
+
: `http://${host}:${port}`; // guardrails-allow PREVENT-PI-004: dashboard URL (tailnet-local via Tailscale mesh or loopback)
|
|
340
|
+
log("server running", { url, host });
|
|
316
341
|
// eslint-disable-next-line no-console
|
|
317
342
|
console.log(`[mega-compact] dashboard server running: ${url}`);
|
|
318
343
|
|
|
319
|
-
// v0.8.2: also bind the IPv6 loopback (::1)
|
|
320
|
-
// resolves to ::1 first (see
|
|
321
|
-
// browser hit ::1:port and
|
|
322
|
-
// (loopback-only) means BOTH
|
|
323
|
-
//
|
|
344
|
+
// v0.8.2: also bind the IPv6 loopback (::1) when bound to the IPv4
|
|
345
|
+
// loopback. On many systems `localhost` resolves to ::1 first (see
|
|
346
|
+
// /etc/hosts), so an IPv4-only bind makes the browser hit ::1:port and
|
|
347
|
+
// get connection refused. PREVENT-PI-004 (loopback-only) means BOTH
|
|
348
|
+
// 127.0.0.1 and ::1. When bound to a tailscale/0.0.0.0 host, we do NOT
|
|
349
|
+
// bind ::1 — the tailnet address is what the user reaches. Non-fatal:
|
|
350
|
+
// IPv4-only hosts or a ::1 already in use just skip the mirror.
|
|
324
351
|
let v6: ReturnType<typeof createServer> | undefined;
|
|
325
352
|
const v4Handler = server.listeners("request")[0];
|
|
326
|
-
if (v4Handler) {
|
|
353
|
+
if (host === "127.0.0.1" && v4Handler) {
|
|
327
354
|
v6 = createServer((r, s) =>
|
|
328
355
|
(v4Handler as (a: IncomingMessage, b: ServerResponse) => void).call(
|
|
329
356
|
server,
|
|
@@ -338,7 +365,7 @@ export async function launchDashboardServer(
|
|
|
338
365
|
message: e.message,
|
|
339
366
|
}),
|
|
340
367
|
);
|
|
341
|
-
v6.listen(port, "::1", () => log("ipv6 loopback bound", { port })); // guardrails-allow PREVENT-PI-004: IPv6 loopback (::1) mirror of the
|
|
368
|
+
v6.listen(port, "::1", () => log("ipv6 loopback bound", { port })); // guardrails-allow PREVENT-PI-004: IPv6 loopback (::1) mirror of the dashboard server
|
|
342
369
|
}
|
|
343
370
|
|
|
344
371
|
// Write port.pid
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { networkInterfaces } from "node:os";
|
|
2
|
+
|
|
1
3
|
export function setupTailscaleServe(port = 3000, httpsPort = 443): boolean {
|
|
2
4
|
// Tailscale serve: exposes localhost dashboard securely.
|
|
3
5
|
// Only activates when TAILSCALE_ENABLED=1 is set.
|
|
@@ -5,3 +7,33 @@ export function setupTailscaleServe(port = 3000, httpsPort = 443): boolean {
|
|
|
5
7
|
console.log(`[tailscale] Serve enabled on port ${port} (https:${httpsPort})`);
|
|
6
8
|
return true;
|
|
7
9
|
}
|
|
10
|
+
|
|
11
|
+
// CGNAT block used by Tailscale for client addresses (100.64.0.0/10).
|
|
12
|
+
const TAILSCALE_CGNAT_PREFIX = "100.";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Detect the IPv4 address Tailscale has assigned to this host, so the dashboard
|
|
16
|
+
* server can auto-bind to the tailnet interface and be reachable from other
|
|
17
|
+
* devices on the tailnet. Returns null when Tailscale is not present.
|
|
18
|
+
*
|
|
19
|
+
* The default Tailscale interface name is `tailscale0` on Linux and `Tailscale`
|
|
20
|
+
* on macOS/Windows. We prefer an address inside Tailscale's CGNAT range
|
|
21
|
+
* (100.64.0.0/10) when one exists; otherwise we return the first IPv4 address
|
|
22
|
+
* found on the interface.
|
|
23
|
+
*/
|
|
24
|
+
export function detectTailscaleIP(): string | null {
|
|
25
|
+
const ifaces = networkInterfaces();
|
|
26
|
+
const candidates: Array<string> = [];
|
|
27
|
+
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
28
|
+
if (name !== "tailscale0" && name !== "Tailscale") continue;
|
|
29
|
+
if (!addrs) continue;
|
|
30
|
+
for (const addr of addrs) {
|
|
31
|
+
if (addr.family === "IPv4") {
|
|
32
|
+
candidates.push(addr.address);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (candidates.length === 0) return null;
|
|
37
|
+
const cgnat = candidates.find((ip) => ip.startsWith(TAILSCALE_CGNAT_PREFIX));
|
|
38
|
+
return cgnat ?? candidates[0];
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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",
|
|
@@ -134,6 +134,19 @@ export const VC3B_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC3B");
|
|
|
134
134
|
*/
|
|
135
135
|
export const VC3C_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC3C");
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* VC4A — dual-tier shard contract (SemanticShardV1 / ExactShardV1 /
|
|
139
|
+
* ShardManifestV1).
|
|
140
|
+
* Default ON. `MEGACOMPACT_VC4A=0` disables and is byte-identical to the
|
|
141
|
+
* predecessor (mode C: exact anchors/current transcript only, no semantic or
|
|
142
|
+
* exact shard manifest is assembled or emitted, zero
|
|
143
|
+
* `vector_cortex_shard_manifest_built` / `vector_cortex_protected_span_rejected`
|
|
144
|
+
* emissions; the predecessor derived pointer and goldens are unchanged). The
|
|
145
|
+
* real consumers are the shard assembly seam (manifest.ts) and the reader-only
|
|
146
|
+
* dashboard shards aggregate view.
|
|
147
|
+
*/
|
|
148
|
+
export const VC4A_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC4A");
|
|
149
|
+
|
|
137
150
|
// ---------------------------------------------------------------------------
|
|
138
151
|
// Breaker state machine constants (TRIAD_RESILIENCE.md §breaker).
|
|
139
152
|
// Rolled numbers for one 60s window; VC0C consumes these at its breaker seam.
|
package/src/config.ts
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-cortex/shards/exact.ts — exact tier partition (VC4A).
|
|
3
|
+
*
|
|
4
|
+
* Carves the session's PROTECTED spans — every tool call/result pair, every
|
|
5
|
+
* anchor, every invalid UTF-8 event — into `ExactShardV1` payloads of the
|
|
6
|
+
* ORIGINAL bytes, verbatim and never normalized (SHD-UTF8-002: invalid bytes
|
|
7
|
+
* are exact-only and preserved unchanged — the codec's strict-fatal classifier
|
|
8
|
+
* means an invalid event has no lossy text to re-encode; we copy `originalBytes`
|
|
9
|
+
* directly). Each protected span is ATOMIC: a tool call/result pair is one
|
|
10
|
+
* `ProtectedSpan`, so no exact shard EVER splits a pair, even when the pair
|
|
11
|
+
* straddles the target-size boundary (SHD-PAIR-001).
|
|
12
|
+
*
|
|
13
|
+
* The partition walks the protected spans in canonical stream order (ascending
|
|
14
|
+
* first-seq), accrues each span's bytes, and closes an exact shard the moment
|
|
15
|
+
* adding the next atomic span would exceed `targetSize`. A single span over the
|
|
16
|
+
* budget still occupies exactly one shard — the pair case never splits.
|
|
17
|
+
*
|
|
18
|
+
* Triad: A = used with the semantic tier; B = the same exact spans preserved
|
|
19
|
+
* alongside extractive-derived semantic content; C = exact anchors/current
|
|
20
|
+
* transcript ONLY (this partition alone, with zero semantic shards).
|
|
21
|
+
*
|
|
22
|
+
* Pure/deterministic: hashes + array walking only, no storage, no console, no
|
|
23
|
+
* network (PREVENT-PI-004 / PREVENT-011).
|
|
24
|
+
*/
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import type { EventV2 } from "../ledger/types.js";
|
|
27
|
+
import { cumulativeOffsets } from "./semantic.js";
|
|
28
|
+
import type { ExactPartitionInput, ExactPartitionResult, ExactShardCase, ExactShardV1, ProtectedSpan } from "./types.js";
|
|
29
|
+
|
|
30
|
+
/** Deterministic exact-shard digest over the verbatim original bytes. */
|
|
31
|
+
function sealExactDigest(sessionId: string, bytes: Uint8Array): string {
|
|
32
|
+
const h = createHash("sha256");
|
|
33
|
+
h.update(`exact-shard-v1|${sessionId}|`);
|
|
34
|
+
h.update(bytes);
|
|
35
|
+
return `sha256:${h.digest("hex")}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Combine per-span cases into one shard case (invalid dominates, then tool-pair). */
|
|
39
|
+
function combineCase(span: ProtectedSpan, acc: ExactShardCase): ExactShardCase {
|
|
40
|
+
if (span.case === "invalid-utf8" || acc === "invalid-utf8") return "invalid-utf8";
|
|
41
|
+
if (span.case === "tool-pair" || acc === "tool-pair") return "tool-pair";
|
|
42
|
+
return "anchor";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Concatenate an ordered group of events' original bytes, verbatim. */
|
|
46
|
+
function concatBytes(events: readonly EventV2[]): Uint8Array {
|
|
47
|
+
let len = 0;
|
|
48
|
+
for (const e of events) len += e.originalBytes.length;
|
|
49
|
+
const out = new Uint8Array(len);
|
|
50
|
+
let off = 0;
|
|
51
|
+
for (const e of events) {
|
|
52
|
+
out.set(e.originalBytes, off);
|
|
53
|
+
off += e.originalBytes.length;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Partition the exact tier (task 3). Returns `ok:true` with a list of
|
|
60
|
+
* `ExactShardV1` whose (sorted) ranges are contiguous, non-overlapping and
|
|
61
|
+
* jointly cover every protected span exactly once, each preserving the original
|
|
62
|
+
* bytes verbatim. Defensive failures mirror the semantic tier: a bad target size
|
|
63
|
+
* rejects with `SHD_INVALID_TARGET_SIZE`; a protected span referencing an event
|
|
64
|
+
* outside the session's stream, or a mixed-session span, rejects with
|
|
65
|
+
* `SHD_CROSS_SESSION`.
|
|
66
|
+
*/
|
|
67
|
+
export function partitionExact(input: ExactPartitionInput): ExactPartitionResult {
|
|
68
|
+
const { sessionId, events, protectedSpans, targetSize } = input;
|
|
69
|
+
if (!Number.isFinite(targetSize) || targetSize <= 0) {
|
|
70
|
+
return { ok: false, code: "SHD_INVALID_TARGET_SIZE" };
|
|
71
|
+
}
|
|
72
|
+
const stream = new Set(events.map((e) => e.seq.toString()));
|
|
73
|
+
for (const span of protectedSpans) {
|
|
74
|
+
if (span.events.some((e) => e.sessionId !== sessionId || !stream.has(e.seq.toString()))) {
|
|
75
|
+
return { ok: false, code: "SHD_CROSS_SESSION" };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const offsets = cumulativeOffsets(events);
|
|
79
|
+
if (protectedSpans.length === 0) return { ok: true, shards: [] };
|
|
80
|
+
|
|
81
|
+
// Process protected spans in canonical stream order (ascending first-seq;
|
|
82
|
+
// equal first-seq preserves input order for determinism).
|
|
83
|
+
const ordered = [...protectedSpans].sort((a, b) => {
|
|
84
|
+
const as = a.events.length ? a.events[0].seq : 0n;
|
|
85
|
+
const bs = b.events.length ? b.events[0].seq : 0n;
|
|
86
|
+
return as < bs ? -1 : as > bs ? 1 : 0;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const shards: ExactShardV1[] = [];
|
|
90
|
+
let batch: ProtectedSpan[] = [];
|
|
91
|
+
let batchBytes = 0;
|
|
92
|
+
let batchCase: ExactShardCase = "anchor";
|
|
93
|
+
|
|
94
|
+
const spanBytes = (s: ProtectedSpan): number =>
|
|
95
|
+
s.events.reduce((n, e) => n + e.originalBytes.length, 0);
|
|
96
|
+
|
|
97
|
+
const flush = (): void => {
|
|
98
|
+
if (batch.length === 0) return;
|
|
99
|
+
const first = batch[0];
|
|
100
|
+
const last = batch[batch.length - 1];
|
|
101
|
+
const firstEv = first.events[0];
|
|
102
|
+
const lastEv = last.events[last.events.length - 1];
|
|
103
|
+
const start = offsets.of(firstEv.seq);
|
|
104
|
+
const end = offsets.of(lastEv.seq);
|
|
105
|
+
const byteStart = start ? start.byteStart : 0;
|
|
106
|
+
const byteEnd = end ? end.byteEnd : byteStart;
|
|
107
|
+
const bytes = concatBytes(batch.flatMap((s) => s.events));
|
|
108
|
+
shards.push({
|
|
109
|
+
schema: "exact-shard-v1",
|
|
110
|
+
sessionId,
|
|
111
|
+
range: {
|
|
112
|
+
sessionId,
|
|
113
|
+
seqStart: firstEv.seq,
|
|
114
|
+
seqEnd: lastEv.seq,
|
|
115
|
+
byteStart,
|
|
116
|
+
byteEnd,
|
|
117
|
+
},
|
|
118
|
+
kind: "exact",
|
|
119
|
+
originalBytes: bytes,
|
|
120
|
+
digest: sealExactDigest(sessionId, bytes),
|
|
121
|
+
byteCount: batchBytes,
|
|
122
|
+
case: batchCase,
|
|
123
|
+
});
|
|
124
|
+
batch = [];
|
|
125
|
+
batchBytes = 0;
|
|
126
|
+
batchCase = "anchor";
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
for (const span of ordered) {
|
|
130
|
+
const sz = spanBytes(span);
|
|
131
|
+
// Close at a complete-span boundary when adding this atomic span would
|
|
132
|
+
// exceed the budget and we already hold at least one complete span.
|
|
133
|
+
if (batch.length > 0 && batchBytes + sz > targetSize) flush();
|
|
134
|
+
batch.push(span);
|
|
135
|
+
batchBytes += sz;
|
|
136
|
+
batchCase = combineCase(span, batchCase);
|
|
137
|
+
}
|
|
138
|
+
flush();
|
|
139
|
+
|
|
140
|
+
return { ok: true, shards };
|
|
141
|
+
}
|