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.
- package/dist/extensions/dashboard-server/api-contracts/core.js +7 -0
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +104 -0
- package/dist/extensions/dashboard-server/api-contracts/game.js +9 -0
- package/dist/extensions/dashboard-server/api-contracts/index.js +9 -0
- package/dist/extensions/dashboard-server/api-contracts/infrastructure.js +10 -0
- package/dist/extensions/dashboard-server/api-contracts/multi-repo.js +9 -0
- package/dist/extensions/dashboard-server/api-contracts/snapshot.js +8 -0
- package/dist/extensions/dashboard-server/api-contracts.js +8 -0
- package/dist/extensions/dashboard-server/api-contracts.test.js +869 -0
- package/dist/extensions/dashboard-server/auth.js +18 -0
- package/dist/extensions/dashboard-server/helpers.js +37 -0
- package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
- package/dist/extensions/dashboard-server/html/body-open.js +23 -0
- package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
- package/dist/extensions/dashboard-server/html/head-open.js +16 -0
- package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
- package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
- package/dist/extensions/dashboard-server/html/script.js +259 -0
- package/dist/extensions/dashboard-server/html/styles.js +103 -0
- package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
- package/dist/extensions/dashboard-server/html-template.js +41 -0
- package/dist/extensions/dashboard-server/html.js +46 -1
- package/dist/extensions/dashboard-server/perf-server.test.js +80 -0
- package/dist/extensions/dashboard-server/server.js +269 -30
- package/dist/extensions/dashboard-server/tailscale.js +8 -0
- package/dist/extensions/dashboard-server/types.js +4 -0
- package/dist/extensions/mega-dashboard.js +9 -0
- package/dist/extensions/mega-events/perf-handler.js +71 -0
- package/dist/extensions/mega-events/register.js +2 -0
- package/dist/extensions/mega-events.js +1 -0
- package/dist/extensions/mega-runtime/state.js +59 -1
- package/dist/src/store/sqlite/connection.js +35 -0
- package/dist/src/store/sqlite/index-store.js +167 -0
- package/dist/src/store/sqlite/memory.js +54 -0
- package/dist/src/store/sqlite/minhash-lsh.js +47 -0
- package/dist/src/store/sqlite/perf-samples.js +81 -0
- package/dist/src/store/sqlite/perf-samples.test.js +54 -0
- package/dist/src/store/sqlite/schema.js +14 -0
- package/dist/src/store/sqlite/sessions.js +39 -0
- package/dist/src/store/sqlite/transaction.js +19 -0
- package/dist/src/store/sqlite.js +1 -0
- package/dist/src/vectorStore/add.js +260 -0
- package/dist/src/vectorStore/dedup.js +52 -0
- package/dist/src/vectorStore/index.js +10 -0
- package/dist/src/vectorStore/queries.js +83 -0
- package/dist/src/vectorStore/search.js +95 -0
- package/dist/src/vectorStore/session.js +19 -0
- package/dist/src/vectorStore/store.js +105 -0
- package/dist/src/vectorStore/types.js +6 -0
- package/dist/src/vectorStore/utils.js +23 -0
- package/extensions/dashboard-client/package-lock.json +1771 -0
- package/extensions/dashboard-client/package.json +27 -0
- package/extensions/dashboard-client/src/App.tsx +82 -0
- package/extensions/dashboard-client/src/api/client.ts +145 -0
- package/extensions/dashboard-client/src/components/CacheStatusPerModel.tsx +44 -0
- package/extensions/dashboard-client/src/components/CompressionCard.tsx +61 -0
- package/extensions/dashboard-client/src/components/ContextGauge.tsx +59 -0
- package/extensions/dashboard-client/src/components/DataSafetyCard.tsx +49 -0
- package/extensions/dashboard-client/src/components/ErrorBoundary.tsx +53 -0
- package/extensions/dashboard-client/src/components/EventCategoryFilter.tsx +16 -0
- package/extensions/dashboard-client/src/components/EventStream.tsx +152 -0
- package/extensions/dashboard-client/src/components/LoadingSpinner.tsx +7 -0
- package/extensions/dashboard-client/src/components/MemoryStatusCard.tsx +24 -0
- package/extensions/dashboard-client/src/components/ModelBadge.tsx +41 -0
- package/extensions/dashboard-client/src/components/PerfChart.tsx +160 -0
- package/extensions/dashboard-client/src/components/RepoDetailModal.tsx +126 -0
- package/extensions/dashboard-client/src/components/RepoTable.tsx +150 -0
- package/extensions/dashboard-client/src/components/SessionInfo.tsx +65 -0
- package/extensions/dashboard-client/src/components/SummaryTiles.tsx +52 -0
- package/extensions/dashboard-client/src/components/TabBar.tsx +34 -0
- package/extensions/dashboard-client/src/components/TriggerStatus.tsx +62 -0
- package/extensions/dashboard-client/src/hooks/useApi.ts +77 -0
- package/extensions/dashboard-client/src/hooks/useSSE.ts +87 -0
- package/extensions/dashboard-client/src/index.html +12 -0
- package/extensions/dashboard-client/src/main.tsx +23 -0
- package/extensions/dashboard-client/src/styles/base.css +121 -0
- package/extensions/dashboard-client/src/styles/overview-events.css +318 -0
- package/extensions/dashboard-client/src/styles/repos-metrics.css +307 -0
- package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +16 -0
- package/extensions/dashboard-client/src/tabs/EventsTab.tsx +37 -0
- package/extensions/dashboard-client/src/tabs/MetricsTab.tsx +49 -0
- package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +80 -0
- package/extensions/dashboard-client/src/tabs/ReposTab.tsx +59 -0
- package/extensions/dashboard-client/tsconfig.json +28 -0
- package/extensions/dashboard-client/vite.config.ts +38 -0
- package/extensions/dashboard-server/api-contracts/core.ts +302 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +476 -0
- package/extensions/dashboard-server/api-contracts/game.ts +145 -0
- package/extensions/dashboard-server/api-contracts/index.ts +159 -0
- package/extensions/dashboard-server/api-contracts/infrastructure.ts +465 -0
- package/extensions/dashboard-server/api-contracts/multi-repo.ts +328 -0
- package/extensions/dashboard-server/api-contracts/snapshot.ts +386 -0
- package/extensions/dashboard-server/api-contracts.test.ts +1050 -0
- package/extensions/dashboard-server/api-contracts.ts +96 -0
- package/extensions/dashboard-server/auth.ts +20 -0
- package/extensions/dashboard-server/html.ts +46 -1
- package/extensions/dashboard-server/perf-server.test.ts +101 -0
- package/extensions/dashboard-server/server.ts +862 -503
- package/extensions/dashboard-server/tailscale.ts +7 -0
- package/extensions/dashboard-server/types.ts +23 -114
- package/extensions/mega-dashboard.ts +17 -0
- package/extensions/mega-events/perf-handler.ts +113 -0
- package/extensions/mega-events/register.ts +2 -0
- package/extensions/mega-events.ts +1 -0
- package/extensions/mega-runtime/state.ts +57 -0
- package/package.json +2 -1
- package/src/store/sqlite/perf-samples.test.ts +65 -0
- package/src/store/sqlite/perf-samples.ts +125 -0
- package/src/store/sqlite/schema.ts +14 -0
- 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
|
-
|
|
9
|
-
|
|
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:
|
|
29
|
+
export type IndexIndex = { updatedAt: string; summary: IndexSummary | null; repos: IndexesIndexRow[] };
|
|
43
30
|
|
|
44
|
-
|
|
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
|
}
|
|
@@ -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.
|
|
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
|
package/src/store/sqlite.ts
CHANGED