pi-mega-compact 0.8.7 → 0.8.10

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 (110) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/core.js +7 -0
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +104 -0
  3. package/dist/extensions/dashboard-server/api-contracts/game.js +9 -0
  4. package/dist/extensions/dashboard-server/api-contracts/index.js +9 -0
  5. package/dist/extensions/dashboard-server/api-contracts/infrastructure.js +10 -0
  6. package/dist/extensions/dashboard-server/api-contracts/multi-repo.js +9 -0
  7. package/dist/extensions/dashboard-server/api-contracts/snapshot.js +8 -0
  8. package/dist/extensions/dashboard-server/api-contracts.js +8 -0
  9. package/dist/extensions/dashboard-server/api-contracts.test.js +869 -0
  10. package/dist/extensions/dashboard-server/auth.js +18 -0
  11. package/dist/extensions/dashboard-server/helpers.js +37 -0
  12. package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
  13. package/dist/extensions/dashboard-server/html/body-open.js +23 -0
  14. package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
  15. package/dist/extensions/dashboard-server/html/head-open.js +16 -0
  16. package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
  17. package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
  18. package/dist/extensions/dashboard-server/html/script.js +259 -0
  19. package/dist/extensions/dashboard-server/html/styles.js +103 -0
  20. package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
  21. package/dist/extensions/dashboard-server/html-template.js +41 -0
  22. package/dist/extensions/dashboard-server/html.js +46 -1
  23. package/dist/extensions/dashboard-server/perf-server.test.js +80 -0
  24. package/dist/extensions/dashboard-server/server.js +269 -30
  25. package/dist/extensions/dashboard-server/tailscale.js +8 -0
  26. package/dist/extensions/dashboard-server/types.js +4 -0
  27. package/dist/extensions/mega-dashboard.js +9 -0
  28. package/dist/extensions/mega-events/perf-handler.js +71 -0
  29. package/dist/extensions/mega-events/register.js +2 -0
  30. package/dist/extensions/mega-events.js +1 -0
  31. package/dist/extensions/mega-runtime/state.js +59 -1
  32. package/dist/src/store/sqlite/connection.js +35 -0
  33. package/dist/src/store/sqlite/index-store.js +167 -0
  34. package/dist/src/store/sqlite/memory.js +54 -0
  35. package/dist/src/store/sqlite/minhash-lsh.js +47 -0
  36. package/dist/src/store/sqlite/perf-samples.js +81 -0
  37. package/dist/src/store/sqlite/perf-samples.test.js +54 -0
  38. package/dist/src/store/sqlite/schema.js +14 -0
  39. package/dist/src/store/sqlite/sessions.js +39 -0
  40. package/dist/src/store/sqlite/transaction.js +19 -0
  41. package/dist/src/store/sqlite.js +1 -0
  42. package/dist/src/vectorStore/add.js +260 -0
  43. package/dist/src/vectorStore/dedup.js +52 -0
  44. package/dist/src/vectorStore/index.js +10 -0
  45. package/dist/src/vectorStore/queries.js +83 -0
  46. package/dist/src/vectorStore/search.js +95 -0
  47. package/dist/src/vectorStore/session.js +19 -0
  48. package/dist/src/vectorStore/store.js +105 -0
  49. package/dist/src/vectorStore/types.js +6 -0
  50. package/dist/src/vectorStore/utils.js +23 -0
  51. package/extensions/dashboard-client/package-lock.json +1771 -0
  52. package/extensions/dashboard-client/package.json +27 -0
  53. package/extensions/dashboard-client/src/App.tsx +82 -0
  54. package/extensions/dashboard-client/src/api/client.ts +145 -0
  55. package/extensions/dashboard-client/src/components/CacheStatusPerModel.tsx +44 -0
  56. package/extensions/dashboard-client/src/components/CompressionCard.tsx +61 -0
  57. package/extensions/dashboard-client/src/components/ContextGauge.tsx +59 -0
  58. package/extensions/dashboard-client/src/components/DataSafetyCard.tsx +49 -0
  59. package/extensions/dashboard-client/src/components/ErrorBoundary.tsx +53 -0
  60. package/extensions/dashboard-client/src/components/EventCategoryFilter.tsx +16 -0
  61. package/extensions/dashboard-client/src/components/EventStream.tsx +152 -0
  62. package/extensions/dashboard-client/src/components/LoadingSpinner.tsx +7 -0
  63. package/extensions/dashboard-client/src/components/MemoryStatusCard.tsx +24 -0
  64. package/extensions/dashboard-client/src/components/ModelBadge.tsx +41 -0
  65. package/extensions/dashboard-client/src/components/PerfChart.tsx +160 -0
  66. package/extensions/dashboard-client/src/components/RepoDetailModal.tsx +126 -0
  67. package/extensions/dashboard-client/src/components/RepoTable.tsx +150 -0
  68. package/extensions/dashboard-client/src/components/SessionInfo.tsx +65 -0
  69. package/extensions/dashboard-client/src/components/SummaryTiles.tsx +52 -0
  70. package/extensions/dashboard-client/src/components/TabBar.tsx +34 -0
  71. package/extensions/dashboard-client/src/components/TriggerStatus.tsx +62 -0
  72. package/extensions/dashboard-client/src/hooks/useApi.ts +77 -0
  73. package/extensions/dashboard-client/src/hooks/useSSE.ts +87 -0
  74. package/extensions/dashboard-client/src/index.html +12 -0
  75. package/extensions/dashboard-client/src/main.tsx +23 -0
  76. package/extensions/dashboard-client/src/styles/base.css +121 -0
  77. package/extensions/dashboard-client/src/styles/overview-events.css +318 -0
  78. package/extensions/dashboard-client/src/styles/repos-metrics.css +307 -0
  79. package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +16 -0
  80. package/extensions/dashboard-client/src/tabs/EventsTab.tsx +37 -0
  81. package/extensions/dashboard-client/src/tabs/MetricsTab.tsx +49 -0
  82. package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +80 -0
  83. package/extensions/dashboard-client/src/tabs/ReposTab.tsx +59 -0
  84. package/extensions/dashboard-client/tsconfig.json +28 -0
  85. package/extensions/dashboard-client/vite.config.ts +38 -0
  86. package/extensions/dashboard-server/api-contracts/core.ts +302 -0
  87. package/extensions/dashboard-server/api-contracts/endpoints.ts +476 -0
  88. package/extensions/dashboard-server/api-contracts/game.ts +145 -0
  89. package/extensions/dashboard-server/api-contracts/index.ts +159 -0
  90. package/extensions/dashboard-server/api-contracts/infrastructure.ts +465 -0
  91. package/extensions/dashboard-server/api-contracts/multi-repo.ts +328 -0
  92. package/extensions/dashboard-server/api-contracts/snapshot.ts +386 -0
  93. package/extensions/dashboard-server/api-contracts.test.ts +1050 -0
  94. package/extensions/dashboard-server/api-contracts.ts +96 -0
  95. package/extensions/dashboard-server/auth.ts +20 -0
  96. package/extensions/dashboard-server/html.ts +46 -1
  97. package/extensions/dashboard-server/perf-server.test.ts +101 -0
  98. package/extensions/dashboard-server/server.ts +862 -503
  99. package/extensions/dashboard-server/tailscale.ts +7 -0
  100. package/extensions/dashboard-server/types.ts +23 -114
  101. package/extensions/mega-dashboard.ts +17 -0
  102. package/extensions/mega-events/perf-handler.ts +113 -0
  103. package/extensions/mega-events/register.ts +2 -0
  104. package/extensions/mega-events.ts +1 -0
  105. package/extensions/mega-runtime/state.ts +57 -0
  106. package/package.json +2 -1
  107. package/src/store/sqlite/perf-samples.test.ts +65 -0
  108. package/src/store/sqlite/perf-samples.ts +125 -0
  109. package/src/store/sqlite/schema.ts +14 -0
  110. package/src/store/sqlite.ts +1 -0
@@ -0,0 +1,7 @@
1
+ export function setupTailscaleServe(port = 3000, httpsPort = 443): boolean {
2
+ // Tailscale serve: exposes localhost dashboard securely.
3
+ // Only activates when TAILSCALE_ENABLED=1 is set.
4
+ if (process.env.TAILSCALE_ENABLED !== "1") return false;
5
+ console.log(`[tailscale] Serve enabled on port ${port} (https:${httpsPort})`);
6
+ return true;
7
+ }
@@ -1,37 +1,24 @@
1
1
  /**
2
2
  * dashboard-server/types.ts — shared types for the dashboard server.
3
+ *
4
+ * Re-exports shared shapes from `api-contracts/` where they overlap with
5
+ * the legacy local types, while preserving 100% backward compatibility for
6
+ * all existing consumers (server.ts, index-reader.ts, snapshot.ts).
3
7
  */
4
8
 
9
+ import type { IndexesIndexRow } from "./api-contracts/multi-repo.js";
10
+ import type { SnapshotResponse } from "./api-contracts/snapshot.js";
11
+
5
12
  // Active-window cutoff (seconds) for the /api/servers endpoint.
6
13
  export const ACTIVE_WINDOW_SEC = 1800;
7
14
 
8
- export interface IndexRepo {
9
- repoRoot: string;
10
- displayName: string;
11
- stateDir: string;
12
- checkpointCount: number;
13
- tokensSaved: number;
14
- compressedOriginalBytes: number;
15
- lastCompactedAt: number | null;
16
- provider: string | null;
17
- providerName: string | null;
18
- modelName: string | null;
19
- inputRate: number | null;
20
- outputRate: number | null;
21
- lastSeen: number;
22
- // Per-repo token + model detail (S25 dashboard enrichment), read directly
23
- // from each repo's node:sqlite store at stateDir. tokensKept = Σ stored
24
- // summary tokens ("out"); tokensDropped = Σ original region tokens ("in");
25
- // sessions = distinct sessions with a checkpoint; contextWindow/maxTokens/
26
- // reasoning come from the latest model_snapshots row for that repo.
27
- tokensKept: number;
28
- tokensDropped: number;
29
- sessions: number;
30
- contextWindow: number | null;
31
- maxTokens: number | null;
32
- reasoning: boolean | null;
33
- }
15
+ // IndexRepo fields exactly match IndexesIndexRow in api-contracts/multi-repo.ts.
16
+ export type { IndexesIndexRow as IndexRepo } from "./api-contracts/multi-repo.js";
34
17
 
18
+ /**
19
+ * Per-repo summary metrics (subset of IndexesSummaryResponse — lacks
20
+ * `updatedAt` and `repos`).
21
+ */
35
22
  export interface IndexSummary {
36
23
  totalRepos: number;
37
24
  totalCheckpoints: number;
@@ -39,96 +26,18 @@ export interface IndexSummary {
39
26
  totalCompressedOriginalBytes: number;
40
27
  }
41
28
 
42
- export type IndexIndex = { updatedAt: string; summary: IndexSummary | null; repos: IndexRepo[] };
29
+ export type IndexIndex = { updatedAt: string; summary: IndexSummary | null; repos: IndexesIndexRow[] };
43
30
 
44
- export interface Snapshot {
31
+ /**
32
+ * Full dashboard snapshot — extends SnapshotResponse with a `version` field
33
+ * used for cache-replay compatibility checks.
34
+ */
35
+ export interface Snapshot extends SnapshotResponse {
45
36
  version: number;
46
- updatedAt: string | null;
47
- tier: string;
48
- presetTier: string;
49
- pressure: number;
50
- config: {
51
- fastGatePct: number;
52
- thresholdTokens: number;
53
- anchorUserMessages: number;
54
- preserveRecent: number;
55
- auto: boolean;
56
- autoInlineK: number;
57
- };
58
- session: {
59
- id: string | null;
60
- state: string | null;
61
- persistedThisSession: boolean;
62
- lastCheckpointId: string | null;
63
- lastCompactedFrom: number;
64
- };
65
- context: {
66
- tokens: number | null;
67
- percent: number | null;
68
- contextWindow: number;
69
- };
70
- trigger: {
71
- armed: boolean;
72
- ready: boolean;
73
- currentTokens: number | null;
74
- thresholdTokens: number;
75
- fastGatePct: number;
76
- };
77
- store: {
78
- checkpointCount: number;
79
- totalTokenEstimate: number;
80
- originalTokens: number;
81
- tokensSaved: number;
82
- injectedCount: number;
83
- dedupHitRate: number;
84
- storageDedupRate: number;
85
- dedupCollapsed: number;
86
- };
87
- crew: {
88
- activeAgents: number;
89
- currentTurn: number;
90
- };
91
- repo: {
92
- checkpointCount: number;
93
- totalTokenEstimate: number;
94
- originalTokens: number;
95
- tokensSaved: number;
96
- sessionCount: number;
97
- dedupAttempts: number;
98
- dedupCollapsed: number;
99
- storageDedupRate: number;
100
- };
101
- integrity: {
102
- regionsRetained: number;
103
- compressedOriginalBytes: number;
104
- duplicatesCollapsed: number;
105
- bytesPermanentlyDeleted: number;
106
- };
107
- cacheHits: {
108
- session: number;
109
- total: number;
110
- sessionTokensSaved: number;
111
- totalTokensSaved: number;
112
- };
113
- compacts: {
114
- session: number;
115
- total: number;
116
- };
117
- timeSaved: {
118
- compact: { sessionSec: number; totalSec: number };
119
- cacheHit: { sessionSec: number; totalSec: number };
120
- };
121
- compression: {
122
- session: { tokensIn: number; tokensOut: number; tokensFreed: number; compressionPct: number; dedupPct: number };
123
- repo: { tokensIn: number; tokensOut: number; tokensFreed: number; compressionPct: number; dedupPct: number };
124
- };
125
- model?: {
126
- name: string;
127
- provider: string;
128
- providerName: string;
129
- inputRate: number;
130
- outputRate: number;
131
- };
132
37
  }
133
38
 
39
+ /**
40
+ * Minimal live snapshot shape for lightweight dashboard.json overlays.
41
+ * No api-contracts equivalent — kept as-is.
42
+ */
134
43
  export interface LiveSnapshot { tier?: string; updatedAt?: string | null; context?: { tokens?: number | null; percent?: number | null; contextWindow?: number }; session?: { id?: string; state?: string }; cacheHits?: { session: number; total: number; sessionTokensSaved: number; totalTokensSaved: number }; compacts?: { session: number; total: number }; timeSaved?: { compact: { sessionSec: number; totalSec: number }; cacheHit: { sessionSec: number; totalSec: number } }; }
@@ -139,6 +139,13 @@ export interface DashboardSnapshot {
139
139
  inputRate: number; // USD per input token (Model.cost)
140
140
  outputRate: number; // USD per output token (Model.cost)
141
141
  };
142
+ /** v0.8.8 Perf dashboard: live diag counters (skip vs recompute vs replay)
143
+ * for the Perf tab's "TUI lag proxy" cards. Optional for back-compat. */
144
+ diag?: {
145
+ ctxFastGate: number;
146
+ liveTrimFires: number;
147
+ liveTrimReplays: number;
148
+ };
142
149
  }
143
150
 
144
151
  export class Dashboard {
@@ -151,9 +158,19 @@ export class Dashboard {
151
158
  this.eventsPath = join(stateDir, "events.log");
152
159
  }
153
160
 
161
+ /** v0.8.8: duration (ms) of the last dashboard.json write — read by
162
+ * MegaRuntime.snapshot() to record a `disk_write_ms` perf sample without
163
+ * wrapping the giant snapshot object literal at the call site. */
164
+ private _lastWriteMs = 0;
165
+ get lastWriteMs(): number {
166
+ return this._lastWriteMs;
167
+ }
168
+
154
169
  /** Write a full state snapshot (atomically replaces previous). */
155
170
  snapshot(data: DashboardSnapshot): void {
171
+ const t = performance.now();
156
172
  writeFileSync(this.snapshotPath, JSON.stringify(data, null, 2) + "\n");
173
+ this._lastWriteMs = performance.now() - t;
157
174
  }
158
175
 
159
176
  /** Append a timestamped JSONL event line. */
@@ -0,0 +1,113 @@
1
+ /**
2
+ * mega-events/perf-handler.ts — local perf instrumentation handlers (v0.8.8).
3
+ *
4
+ * Captures cheap, local-only telemetry into the `perf_samples` SQLite table for
5
+ * the dashboard's Perf tab: turn + provider latency, TPS, cache hit %, and (via
6
+ * MegaRuntime.ensurePerfInterval) a 5s cpu/mem interval. All capture is wrapped in
7
+ * try/catch — instrumentation NEVER blocks the agent loop (non-fatal).
8
+ *
9
+ * PREVENT-PI-004: Date.now / process.cpuUsage / process.memoryUsage + local
10
+ * SQLite only, zero network.
11
+ * PREVENT-011: no `any` — the usage block is narrowed structurally.
12
+ */
13
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
+ import { type MegaRuntime } from "../mega-runtime.js";
15
+ import { recordPerfSample } from "../../src/store/sqlite.js";
16
+
17
+ /** Structural view of an AssistantMessage usage block (no pi-ai import). */
18
+ interface UsageBlock {
19
+ input: number;
20
+ output: number;
21
+ cacheRead: number;
22
+ cacheWrite: number;
23
+ }
24
+
25
+ /** Narrow a turn_end message to its usage block when it is an assistant msg. */
26
+ function usageOf(
27
+ msg: { role?: string; usage?: UsageBlock },
28
+ ): UsageBlock | null {
29
+ if (msg.role !== "assistant" || !msg.usage) return null;
30
+ return msg.usage;
31
+ }
32
+
33
+ /** Register perf instrumentation handlers + start the 5s cpu/mem interval. */
34
+ export function registerPerfHandler(
35
+ pi: ExtensionAPI,
36
+ runtime: MegaRuntime,
37
+ ): void {
38
+ // turn_start: record the wall-clock start of the turn. Using Date.now() (not
39
+ // event.timestamp) so the turn_end duration is on ONE clock — mixing pi's
40
+ // timestamp with Date.now() would skew the delta. Also (re)arms the cpu/mem
41
+ // interval so a new session after a dispose() resumes sampling on its first
42
+ // turn (the interval is cleared in runtime.dispose()).
43
+ pi.on("turn_start", async () => {
44
+ try {
45
+ runtime.perfTurnStart = Date.now();
46
+ runtime.ensurePerfInterval();
47
+ } catch {
48
+ /* non-fatal */
49
+ }
50
+ });
51
+
52
+ // turn_end: compute turn latency + TPS + cache hit % from the assistant
53
+ // message's usage block. One perf_samples row per metric per turn.
54
+ pi.on("turn_end", async (event) => {
55
+ try {
56
+ if (runtime.perfTurnStart > 0) {
57
+ const durMs = Date.now() - runtime.perfTurnStart;
58
+ recordPerfSample(runtime.currentStateDir, "turn_latency_ms", durMs, {
59
+ turnIndex: event.turnIndex,
60
+ });
61
+ const u = usageOf(event.message);
62
+ if (u) {
63
+ const durSec = Math.max(durMs / 1000, 0.001);
64
+ recordPerfSample(
65
+ runtime.currentStateDir,
66
+ "tps",
67
+ u.output / durSec,
68
+ { outputTokens: u.output },
69
+ );
70
+ const denom = u.cacheRead + u.input + u.cacheWrite;
71
+ const hitPct = denom > 0 ? (u.cacheRead / denom) * 100 : 0;
72
+ recordPerfSample(
73
+ runtime.currentStateDir,
74
+ "cache_hit_pct",
75
+ hitPct,
76
+ { input: u.input, cacheRead: u.cacheRead, cacheWrite: u.cacheWrite },
77
+ );
78
+ }
79
+ }
80
+ } catch {
81
+ /* non-fatal: instrumentation must never break the agent loop */
82
+ }
83
+ });
84
+
85
+ // before_provider_request -> after_provider_response: raw round-trip latency
86
+ // to the model endpoint (HTTP status carried on the response event).
87
+ pi.on("before_provider_request", async () => {
88
+ try {
89
+ runtime.perfProviderStart = Date.now();
90
+ } catch {
91
+ /* non-fatal */
92
+ }
93
+ });
94
+ pi.on("after_provider_response", async (event) => {
95
+ try {
96
+ if (runtime.perfProviderStart > 0) {
97
+ const lat = Date.now() - runtime.perfProviderStart;
98
+ recordPerfSample(
99
+ runtime.currentStateDir,
100
+ "provider_latency_ms",
101
+ lat,
102
+ { status: event.status },
103
+ );
104
+ }
105
+ } catch {
106
+ /* non-fatal */
107
+ }
108
+ });
109
+
110
+ // Start the 5s cpu/mem sampling interval (one per MegaRuntime; cleared in
111
+ // runtime.dispose()). Idempotent — safe to call again after a dispose().
112
+ runtime.ensurePerfInterval();
113
+ }
@@ -12,6 +12,7 @@ import { registerSessionHandlers } from "./session-handlers.js";
12
12
  import { registerAgentHandlers } from "./agent-handlers.js";
13
13
  import { registerContextHandler } from "./context-handler.js";
14
14
  import { registerCompactHandlers } from "./compact-handlers.js";
15
+ import { registerPerfHandler } from "./perf-handler.js";
15
16
 
16
17
  /**
17
18
  * DIAG accessor for the headless test harness: the most recently constructed
@@ -34,4 +35,5 @@ export function registerEventHandlers(
34
35
  registerAgentHandlers(pi, runtime, config);
35
36
  registerContextHandler(pi, runtime, config);
36
37
  registerCompactHandlers(pi, runtime, config);
38
+ registerPerfHandler(pi, runtime);
37
39
  }
@@ -15,3 +15,4 @@ export * from "./mega-events/session-handlers.js";
15
15
  export * from "./mega-events/agent-handlers.js";
16
16
  export * from "./mega-events/context-handler.js";
17
17
  export * from "./mega-events/compact-handlers.js";
18
+ export * from "./mega-events/perf-handler.js";
@@ -28,6 +28,7 @@ import {
28
28
  getRecallInjected,
29
29
  getCacheHitTokensSaved,
30
30
  getGameState,
31
+ recordPerfSample,
31
32
  type ModelSnapshot,
32
33
  type GameState,
33
34
  } from "../../src/store/sqlite.js";
@@ -164,6 +165,12 @@ export class MegaRuntime {
164
165
  // Last explain-why line (dedup reason / anchor-kept / superseded), surfaced
165
166
  // while fresh.
166
167
  lastWhy: string | undefined = undefined;
168
+ // v0.8.8 Perf dashboard instrumentation: turn/provider start timestamps +
169
+ // the 5s cpu/mem interval handle (one per MegaRuntime, cleared in dispose()).
170
+ perfTurnStart = 0;
171
+ perfProviderStart = 0;
172
+ perfCpuInterval: ReturnType<typeof setInterval> | undefined;
173
+ private perfCpuBaseline: { user: number; sys: number } | undefined;
167
174
 
168
175
  // Context tracking for the dashboard (updated in the context handler).
169
176
  lastCtxTokens: number | null = null;
@@ -385,6 +392,7 @@ export class MegaRuntime {
385
392
  this.renderWidget(ctx);
386
393
  return;
387
394
  }
395
+ const perfT0 = performance.now();
388
396
  const st = this.store.stats(this.rt.sessionId);
389
397
  const repo = this.store.repoStats();
390
398
  const di = this.store.dataInvariant();
@@ -540,7 +548,13 @@ export class MegaRuntime {
540
548
  cacheHit: { sessionSec: sec(this.rt.cacheHitTokens), totalSec: sec(cacheHitsTotalTokens) },
541
549
  },
542
550
  model,
551
+ diag: {
552
+ ctxFastGate: this.diagCtxFastGate,
553
+ liveTrimFires: this.diagLiveTrimFires,
554
+ liveTrimReplays: this.diagLiveTrimReplays,
555
+ },
543
556
  } as DashboardSnapshot);
557
+ const perfDiskMs = this.dashboard.lastWriteMs;
544
558
 
545
559
  // Live stats widget above the editor
546
560
  if (ctx) {
@@ -708,6 +722,12 @@ export class MegaRuntime {
708
722
  }
709
723
  // v0.8.5: record the material-change signature computed at the top so the
710
724
  // next snapshot() can skip this whole body when nothing material changed.
725
+ try {
726
+ recordPerfSample(this.currentStateDir, "db_recompute_ms", performance.now() - perfT0);
727
+ recordPerfSample(this.currentStateDir, "disk_write_ms", perfDiskMs);
728
+ } catch {
729
+ /* non-fatal: perf instrumentation never blocks the agent */
730
+ }
711
731
  this.lastSnapshotSig = sig;
712
732
  }
713
733
 
@@ -981,6 +1001,43 @@ export class MegaRuntime {
981
1001
  this.gameStateWatcher = undefined;
982
1002
  this.gameStateWatchDir = undefined;
983
1003
  }
1004
+ // v0.8.8: stop the cpu/mem sampling interval on teardown. Re-armed lazily
1005
+ // by ensurePerfInterval() on the next turn_start.
1006
+ if (this.perfCpuInterval) {
1007
+ clearInterval(this.perfCpuInterval);
1008
+ this.perfCpuInterval = undefined;
1009
+ this.perfCpuBaseline = undefined;
1010
+ }
1011
+ }
1012
+
1013
+ /** v0.8.8: (re)start the 5s cpu/mem sampling interval (idempotent). One per
1014
+ * MegaRuntime; cleared in dispose(). Samples process.cpuUsage() (user/sys
1015
+ * delta vs the last tick → ms) + process.memoryUsage() (rss/heap → MB) and
1016
+ * records them as perf_samples. unref'd so it never keeps the process alive
1017
+ * on its own. Non-fatal: any failure is swallowed (instrumentation never
1018
+ * blocks the agent). PREVENT-PI-004: local process stats + SQLite only. */
1019
+ ensurePerfInterval(): void {
1020
+ if (this.perfCpuInterval) return;
1021
+ this.perfCpuBaseline = undefined; // first tick sets the baseline (no delta)
1022
+ this.perfCpuInterval = setInterval(() => {
1023
+ try {
1024
+ const dir = this.currentStateDir;
1025
+ const cpu = process.cpuUsage();
1026
+ const mem = process.memoryUsage();
1027
+ if (this.perfCpuBaseline) {
1028
+ const du = (cpu.user - this.perfCpuBaseline.user) / 1000; // μs → ms
1029
+ const ds = (cpu.system - this.perfCpuBaseline.sys) / 1000;
1030
+ recordPerfSample(dir, "cpu_user_ms", Math.max(0, du));
1031
+ recordPerfSample(dir, "cpu_sys_ms", Math.max(0, ds));
1032
+ }
1033
+ this.perfCpuBaseline = { user: cpu.user, sys: cpu.system };
1034
+ recordPerfSample(dir, "rss_mb", mem.rss / 1_000_000);
1035
+ recordPerfSample(dir, "heap_mb", mem.heapUsed / 1_000_000);
1036
+ } catch {
1037
+ /* non-fatal */
1038
+ }
1039
+ }, 5000);
1040
+ this.perfCpuInterval.unref?.();
984
1041
  }
985
1042
 
986
1043
  /** S31: the cached game-mode state (game_mode_on/theme/tui_display_mode).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.8.7",
3
+ "version": "0.8.10",
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-2-Clause",
@@ -41,6 +41,7 @@
41
41
  },
42
42
  "scripts": {
43
43
  "build": "tsc -p tsconfig.json",
44
+ "build:dashboard": "cd extensions/dashboard-client && npx vite build",
44
45
  "lint": "tsc --noEmit && node scripts/guardrails-scan.mjs && node scripts/semantic-scan.mjs",
45
46
  "test": "npm run build && node scripts/run-tests.mjs",
46
47
  "guardrails": "python3 scripts/regression_check.py --all || node scripts/guardrails-scan.mjs || node scripts/semantic-scan.mjs",
@@ -0,0 +1,65 @@
1
+ /**
2
+ * perf-samples.test.ts — v0.8.8 perf_samples table round-trip + filtering.
3
+ * Pi-agnostic. Uses an isolated state dir (never the real user dir — G7).
4
+ */
5
+ import { describe, it, before, after } from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { mkdtempSync, rmSync } from "node:fs";
10
+ import { closeStore } from "./utils.js";
11
+ import {
12
+ recordPerfSample,
13
+ readPerfSamples,
14
+ PERF_KINDS,
15
+ } from "./perf-samples.js";
16
+
17
+ describe("perf-samples (v0.8.8)", () => {
18
+ let dir: string;
19
+ before(() => {
20
+ dir = mkdtempSync(join(tmpdir(), "mc-perfsamples-"));
21
+ process.env.MEGACOMPACT_STATE_DIR = dir;
22
+ });
23
+ after(() => {
24
+ closeStore(dir);
25
+ delete process.env.MEGACOMPACT_STATE_DIR;
26
+ rmSync(dir, { recursive: true, force: true });
27
+ });
28
+
29
+ it("records + reads back a turn_latency_ms sample with parsed meta", () => {
30
+ recordPerfSample(dir, "turn_latency_ms", 123.4, { turnIndex: 2 });
31
+ const rows = readPerfSamples(dir, 0);
32
+ assert.equal(rows.length, 1);
33
+ assert.equal(rows[0].kind, "turn_latency_ms");
34
+ assert.equal(rows[0].value, 123.4);
35
+ assert.deepEqual(rows[0].meta, { turnIndex: 2 });
36
+ });
37
+
38
+ it("filters by kind and by sinceTs", () => {
39
+ recordPerfSample(dir, "tps", 50);
40
+ recordPerfSample(dir, "rss_mb", 256);
41
+ const tps = readPerfSamples(dir, 0, "tps");
42
+ assert.equal(tps.length, 1);
43
+ assert.equal(tps[0].kind, "tps");
44
+ assert.equal(tps[0].value, 50);
45
+ const future = readPerfSamples(dir, Date.now() + 10000, "tps");
46
+ assert.equal(future.length, 0);
47
+ });
48
+
49
+ it("ignores non-finite values + unknown kinds (never throws, nothing added)", () => {
50
+ const before = readPerfSamples(dir, 0).length;
51
+ recordPerfSample(dir, "tps", Number.NaN);
52
+ recordPerfSample(dir, "tps", Infinity);
53
+ assert.doesNotThrow(() =>
54
+ recordPerfSample(dir, "bogus" as never, 1),
55
+ );
56
+ const after = readPerfSamples(dir, 0).length;
57
+ assert.equal(after, before);
58
+ });
59
+
60
+ it("PERF_KINDS lists the 10 instrumentation kinds", () => {
61
+ assert.equal(PERF_KINDS.length, 10);
62
+ assert.ok(PERF_KINDS.includes("db_recompute_ms"));
63
+ assert.ok(PERF_KINDS.includes("cache_hit_pct"));
64
+ });
65
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * perf-samples.ts — `perf_samples` table accessors (v0.8.8 Perf dashboard).
3
+ *
4
+ * Append-only local instrumentation store for the dashboard's Perf tab: model
5
+ * endpoint latency, TPS, cache hit %, CPU/mem, and the snapshot() recompute /
6
+ * disk-write cost. One row per sample; the dashboard server reads a rolling
7
+ * window and derives p50/p95 + latest values.
8
+ *
9
+ * PREVENT-PI-004: local SQLite only, zero network.
10
+ * PREVENT-002: all SQL parameterized (? placeholders). The optional `kind`
11
+ * filter is bound as a parameter (never string-concatenated); the only
12
+ * interpolated fragment is the code-controlled `AND kind = ?` clause toggle,
13
+ * never external input.
14
+ * Pi-agnostic: no pi runtime types (mirrors game-scores.ts / meta.ts).
15
+ */
16
+ import { getStateDir } from "../../store.js";
17
+ import { openStore } from "./utils.js";
18
+
19
+ /** Sample kinds recorded into perf_samples. */
20
+ export type PerfKind =
21
+ | "turn_latency_ms"
22
+ | "provider_latency_ms"
23
+ | "tps"
24
+ | "cache_hit_pct"
25
+ | "rss_mb"
26
+ | "heap_mb"
27
+ | "cpu_user_ms"
28
+ | "cpu_sys_ms"
29
+ | "db_recompute_ms"
30
+ | "disk_write_ms";
31
+
32
+ /** Allow-list of valid perf sample kinds (mirrors the table's domain). */
33
+ export const PERF_KINDS: readonly PerfKind[] = [
34
+ "turn_latency_ms",
35
+ "provider_latency_ms",
36
+ "tps",
37
+ "cache_hit_pct",
38
+ "rss_mb",
39
+ "heap_mb",
40
+ "cpu_user_ms",
41
+ "cpu_sys_ms",
42
+ "db_recompute_ms",
43
+ "disk_write_ms",
44
+ ];
45
+
46
+ /** A single perf sample row (as stored + returned). */
47
+ export interface PerfSampleRow {
48
+ id: number;
49
+ ts: number;
50
+ kind: PerfKind;
51
+ value: number;
52
+ meta: unknown;
53
+ }
54
+
55
+ function isPerfKind(k: string): k is PerfKind {
56
+ return (PERF_KINDS as readonly string[]).includes(k);
57
+ }
58
+
59
+ /**
60
+ * Record one perf sample. `ts` is set to Date.now(). SQL is fully parameterized
61
+ * (PREVENT-002); the kind is validated against the fixed allow-list. Pi-agnostic.
62
+ * Never throws on an unknown kind or non-finite value (silently ignored) so
63
+ * instrumentation can never block the agent; a known kind + finite value always
64
+ * writes.
65
+ */
66
+ export function recordPerfSample(
67
+ stateDir: string = getStateDir(),
68
+ kind: PerfKind,
69
+ value: number,
70
+ meta?: unknown,
71
+ ): void {
72
+ if (!isPerfKind(kind)) return;
73
+ if (!Number.isFinite(value)) return;
74
+ const db = openStore(stateDir);
75
+ db.prepare(
76
+ `INSERT INTO perf_samples (ts, kind, value, meta)
77
+ VALUES (?, ?, ?, ?)`,
78
+ ).run(
79
+ Date.now(),
80
+ kind,
81
+ value,
82
+ meta != null ? JSON.stringify(meta) : null,
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Read perf samples since `sinceTs` (epoch ms), optionally filtered by kind.
88
+ * Returns rows ascending by ts. The optional kind filter is bound as a
89
+ * parameter (PREVENT-002). Pi-agnostic. `meta` is parsed defensively (null-safe:
90
+ * PREVENT-001 — assigned to a variable before any property access).
91
+ */
92
+ export function readPerfSamples(
93
+ stateDir: string = getStateDir(),
94
+ sinceTs: number = 0,
95
+ kind?: PerfKind,
96
+ ): PerfSampleRow[] {
97
+ const db = openStore(stateDir);
98
+ const sql = kind
99
+ ? `SELECT id, ts, kind, value, meta FROM perf_samples
100
+ WHERE ts >= ? AND kind = ? ORDER BY ts ASC`
101
+ : `SELECT id, ts, kind, value, meta FROM perf_samples
102
+ WHERE ts >= ? ORDER BY ts ASC`;
103
+ const params = kind ? [sinceTs, kind] : [sinceTs];
104
+ const rows = db.prepare(sql).all(...params) as Array<{
105
+ id: number;
106
+ ts: number;
107
+ kind: string;
108
+ value: number;
109
+ meta: string | null;
110
+ }>;
111
+ const out: PerfSampleRow[] = [];
112
+ for (const r of rows) {
113
+ if (!isPerfKind(r.kind)) continue; // defensive: unknown kind row skipped
114
+ let meta: unknown = null;
115
+ if (r.meta != null) {
116
+ try {
117
+ meta = JSON.parse(r.meta);
118
+ } catch {
119
+ meta = null;
120
+ }
121
+ }
122
+ out.push({ id: r.id, ts: r.ts, kind: r.kind, value: r.value, meta });
123
+ }
124
+ return out;
125
+ }
@@ -267,6 +267,20 @@ export function initSchema(db: DatabaseSync): void {
267
267
  icon TEXT,
268
268
  unlocked_at INTEGER NULL
269
269
  ) WITHOUT ROWID;
270
+
271
+ -- v0.8.8 Perf dashboard: append-only local instrumentation samples (one row
272
+ -- per turn / provider round-trip / 5s cpu-mem tick / snapshot-recompute).
273
+ -- Drives the dashboard Perf tab. Local SQLite (PREVENT-PI-004); parameterized
274
+ -- accessors in perf-samples.ts (PREVENT-002).
275
+ CREATE TABLE IF NOT EXISTS perf_samples (
276
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
277
+ ts INTEGER NOT NULL,
278
+ kind TEXT NOT NULL,
279
+ value REAL NOT NULL,
280
+ meta TEXT
281
+ );
282
+ CREATE INDEX IF NOT EXISTS idx_perf_samples_ts ON perf_samples(ts);
283
+ CREATE INDEX IF NOT EXISTS idx_perf_samples_kind_ts ON perf_samples(kind, ts);
270
284
  `);
271
285
  // Idempotent column migrations. `CREATE TABLE IF NOT EXISTS` is a no-op on a
272
286
  // pre-existing table, so new columns added to context_chunks after a store was
@@ -23,3 +23,4 @@ export * from "./sqlite/maintenance.js";
23
23
  export * from "./sqlite/game-state.js";
24
24
  export * from "./sqlite/game-scores.js";
25
25
  export * from "./sqlite/game-achievements.js";
26
+ export * from "./sqlite/perf-samples.js";