pi-mega-compact 0.19.2 → 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.
Files changed (98) hide show
  1. package/dist/config/vector-cortex.js +24 -0
  2. package/dist/config.js +1 -1
  3. package/dist/extensions/dashboard-server/routes-cache.test/_helpers.js +2 -0
  4. package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +2 -0
  5. package/dist/extensions/dashboard-server/routes-vector-cortex-helpers.js +100 -0
  6. package/dist/extensions/dashboard-server/routes-vector-cortex-query.js +46 -0
  7. package/dist/extensions/dashboard-server/routes-vector-cortex-shards.js +48 -0
  8. package/dist/extensions/dashboard-server/routes-vector-cortex.js +2 -0
  9. package/dist/extensions/dashboard-server/routes.js +1 -1
  10. package/dist/extensions/dashboard-server/server.js +41 -16
  11. package/dist/extensions/dashboard-server/tailscale.js +32 -0
  12. package/dist/src/config/vector-cortex.js +24 -0
  13. package/dist/src/config.js +1 -1
  14. package/dist/src/tieredRouter.js +8 -0
  15. package/dist/src/vector-cortex/migrations/router-generation-v2.js +252 -0
  16. package/dist/src/vector-cortex/shards/exact.js +134 -0
  17. package/dist/src/vector-cortex/shards/manifest.js +237 -0
  18. package/dist/src/vector-cortex/shards/semantic.js +137 -0
  19. package/dist/src/vector-cortex/shards/types.js +51 -0
  20. package/dist/src/vector-cortex/topology/query.js +342 -0
  21. package/dist/vector-cortex/migrations/router-generation-v2.js +252 -0
  22. package/dist/vector-cortex/shards/exact.js +134 -0
  23. package/dist/vector-cortex/shards/manifest.js +237 -0
  24. package/dist/vector-cortex/shards/semantic.js +137 -0
  25. package/dist/vector-cortex/shards/types.js +51 -0
  26. package/dist/vector-cortex/topology/query.js +342 -0
  27. package/extensions/dashboard-client/dist/assets/{AreaChart-CSaJZXru.js → AreaChart-n4z5w9Ox.js} +2 -2
  28. package/extensions/dashboard-client/dist/assets/{AreaChart-CSaJZXru.js.map → AreaChart-n4z5w9Ox.js.map} +1 -1
  29. package/extensions/dashboard-client/dist/assets/{BarChart-CQouNj1g.js → BarChart-BLGjWWBr.js} +2 -2
  30. package/extensions/dashboard-client/dist/assets/{BarChart-CQouNj1g.js.map → BarChart-BLGjWWBr.js.map} +1 -1
  31. package/extensions/dashboard-client/dist/assets/{CacheTab-m_eiCYoL.js → CacheTab-CAnrKKVK.js} +2 -2
  32. package/extensions/dashboard-client/dist/assets/{CacheTab-m_eiCYoL.js.map → CacheTab-CAnrKKVK.js.map} +1 -1
  33. package/extensions/dashboard-client/dist/assets/{EventsTab-DRyOwjYN.js → EventsTab-D-GL7Ngq.js} +2 -2
  34. package/extensions/dashboard-client/dist/assets/{EventsTab-DRyOwjYN.js.map → EventsTab-D-GL7Ngq.js.map} +1 -1
  35. package/extensions/dashboard-client/dist/assets/{HealthTab-Dj9k40TJ.js → HealthTab-C4x5qQ8s.js} +2 -2
  36. package/extensions/dashboard-client/dist/assets/{HealthTab-Dj9k40TJ.js.map → HealthTab-C4x5qQ8s.js.map} +1 -1
  37. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Bk9mhYfD.js → MaintenanceTab-BSPs0eq5.js} +2 -2
  38. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-Bk9mhYfD.js.map → MaintenanceTab-BSPs0eq5.js.map} +1 -1
  39. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-C7ve49VG.js → MemoryMapTab-BWEUtTSu.js} +2 -2
  40. package/extensions/dashboard-client/dist/assets/{MemoryMapTab-C7ve49VG.js.map → MemoryMapTab-BWEUtTSu.js.map} +1 -1
  41. package/extensions/dashboard-client/dist/assets/{MetricsTab-DzjhqZA9.js → MetricsTab-Dhm1c3Cx.js} +2 -2
  42. package/extensions/dashboard-client/dist/assets/{MetricsTab-DzjhqZA9.js.map → MetricsTab-Dhm1c3Cx.js.map} +1 -1
  43. package/extensions/dashboard-client/dist/assets/{OverviewTab-CFQdfIm_.js → OverviewTab-Cm6cySiQ.js} +2 -2
  44. package/extensions/dashboard-client/dist/assets/{OverviewTab-CFQdfIm_.js.map → OverviewTab-Cm6cySiQ.js.map} +1 -1
  45. package/extensions/dashboard-client/dist/assets/{ReposTab-DyjwEFUr.js → ReposTab-DxLN1N4K.js} +2 -2
  46. package/extensions/dashboard-client/dist/assets/{ReposTab-DyjwEFUr.js.map → ReposTab-DxLN1N4K.js.map} +1 -1
  47. package/extensions/dashboard-client/dist/assets/{SessionsTab-njLIy7ab.js → SessionsTab-CZuI4ISW.js} +2 -2
  48. package/extensions/dashboard-client/dist/assets/{SessionsTab-njLIy7ab.js.map → SessionsTab-CZuI4ISW.js.map} +1 -1
  49. package/extensions/dashboard-client/dist/assets/{SetupTab-UHIs8-Lh.js → SetupTab-C_0VOomm.js} +2 -2
  50. package/extensions/dashboard-client/dist/assets/{SetupTab-UHIs8-Lh.js.map → SetupTab-C_0VOomm.js.map} +1 -1
  51. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-inWHjCxg.js → TimeSavedCard-BhbaRLmi.js} +2 -2
  52. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-inWHjCxg.js.map → TimeSavedCard-BhbaRLmi.js.map} +1 -1
  53. package/extensions/dashboard-client/dist/assets/{TurnsTab-CXJYugBu.js → TurnsTab-CXJmbjje.js} +2 -2
  54. package/extensions/dashboard-client/dist/assets/{TurnsTab-CXJYugBu.js.map → TurnsTab-CXJmbjje.js.map} +1 -1
  55. package/extensions/dashboard-client/dist/assets/VectorCortexTab-BGe9hu3r.js +2 -0
  56. package/extensions/dashboard-client/dist/assets/VectorCortexTab-BGe9hu3r.js.map +1 -0
  57. package/extensions/dashboard-client/dist/assets/{WikiTab-BJ4A2oi9.js → WikiTab-C5uvMCzp.js} +2 -2
  58. package/extensions/dashboard-client/dist/assets/{WikiTab-BJ4A2oi9.js.map → WikiTab-C5uvMCzp.js.map} +1 -1
  59. package/extensions/dashboard-client/dist/assets/{button-BWl-moyn.js → button-Cl_fxJcg.js} +2 -2
  60. package/extensions/dashboard-client/dist/assets/{button-BWl-moyn.js.map → button-Cl_fxJcg.js.map} +1 -1
  61. package/extensions/dashboard-client/dist/assets/{card-C58RVPOk.js → card-BH8Le5YM.js} +2 -2
  62. package/extensions/dashboard-client/dist/assets/{card-C58RVPOk.js.map → card-BH8Le5YM.js.map} +1 -1
  63. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-vgeOQU3L.js → generateCategoricalChart-sZ6ZNtBi.js} +2 -2
  64. package/extensions/dashboard-client/dist/assets/{generateCategoricalChart-vgeOQU3L.js.map → generateCategoricalChart-sZ6ZNtBi.js.map} +1 -1
  65. package/extensions/dashboard-client/dist/assets/{index-lVrWQjpu.js → index-CX9zv8cJ.js} +3 -3
  66. package/extensions/dashboard-client/dist/assets/{index-lVrWQjpu.js.map → index-CX9zv8cJ.js.map} +1 -1
  67. package/extensions/dashboard-client/dist/assets/{switch-CSeplFQT.js → switch-CXJhcjhM.js} +2 -2
  68. package/extensions/dashboard-client/dist/assets/{switch-CSeplFQT.js.map → switch-CXJhcjhM.js.map} +1 -1
  69. package/extensions/dashboard-client/dist/assets/{toggle-CQRxgVRi.js → toggle-CoLtIc6y.js} +2 -2
  70. package/extensions/dashboard-client/dist/assets/{toggle-CQRxgVRi.js.map → toggle-CoLtIc6y.js.map} +1 -1
  71. package/extensions/dashboard-client/dist/assets/{useSSE-BUz6IOI-.js → useSSE-fSXho606.js} +2 -2
  72. package/extensions/dashboard-client/dist/assets/{useSSE-BUz6IOI-.js.map → useSSE-fSXho606.js.map} +1 -1
  73. package/extensions/dashboard-client/dist/index.html +1 -1
  74. package/extensions/dashboard-client/src/api/vector-cortex.ts +18 -0
  75. package/extensions/dashboard-client/src/tabs/VectorCortexTab.tsx +62 -0
  76. package/extensions/dashboard-client/src/types/vector-cortex.ts +25 -0
  77. package/extensions/dashboard-server/api-contracts/vector-cortex.ts +41 -0
  78. package/extensions/dashboard-server/routes-cache.test/_helpers.ts +2 -0
  79. package/extensions/dashboard-server/routes-rag-settings-helpers.ts +12 -0
  80. package/extensions/dashboard-server/routes-vector-cortex-helpers.ts +110 -0
  81. package/extensions/dashboard-server/routes-vector-cortex-query.ts +54 -0
  82. package/extensions/dashboard-server/routes-vector-cortex-shards.ts +57 -0
  83. package/extensions/dashboard-server/routes-vector-cortex.ts +2 -0
  84. package/extensions/dashboard-server/routes.ts +2 -0
  85. package/extensions/dashboard-server/server.ts +46 -17
  86. package/extensions/dashboard-server/tailscale.ts +32 -0
  87. package/package.json +1 -1
  88. package/src/config/vector-cortex.ts +26 -0
  89. package/src/config.ts +2 -0
  90. package/src/tieredRouter.ts +9 -0
  91. package/src/vector-cortex/migrations/router-generation-v2.ts +317 -0
  92. package/src/vector-cortex/shards/exact.ts +141 -0
  93. package/src/vector-cortex/shards/manifest.ts +276 -0
  94. package/src/vector-cortex/shards/semantic.ts +156 -0
  95. package/src/vector-cortex/shards/types.ts +212 -0
  96. package/src/vector-cortex/topology/query.ts +485 -0
  97. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DkS0Azx9.js +0 -2
  98. package/extensions/dashboard-client/dist/assets/VectorCortexTab-DkS0Azx9.js.map +0 -1
@@ -160,6 +160,47 @@ export interface VectorCortexTopologyView {
160
160
  readonly updatedAt: string;
161
161
  }
162
162
 
163
+ /**
164
+ * Reader-only query-layer diagnostics view for GET /api/vector-cortex/query
165
+ * (VC3C). Purely a flag-status + structural diagnostic — reports whether the
166
+ * VC3C flag is enabled and the router-generation v2 version constant. The VC3C
167
+ * query index is in-memory (not durable), so no payloads, prompts, or index
168
+ * contents are ever exposed. Non-fatal: a missing state dir or internal error
169
+ * degrades to `enabled:false`.
170
+ */
171
+ export interface VectorCortexQueryView {
172
+ /** Whether the VC3C query-layer flag is enabled in this process. */
173
+ readonly enabled: boolean;
174
+ /** Router-generation v2 key version constant (query index format). */
175
+ readonly routerVersion: number;
176
+ /** ISO timestamp of the snapshot. */
177
+ readonly updatedAt: string;
178
+ }
179
+
180
+ /**
181
+ * Reader-only dual-tier shard aggregate for GET /api/vector-cortex/shards
182
+ * (VC4A). Purely an enabled-flag + COUNT/BYTE aggregate over the most recently
183
+ * built shard manifest — semantic shards, exact shards, and the protected span
184
+ * byte total. Reader-only: never exposes shard payloads, verbatim exact bytes,
185
+ * or prompt text. The shard tier is pure in-memory partition logic in this
186
+ * sprint (no durable manifest store), so when no partition has been staged the
187
+ * counts are zero. Non-fatal: a missing state dir degrades to `enabled:false`.
188
+ */
189
+ export interface VectorCortexShardsView {
190
+ /** Whether the VC4A dual-tier shard flag is enabled in this process. */
191
+ readonly enabled: boolean;
192
+ /** Number of semantic shards in the most recent manifest. */
193
+ readonly semanticCount: number;
194
+ /** Number of exact shards in the most recent manifest. */
195
+ readonly exactCount: number;
196
+ /** Combined byte total across both tiers. */
197
+ readonly byteTotal: number;
198
+ /** Total protected-span bytes the exact tier must tile. */
199
+ readonly protectedByteTotal: number;
200
+ /** ISO timestamp of the snapshot. */
201
+ readonly updatedAt: string;
202
+ }
203
+
163
204
  /**
164
205
  * Reader-only occurrence-ledger view for GET /api/vector-cortex/ledger (VC1B).
165
206
  * Built on the LedgerReader capability surface. Exposes occurrence IDENTITY
@@ -34,6 +34,7 @@ export async function withServer<T>(
34
34
  fn: (port: number) => Promise<T>,
35
35
  ): Promise<T> {
36
36
  process.env.MEGACOMPACT_DASHBOARD_PORT = port;
37
+ process.env.MEGACOMPACT_DASHBOARD_HOST = "127.0.0.1";
37
38
  const child = spawn(process.execPath, [SERVER_ENTRY, dir], { stdio: "ignore" });
38
39
  try {
39
40
  await waitFor(async () => {
@@ -50,6 +51,7 @@ export async function withServer<T>(
50
51
  } finally {
51
52
  child.kill("SIGTERM");
52
53
  delete process.env.MEGACOMPACT_DASHBOARD_PORT;
54
+ delete process.env.MEGACOMPACT_DASHBOARD_HOST;
53
55
  rmSync(dir, { recursive: true, force: true });
54
56
  }
55
57
  }
@@ -343,6 +343,18 @@ export const SETTINGS: ReadonlyArray<{
343
343
  "Deterministic cortical topology (TopologyV1/EdgeV1): per-(source,head) top-k=16/head calibrated-threshold edges, stable score-desc/then-target-ID sort, dependency directed + contradiction symmetric paired records, one stable generation digest. OFF = mode C, no topology graph built/emitted, predecessor-precise topology view, byte-identical predecessor.",
344
344
  true,
345
345
  ),
346
+ boolDirect(
347
+ "MEGACOMPACT_VC3C",
348
+ "VC3C Topology Query + Router Invalidation",
349
+ "TopologyQueryV1/RouterKeyV2 structured keys (length-delimited, unsigned-byte order, no prefix ambiguity), exact (session,generation) invalidation, stale-generation rejection (TOP_GENERATION_STALE), and the M6 router-generation-v2 copy/validate/switch migration. OFF = mode C, no structured router key / generation invalidation, byte-identical predecessor.",
350
+ true,
351
+ ),
352
+ boolDirect(
353
+ "MEGACOMPACT_VC4A",
354
+ "VC4A Dual-Tier Shards",
355
+ "SemanticShardV1/ExactShardV1/ShardManifestV1: partition a session ONLY at complete EventV2 boundaries; exact shards preserve every tool call/result pair, anchor and invalid UTF-8 event as original bytes (pairs never split across exact shards); manifest enforces disjoint sorted ranges + complete protected-span coverage. OFF = mode C, exact anchors/current transcript only, byte-identical predecessor.",
356
+ true,
357
+ ),
346
358
  ],
347
359
  },
348
360
  {
@@ -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,54 @@
1
+ /**
2
+ * dashboard-server/routes-vector-cortex-query.ts — VC3C vector-cortex query-layer
3
+ * diagnostics dashboard route.
4
+ *
5
+ * Reader-only GET /api/vector-cortex/query. Purely a flag-status + structural
6
+ * diagnostic: the VC3C query index is in-memory (not durable), so this route
7
+ * reports whether the VC3C flag is enabled, the router-generation v2 version
8
+ * constant, and a snapshot timestamp. It NEVER exposes payloads or prompts.
9
+ * Non-fatal: a missing state dir (or any internal error) degrades to
10
+ * `enabled:false`.
11
+ *
12
+ * Guardrails: PREVENT-PI-004 (local flag/config read only, no network),
13
+ * PREVENT-011 (no `any`), reader-only aggregate (never payloads/prompts).
14
+ */
15
+
16
+ import type { IncomingMessage, ServerResponse } from "node:http";
17
+ import type { RouteContext } from "./routes-core.js";
18
+ import { VC3C_ENABLED } from "../../src/config.js";
19
+ import { ROUTER_KEY_VERSION } from "../../src/vector-cortex/topology/query.js";
20
+ import { sendJson } from "./routes-vector-cortex-shared.js";
21
+ import type { VectorCortexQueryView } from "./api-contracts/vector-cortex.js";
22
+
23
+ /**
24
+ * Reader-only GET /api/vector-cortex/query (VC3C query-layer diagnostics).
25
+ */
26
+ export function handleVectorCortexQuery(
27
+ req: IncomingMessage,
28
+ res: ServerResponse,
29
+ _ctx: RouteContext,
30
+ ): boolean {
31
+ const url = req.url ?? "";
32
+ const path = url.split("?")[0] ?? url;
33
+ if (path !== "/api/vector-cortex/query") return false;
34
+ if (req.method !== "GET") {
35
+ // Reader-only path: no mutation endpoint lives at /query.
36
+ sendJson(res, 405, { error: "method_not_allowed" });
37
+ return true;
38
+ }
39
+
40
+ let enabled = false;
41
+ try {
42
+ enabled = VC3C_ENABLED();
43
+ } catch {
44
+ // Non-fatal: a missing/unavailable flag degrades to `enabled:false`.
45
+ enabled = false;
46
+ }
47
+ const body: VectorCortexQueryView = {
48
+ enabled,
49
+ routerVersion: ROUTER_KEY_VERSION,
50
+ updatedAt: new Date().toISOString(),
51
+ };
52
+ sendJson(res, 200, body);
53
+ return true;
54
+ }
@@ -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
+ }
@@ -18,3 +18,5 @@ export { handleVectorCortexEvaluation } from "./routes-vector-cortex-eval.js";
18
18
  export { handleVectorCortexHealth, handleVectorCortexBreakersReset } from "./routes-vector-cortex-health.js";
19
19
  export { handleVectorCortexLedger } from "./routes-vector-cortex-ledger.js";
20
20
  export { handleVectorCortexTopology } from "./routes-vector-cortex-topology.js";
21
+ export { handleVectorCortexQuery } from "./routes-vector-cortex-query.js";
22
+ export { handleVectorCortexShards } from "./routes-vector-cortex-shards.js";
@@ -46,4 +46,6 @@ export {
46
46
  handleVectorCortexBreakersReset,
47
47
  handleVectorCortexLedger,
48
48
  handleVectorCortexTopology,
49
+ handleVectorCortexQuery,
50
+ handleVectorCortexShards,
49
51
  } from "./routes-vector-cortex.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,
@@ -61,6 +62,8 @@ import {
61
62
  handleVectorCortexBreakersReset,
62
63
  handleVectorCortexLedger,
63
64
  handleVectorCortexTopology,
65
+ handleVectorCortexQuery,
66
+ handleVectorCortexShards,
64
67
  handleStatic,
65
68
  } from "./routes.js";
66
69
 
@@ -230,15 +233,31 @@ export async function launchDashboardServer(
230
233
  detectCrossRepoDrift,
231
234
  });
232
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.
233
247
  const server = createServer((req: IncomingMessage, res: ServerResponse) => {
234
- // guardrails-allow PREVENT-PI-004: optional, user-triggered /dashboard localhost server (loopback-only) CORS restricted to same-origin localhost browsers.
235
- // CORS for local access — restricted to loopback origins (the dashboard server only binds to localhost).
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.
236
252
  const origin = req.headers.origin;
237
- if (
238
- typeof origin === "string" &&
239
- /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)
240
- ) {
241
- res.setHeader("Access-Control-Allow-Origin", origin);
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 ?? "*");
242
261
  res.setHeader("Vary", "Origin");
243
262
  }
244
263
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS");
@@ -285,6 +304,8 @@ export async function launchDashboardServer(
285
304
  if (handleVectorCortexBreakersReset(req, res, ctx)) return;
286
305
  if (handleVectorCortexLedger(req, res, ctx)) return;
287
306
  if (handleVectorCortexTopology(req, res, ctx)) return;
307
+ if (handleVectorCortexQuery(req, res, ctx)) return;
308
+ if (handleVectorCortexShards(req, res, ctx)) return;
288
309
  handleStatic(req, res, ctx);
289
310
  });
290
311
 
@@ -308,20 +329,28 @@ export async function launchDashboardServer(
308
329
  }
309
330
  });
310
331
 
311
- server.listen(port, "127.0.0.1", () => {
312
- const url = `http://localhost:${port}`; // guardrails-allow PREVENT-PI-004: localhost dashboard URL (loopback-only)
313
- log("server running", { url });
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 });
314
341
  // eslint-disable-next-line no-console
315
342
  console.log(`[mega-compact] dashboard server running: ${url}`);
316
343
 
317
- // v0.8.2: also bind the IPv6 loopback (::1). On many systems `localhost`
318
- // resolves to ::1 first (see /etc/hosts), so an IPv4-only bind makes the
319
- // browser hit ::1:port and get connection refused. PREVENT-PI-004
320
- // (loopback-only) means BOTH 127.0.0.1 and ::1. Non-fatal: IPv4-only
321
- // hosts or a ::1 already in use just skip the mirror.
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.
322
351
  let v6: ReturnType<typeof createServer> | undefined;
323
352
  const v4Handler = server.listeners("request")[0];
324
- if (v4Handler) {
353
+ if (host === "127.0.0.1" && v4Handler) {
325
354
  v6 = createServer((r, s) =>
326
355
  (v4Handler as (a: IncomingMessage, b: ServerResponse) => void).call(
327
356
  server,
@@ -336,7 +365,7 @@ export async function launchDashboardServer(
336
365
  message: e.message,
337
366
  }),
338
367
  );
339
- v6.listen(port, "::1", () => log("ipv6 loopback bound", { port })); // guardrails-allow PREVENT-PI-004: IPv6 loopback (::1) mirror of the localhost dashboard server
368
+ v6.listen(port, "::1", () => log("ipv6 loopback bound", { port })); // guardrails-allow PREVENT-PI-004: IPv6 loopback (::1) mirror of the dashboard server
340
369
  }
341
370
 
342
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.19.2",
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",
@@ -121,6 +121,32 @@ export const VC3A_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC3A");
121
121
  */
122
122
  export const VC3B_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC3B");
123
123
 
124
+ /**
125
+ * VC3C — topology query + router invalidation (TopologyQueryV1 / RouterKeyV2,
126
+ * M6 router-generation-v2 migration).
127
+ * Default ON. `MEGACOMPACT_VC3C=0` disables and is byte-identical to the
128
+ * predecessor (mode C: no length-delimited router key, no generation
129
+ * invalidation, zero `vector_cortex_router_generation_invalidated` /
130
+ * `vector_cortex_topology_query_demoted` emissions; the predecessor tiered
131
+ * router keying is unchanged). The real consumers are the topology query seam
132
+ * (query.ts), the M6 router-generation-v2 migration seam, and the reader-only
133
+ * dashboard topology query diagnostic view.
134
+ */
135
+ export const VC3C_ENABLED = (): boolean => sprintFlag("MEGACOMPACT_VC3C");
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
+
124
150
  // ---------------------------------------------------------------------------
125
151
  // Breaker state machine constants (TRIAD_RESILIENCE.md §breaker).
126
152
  // Rolled numbers for one 60s window; VC0C consumes these at its breaker seam.
package/src/config.ts CHANGED
@@ -162,6 +162,8 @@ export {
162
162
  VC2C_ENABLED,
163
163
  VC3A_ENABLED,
164
164
  VC3B_ENABLED,
165
+ VC3C_ENABLED,
166
+ VC4A_ENABLED,
165
167
  BREAKER_WINDOW_MS,
166
168
  BREAKER_MIN_ATTEMPTS,
167
169
  BREAKER_PERF_FAILURES,
@@ -16,6 +16,9 @@
16
16
  import { createHash } from "node:crypto";
17
17
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
18
18
  import { dirname, join } from "node:path";
19
+ import { VC3C_ENABLED } from "./config.js";
20
+ import { Logger } from "./log.js";
21
+ import { routerGenerationInvalidationSeam, type RouterKeyV2 } from "./vector-cortex/topology/query.js";
19
22
  import { openStore } from "./store/sqlite/utils.js";
20
23
  import { fts5SearchScoped } from "./store/sqlite/fts5-search.js";
21
24
  import { listCheckpoints } from "./store/sqlite.js";
@@ -343,6 +346,12 @@ export class TieredRouter {
343
346
  this.embeddingCache.clear();
344
347
  }
345
348
 
349
+ /** VC3C narrow delegate: exact (session,generation) RouterKeyV2 invalidation.
350
+ * No routing rewrite; `MEGACOMPACT_VC3C=0` is a strict no-op, byte-identical. */
351
+ invalidateGeneration(key: RouterKeyV2): void {
352
+ routerGenerationInvalidationSeam(key, VC3C_ENABLED(), (e, f) => new Logger().info(e, f));
353
+ }
354
+
346
355
  /** Snapshot of current metrics for dashboard / logging. */
347
356
  getMetrics(): TieredRouterMetrics {
348
357
  const q = Math.max(this.totalQueries, 1);