pi-mega-compact 0.9.2 → 0.10.1
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/README.md +3 -2
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +95 -47
- package/dist/extensions/dashboard-server/api-contracts/index.js +1 -1
- package/dist/extensions/dashboard-server/api-contracts/turns.js +10 -0
- package/dist/extensions/dashboard-server/api-contracts.test.js +3 -2
- package/dist/extensions/dashboard-server/routes-topics.js +87 -0
- package/dist/extensions/dashboard-server/routes-turns.js +251 -0
- package/dist/extensions/dashboard-server/routes.js +3 -1
- package/dist/extensions/dashboard-server/server.js +6 -2
- package/dist/extensions/mega-compact.js +10 -6
- package/dist/extensions/mega-config.js +4 -0
- package/dist/extensions/mega-events/agent-handlers.js +62 -49
- package/dist/extensions/mega-events/context-handler.js +48 -2
- package/dist/extensions/mega-events/session-handlers.js +24 -0
- package/dist/extensions/mega-metrics-cmds.js +90 -0
- package/dist/extensions/mega-pipeline/recall.js +47 -17
- package/dist/extensions/mega-topics-cmds.js +111 -0
- package/dist/extensions/mega-turn-store.js +110 -0
- package/dist/extensions/mega-turn-store.test.js +68 -0
- package/dist/src/config/turns.js +48 -0
- package/dist/src/fork.js +33 -0
- package/dist/src/fork.test.js +81 -0
- package/dist/src/intent.js +51 -0
- package/dist/src/intent.test.js +78 -0
- package/dist/src/metrics/index.js +4 -0
- package/dist/src/metrics/turns.js +118 -0
- package/dist/src/metrics/turns.test.js +152 -0
- package/dist/src/store/sqlite/raw-transcript.js +5 -3
- package/dist/src/store/sqlite.dbmirror.test.js +9 -0
- package/dist/src/store/turns/connection.js +123 -0
- package/dist/src/store/turns/contract-compliance.test.js +428 -0
- package/dist/src/store/turns/index.js +31 -0
- package/dist/src/store/turns/memory-store.js +357 -0
- package/dist/src/store/turns/memory-store.test.js +9 -0
- package/dist/src/store/turns/migrations.js +178 -0
- package/dist/src/store/turns/migrations.test.js +138 -0
- package/dist/src/store/turns/schema.js +115 -0
- package/dist/src/store/turns/sqlite-store.js +455 -0
- package/dist/src/store/turns/sqlite-store.test.js +144 -0
- package/dist/src/store/turns/types.js +16 -0
- package/dist/src/topics/cluster.js +141 -0
- package/dist/src/topics/cluster.test.js +150 -0
- package/dist/src/topics/index.js +6 -0
- package/dist/src/topics/kselection.js +142 -0
- package/dist/src/topics/labels.js +56 -0
- package/dist/src/topics/store.js +119 -0
- package/dist/src/topics/store.test.js +117 -0
- package/dist/src/topics/types.js +8 -0
- package/dist/src/wiki.js +133 -0
- package/dist/src/wiki.test.js +171 -0
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js → AchievementTiles-DUulvXUY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-QPo46Pwg.js.map → AchievementTiles-DUulvXUY.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js → AchievementsTab-Dx0YtCNf.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-BDJxRXRY.js.map → AchievementsTab-Dx0YtCNf.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js → CacheTab-DtkaUo1v.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-CUMqXQjd.js.map → CacheTab-DtkaUo1v.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js → ConfigTab-CLa4KfiZ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-j1dRzurG.js.map → ConfigTab-CLa4KfiZ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js → EventsTab-Q6tEIBBi.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-CMglXK4p.js.map → EventsTab-Q6tEIBBi.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js → GameTab-Hq7GNWSY.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-fpaDoyxi.js.map → GameTab-Hq7GNWSY.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js → MetricsTab-BtmBV6lU.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CWG2crG_.js.map → MetricsTab-BtmBV6lU.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js → OverviewTab-CL10PwVU.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-BaV2D2nf.js.map → OverviewTab-CL10PwVU.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js → ReposTab-DAwANwbI.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-C-0kuZ_0.js.map → ReposTab-DAwANwbI.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js → SessionsTab-Ec08TZRQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-CSYhddCA.js.map → SessionsTab-Ec08TZRQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js → TimeSavedCard-COb8dpTy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Dy4KRjSM.js.map → TimeSavedCard-COb8dpTy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TopicsTab-kvX3F3dY.js +2 -0
- package/extensions/dashboard-client/dist/assets/TopicsTab-kvX3F3dY.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-CCUE73Aa.js +2 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-CCUE73Aa.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{index-D_WtU2TV.js → index-UMrussFX.js} +10 -10
- package/extensions/dashboard-client/dist/assets/index-UMrussFX.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{index-og3ghxyY.css → index-gn66lYD2.css} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js → useSSE-BDHZ7CeS.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-C9yVJlGP.js.map → useSSE-BDHZ7CeS.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/App.tsx +10 -2
- package/extensions/dashboard-client/src/api/client.ts +84 -1
- package/extensions/dashboard-client/src/main.tsx +1 -0
- package/extensions/dashboard-client/src/styles/turns.css +142 -0
- package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +185 -0
- package/extensions/dashboard-client/src/tabs/TurnsTab.tsx +326 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +389 -291
- package/extensions/dashboard-server/api-contracts/game-types.ts +141 -88
- package/extensions/dashboard-server/api-contracts/index.ts +153 -133
- package/extensions/dashboard-server/api-contracts/turns.ts +114 -0
- package/extensions/dashboard-server/api-contracts.test.ts +920 -919
- package/extensions/dashboard-server/routes-topics.ts +119 -0
- package/extensions/dashboard-server/routes-turns.ts +282 -0
- package/extensions/dashboard-server/routes.ts +8 -1
- package/extensions/dashboard-server/server.ts +6 -2
- package/extensions/mega-compact.ts +75 -59
- package/extensions/mega-config.ts +232 -220
- package/extensions/mega-events/agent-handlers.ts +194 -151
- package/extensions/mega-events/context-handler.ts +50 -1
- package/extensions/mega-events/session-handlers.ts +23 -0
- package/extensions/mega-metrics-cmds.ts +114 -0
- package/extensions/mega-pipeline/recall.ts +215 -152
- package/extensions/mega-topics-cmds.ts +151 -0
- package/extensions/mega-turn-store.test.ts +108 -0
- package/extensions/mega-turn-store.ts +169 -0
- package/package.json +3 -1
- package/src/config/turns.ts +69 -0
- package/src/fork.test.ts +94 -0
- package/src/fork.ts +78 -0
- package/src/intent.test.ts +87 -0
- package/src/intent.ts +115 -0
- package/src/metrics/index.ts +9 -0
- package/src/metrics/turns.test.ts +165 -0
- package/src/metrics/turns.ts +201 -0
- package/src/store/sqlite/raw-transcript.ts +9 -3
- package/src/store/sqlite.dbmirror.test.ts +10 -0
- package/src/store/turns/connection.ts +137 -0
- package/src/store/turns/contract-compliance.test.ts +562 -0
- package/src/store/turns/index.ts +65 -0
- package/src/store/turns/memory-store.test.ts +15 -0
- package/src/store/turns/memory-store.ts +469 -0
- package/src/store/turns/migrations.test.ts +161 -0
- package/src/store/turns/migrations.ts +203 -0
- package/src/store/turns/schema.ts +158 -0
- package/src/store/turns/sqlite-store.test.ts +161 -0
- package/src/store/turns/sqlite-store.ts +656 -0
- package/src/store/turns/types.ts +181 -0
- package/src/topics/cluster.test.ts +176 -0
- package/src/topics/cluster.ts +191 -0
- package/src/topics/index.ts +18 -0
- package/src/topics/kselection.ts +182 -0
- package/src/topics/labels.ts +67 -0
- package/src/topics/store.test.ts +132 -0
- package/src/topics/store.ts +208 -0
- package/src/topics/types.ts +64 -0
- package/src/wiki.test.ts +217 -0
- package/src/wiki.ts +198 -0
- package/dist/extensions/openclaw-mega-compact.js +0 -291
- package/dist/src/minilm.js +0 -92
- package/dist/src/wordpiece.js +0 -129
- package/extensions/dashboard-client/dist/assets/index-D_WtU2TV.js.map +0 -1
|
@@ -35,35 +35,37 @@ import { registerDashboardCommands } from "./mega-dashboard-cmds.js";
|
|
|
35
35
|
import { registerConflictCommands } from "./mega-conflict-cmds.js";
|
|
36
36
|
import { registerDbCommands } from "./mega-db-cmds.js";
|
|
37
37
|
import { registerGameCommands } from "./mega-game-cmds.js";
|
|
38
|
+
import { registerMetricsCommands } from "./mega-metrics-cmds.js";
|
|
39
|
+
import { registerTopicsCommands } from "./mega-topics-cmds.js";
|
|
38
40
|
export default function (pi) {
|
|
39
41
|
const config = loadConfig();
|
|
40
42
|
// S38.9: preflight env validation — check for obviously invalid values at startup.
|
|
41
43
|
// Non-fatal: log warnings and fall back to defaults.
|
|
42
44
|
if (config.autoRetryTransientMax < 0) {
|
|
43
|
-
console.warn(
|
|
45
|
+
console.warn("[mega-compact] MEGACOMPACT_AUTO_RETRY_TRANSIENT_MAX must be >= 0; using default 5");
|
|
44
46
|
config.autoRetryTransientMax = 5;
|
|
45
47
|
}
|
|
46
48
|
if (config.autoRetryPermanentMax < 0) {
|
|
47
|
-
console.warn(
|
|
49
|
+
console.warn("[mega-compact] MEGACOMPACT_AUTO_RETRY_PERMANENT_MAX must be >= 0; using default 1");
|
|
48
50
|
config.autoRetryPermanentMax = 1;
|
|
49
51
|
}
|
|
50
52
|
if (config.maxConsecutiveErrors < 1) {
|
|
51
|
-
console.warn(
|
|
53
|
+
console.warn("[mega-compact] MEGACOMPACT_MAX_CONSECUTIVE_ERRORS must be >= 1; using default 10");
|
|
52
54
|
config.maxConsecutiveErrors = 10;
|
|
53
55
|
}
|
|
54
56
|
// R2: session-global cap validation — 0 disables (valid), negative is invalid.
|
|
55
57
|
if (config.errorRetrySessionMax < 0) {
|
|
56
|
-
console.warn(
|
|
58
|
+
console.warn("[mega-compact] MEGACOMPACT_ERROR_RETRY_SESSION_MAX must be >= 0; using default 3");
|
|
57
59
|
config.errorRetrySessionMax = 3;
|
|
58
60
|
}
|
|
59
61
|
// R1: backoff base must be >= 0 (0 means no gating, useful for tests).
|
|
60
62
|
if (config.errorRetryBackoffMs < 0) {
|
|
61
|
-
console.warn(
|
|
63
|
+
console.warn("[mega-compact] MEGACOMPACT_ERROR_RETRY_BACKOFF_MS must be >= 0; using default 5000");
|
|
62
64
|
config.errorRetryBackoffMs = 5000;
|
|
63
65
|
}
|
|
64
66
|
// R3: repeat threshold must be >= 1.
|
|
65
67
|
if (config.poisonedContextRepeatThreshold < 1) {
|
|
66
|
-
console.warn(
|
|
68
|
+
console.warn("[mega-compact] MEGACOMPACT_POISONED_REPEAT_THRESHOLD must be >= 1; using default 3");
|
|
67
69
|
config.poisonedContextRepeatThreshold = 3;
|
|
68
70
|
}
|
|
69
71
|
const runtime = new MegaRuntime(config);
|
|
@@ -72,7 +74,9 @@ export default function (pi) {
|
|
|
72
74
|
registerDashboardCommands(pi, runtime);
|
|
73
75
|
registerConflictCommands(pi, runtime);
|
|
74
76
|
registerDbCommands(pi, runtime);
|
|
77
|
+
registerMetricsCommands(pi, runtime, config);
|
|
75
78
|
registerGameCommands(pi, runtime);
|
|
79
|
+
registerTopicsCommands(pi, runtime, config);
|
|
76
80
|
// v0.8.5 (audit P3): release the fs.watch game-state watcher handle on
|
|
77
81
|
// session teardown so it doesn't linger across reloads. pi exposes no
|
|
78
82
|
// extension-unload event (the factory return value is ignored and there is no
|
|
@@ -154,6 +154,10 @@ export function loadConfig() {
|
|
|
154
154
|
raptorEnabled: envBool("MEGACOMPACT_RAPTOR_ENABLED", true),
|
|
155
155
|
legacyDurableTrim: envBool("MEGACOMPACT_LEGACY_DURABLE_TRIM", false),
|
|
156
156
|
dbMirror: envBool("MEGACOMPACT_DB_MIRROR", false),
|
|
157
|
+
// S49: isolated per-turn store (turns.db). Default ON. OFF = legacy main-db
|
|
158
|
+
// turn path (S48 behavior). Mirrors src/config/turns.ts TURNS_DB_ENABLED.
|
|
159
|
+
turnsDbEnabled: envBool("MEGACOMPACT_TURNS_DB", true),
|
|
160
|
+
autoWikiEnabled: envBool("MEGACOMPACT_AUTO_WIKI", true),
|
|
157
161
|
crossRepoEnabled: envBool("MEGACOMPACT_CROSSREPO_ENABLED", true),
|
|
158
162
|
crossRepoCosine: Number(process.env.MEGACOMPACT_CROSSREPO_COSINE ?? "0.90"),
|
|
159
163
|
memoryAutoReview: envBool("MEGACOMPACT_MEMORY_AUTO_REVIEW", true),
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { piCompactWouldNoop, runMemoryReview
|
|
2
|
-
import { memoryReviewCadence
|
|
3
|
-
import { recordScore
|
|
1
|
+
import { piCompactWouldNoop, runMemoryReview } from "../mega-pipeline.js";
|
|
2
|
+
import { memoryReviewCadence } from "../mega-config.js";
|
|
3
|
+
import { recordScore } from "../../src/store/sqlite.js";
|
|
4
4
|
import { evaluateAndUnlockAchievements } from "../../src/store/sqlite/game-achievements.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ensureConversationIdFor, recordTurnWrite, } from "../mega-turn-store.js";
|
|
6
6
|
import { isMegaCache } from "../../src/game/scoring.js";
|
|
7
7
|
import { resolveRepoRoot } from "../mega-config.js";
|
|
8
|
-
import { classifyError, errorRetryBackoffMs, extractErrorSignature } from "./error-classifier.js";
|
|
8
|
+
import { classifyError, errorRetryBackoffMs, extractErrorSignature, } from "./error-classifier.js";
|
|
9
9
|
import { safeSendUserMessage } from "./send-safe.js";
|
|
10
10
|
import { vectorStats } from "../../src/vectorStore.js";
|
|
11
11
|
/** Register agent/turn tracking event handlers. */
|
|
@@ -36,7 +36,8 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
36
36
|
// compaction AND there is queued work AND we haven't nudged recently, nudge
|
|
37
37
|
// once so the agent continues (the live trim should make this rare). Guarded
|
|
38
38
|
// to never busy-loop: one nudge per 30s, only when truly idle + queued.
|
|
39
|
-
if ((config.auto || config.autoContinueLengthStop) &&
|
|
39
|
+
if ((config.auto || config.autoContinueLengthStop) &&
|
|
40
|
+
runtime.activeAgents === 0) {
|
|
40
41
|
try {
|
|
41
42
|
const idle = ctx.isIdle?.() ?? true;
|
|
42
43
|
const queued = ctx.hasPendingMessages?.() ?? false;
|
|
@@ -51,7 +52,7 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
51
52
|
const liveUsage = ctx.getContextUsage?.();
|
|
52
53
|
const liveTokens = typeof liveUsage?.tokens === "number"
|
|
53
54
|
? liveUsage.tokens
|
|
54
|
-
: runtime.lastCtxTokens ?? 0;
|
|
55
|
+
: (runtime.lastCtxTokens ?? 0);
|
|
55
56
|
// Keep the cache fresh for snapshot()/diag regardless of which source we use.
|
|
56
57
|
if (typeof liveUsage?.tokens === "number") {
|
|
57
58
|
runtime.lastCtxTokens = liveUsage.tokens;
|
|
@@ -104,7 +105,10 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
104
105
|
// most. Instead we DECOUPLE the nudge from `queued`: after a durable
|
|
105
106
|
// trim we ALWAYS nudge so the agent reliably restarts. Debounced 30s.
|
|
106
107
|
let didDurableTrim = false;
|
|
107
|
-
if (config.auto &&
|
|
108
|
+
if (config.auto &&
|
|
109
|
+
idle &&
|
|
110
|
+
overThreshold &&
|
|
111
|
+
now >= runtime.debounceUntil) {
|
|
108
112
|
// COMPACT-DEDUP FIX: skip the manual durable-trim trigger when pi's
|
|
109
113
|
// NATIVE auto-compaction just fired (or is in-flight). pi emits
|
|
110
114
|
// agent_end BEFORE its own _checkCompaction (per its docstring:
|
|
@@ -150,7 +154,8 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
150
154
|
if (runtime.rt.sessionId !== liveSid)
|
|
151
155
|
return; // session reset
|
|
152
156
|
const since2 = now - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
153
|
-
if (runtime.rt.lastNativeCompactAt !== stamp &&
|
|
157
|
+
if (runtime.rt.lastNativeCompactAt !== stamp &&
|
|
158
|
+
since2 < cooldownMs)
|
|
154
159
|
return;
|
|
155
160
|
if (piCompactWouldNoop(ctx))
|
|
156
161
|
return;
|
|
@@ -179,7 +184,9 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
179
184
|
runtime.resumeNudgeUntil = now + 30_000;
|
|
180
185
|
if (runtime.rt.lengthStopPending) {
|
|
181
186
|
runtime.rt.lengthStopPending = false; // one-shot: never re-fire for same stop
|
|
182
|
-
runtime.dashboard.event("length_stop_continue", {
|
|
187
|
+
runtime.dashboard.event("length_stop_continue", {
|
|
188
|
+
turnIndex: runtime.currentTurn,
|
|
189
|
+
});
|
|
183
190
|
runtime.logger.info("length_stop_continue", {
|
|
184
191
|
sessionId: runtime.rt.sessionId,
|
|
185
192
|
didDurableTrim,
|
|
@@ -223,11 +230,12 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
223
230
|
// the turn layer is queryable + forkable. Best-effort + non-fatal: a write
|
|
224
231
|
// failure never breaks the agent loop.
|
|
225
232
|
try {
|
|
226
|
-
const convId =
|
|
227
|
-
|
|
233
|
+
const convId = ensureConversationIdFor(config, runtime.rt.sessionId, runtime.currentStateDir);
|
|
234
|
+
recordTurnWrite(config, {
|
|
228
235
|
conversationId: convId,
|
|
229
236
|
sessionId: runtime.rt.sessionId,
|
|
230
237
|
turnIndex: event.turnIndex,
|
|
238
|
+
role: event.role ?? "assistant",
|
|
231
239
|
endedAt: Date.now(),
|
|
232
240
|
startedAt: undefined,
|
|
233
241
|
ctxTokens: runtime.lastCtxTokens ?? undefined,
|
|
@@ -319,8 +327,9 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
319
327
|
// gating backoff, session-global cap, poisoned-context detection.
|
|
320
328
|
try {
|
|
321
329
|
// (1) S28 owns length — skip the classifier entirely for it.
|
|
322
|
-
const sr = event.message
|
|
323
|
-
|
|
330
|
+
const sr = event.message
|
|
331
|
+
?.stopReason;
|
|
332
|
+
if (sr === "length") {
|
|
324
333
|
// S28 handles; nothing for S38 to do here.
|
|
325
334
|
}
|
|
326
335
|
else {
|
|
@@ -335,7 +344,7 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
335
344
|
// (a 'permanent' auth error repeating is still permanent).
|
|
336
345
|
let effectiveCategory = category;
|
|
337
346
|
const errSig = extractErrorSignature(event.message);
|
|
338
|
-
if (category ===
|
|
347
|
+
if (category === "transient" || category === "poisoned-context") {
|
|
339
348
|
if (errSig) {
|
|
340
349
|
if (runtime.rt.lastErrorText === errSig) {
|
|
341
350
|
runtime.rt.errorTextRepeatCount++;
|
|
@@ -344,9 +353,10 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
344
353
|
runtime.rt.lastErrorText = errSig;
|
|
345
354
|
runtime.rt.errorTextRepeatCount = 1;
|
|
346
355
|
}
|
|
347
|
-
if (runtime.rt.errorTextRepeatCount >=
|
|
348
|
-
|
|
349
|
-
|
|
356
|
+
if (runtime.rt.errorTextRepeatCount >=
|
|
357
|
+
config.poisonedContextRepeatThreshold &&
|
|
358
|
+
category === "transient") {
|
|
359
|
+
effectiveCategory = "poisoned-context";
|
|
350
360
|
}
|
|
351
361
|
}
|
|
352
362
|
}
|
|
@@ -363,7 +373,7 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
363
373
|
// R4: a successful assistant turn consumes any queued nudge.
|
|
364
374
|
runtime.rt.retryNudgePending = false;
|
|
365
375
|
}
|
|
366
|
-
else if (effectiveCategory ===
|
|
376
|
+
else if (effectiveCategory === "compaction-noop") {
|
|
367
377
|
// (4) pi race / manual compact catch — NOT retryable. The compaction
|
|
368
378
|
// already succeeded via pi's native path; retrying would race again
|
|
369
379
|
// (FAIL-2026071701). Log a diagnostic, reset the counter, and surface
|
|
@@ -371,16 +381,16 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
371
381
|
runtime.rt.errorRetryCount = 0;
|
|
372
382
|
runtime.rt.consecutiveErrors = 0; // S38.6: circuit-breaker reset
|
|
373
383
|
runtime.rt.retryNudgePending = false; // R4: terminal for this burst
|
|
374
|
-
runtime.dashboard.event(
|
|
384
|
+
runtime.dashboard.event("compaction_noop_diagnostic", {
|
|
375
385
|
turnIndex: event.turnIndex,
|
|
376
386
|
sessionId: runtime.rt.sessionId,
|
|
377
387
|
});
|
|
378
|
-
runtime.logger.info(
|
|
388
|
+
runtime.logger.info("compaction-noop-diagnostic", {
|
|
379
389
|
sessionId: runtime.rt.sessionId,
|
|
380
390
|
turnIndex: event.turnIndex,
|
|
381
391
|
});
|
|
382
392
|
}
|
|
383
|
-
else if (effectiveCategory ===
|
|
393
|
+
else if (effectiveCategory === "cancelled") {
|
|
384
394
|
// (4c) User ESC / Ctrl-C — stopReason === 'aborted'. NOT retryable:
|
|
385
395
|
// nudging would restart a task the user explicitly stopped.
|
|
386
396
|
// Reset both counters (a cancel is not an error for circuit-breaker
|
|
@@ -388,16 +398,16 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
388
398
|
runtime.rt.errorRetryCount = 0;
|
|
389
399
|
runtime.rt.consecutiveErrors = 0;
|
|
390
400
|
runtime.rt.retryNudgePending = false; // R4: terminal for this burst
|
|
391
|
-
runtime.dashboard.event(
|
|
401
|
+
runtime.dashboard.event("error_retry_cancelled", {
|
|
392
402
|
turnIndex: event.turnIndex,
|
|
393
403
|
sessionId: runtime.rt.sessionId,
|
|
394
404
|
});
|
|
395
|
-
runtime.logger.info(
|
|
405
|
+
runtime.logger.info("error-retry-cancelled", {
|
|
396
406
|
sessionId: runtime.rt.sessionId,
|
|
397
407
|
turnIndex: event.turnIndex,
|
|
398
408
|
});
|
|
399
409
|
}
|
|
400
|
-
else if (effectiveCategory ===
|
|
410
|
+
else if (effectiveCategory === "context-overflow") {
|
|
401
411
|
// (4b) context-window overflow 400 ("too long... even after compaction").
|
|
402
412
|
// NOT a blind retry: re-submitting the same oversized prompt would just
|
|
403
413
|
// re-400 and busy-loop. Reset the counters (this turn is terminal, not
|
|
@@ -411,11 +421,11 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
411
421
|
runtime.rt.errorRetryCount = 0;
|
|
412
422
|
runtime.rt.consecutiveErrors = 0;
|
|
413
423
|
runtime.rt.retryNudgePending = false; // R4: terminal for this burst
|
|
414
|
-
runtime.dashboard.event(
|
|
424
|
+
runtime.dashboard.event("context_overflow", {
|
|
415
425
|
turnIndex: event.turnIndex,
|
|
416
426
|
sessionId: runtime.rt.sessionId,
|
|
417
427
|
});
|
|
418
|
-
runtime.logger.warn(
|
|
428
|
+
runtime.logger.warn("context-overflow", {
|
|
419
429
|
sessionId: runtime.rt.sessionId,
|
|
420
430
|
turnIndex: event.turnIndex,
|
|
421
431
|
});
|
|
@@ -432,7 +442,8 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
432
442
|
if (runtime.rt.sessionId !== liveSid2)
|
|
433
443
|
return; // session reset
|
|
434
444
|
const since3 = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
435
|
-
if (runtime.rt.lastNativeCompactAt !== stamp2 &&
|
|
445
|
+
if (runtime.rt.lastNativeCompactAt !== stamp2 &&
|
|
446
|
+
since3 < cooldownMs2)
|
|
436
447
|
return;
|
|
437
448
|
if (piCompactWouldNoop(ctx))
|
|
438
449
|
return;
|
|
@@ -445,7 +456,7 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
445
456
|
}
|
|
446
457
|
}
|
|
447
458
|
}
|
|
448
|
-
else if (effectiveCategory ===
|
|
459
|
+
else if (effectiveCategory === "poisoned-context") {
|
|
449
460
|
// R3: poisoned context — a DETERMINISTIC request-rejection that
|
|
450
461
|
// retrying cannot fix (0-token 'error', generic "request failed",
|
|
451
462
|
// 400 non-overflow, or repeated identical error text). NO blind
|
|
@@ -459,15 +470,15 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
459
470
|
runtime.rt.errorRetryCount = 0; // poisoned is terminal for this burst
|
|
460
471
|
runtime.rt.consecutiveErrors++; // still counts toward the circuit breaker
|
|
461
472
|
runtime.rt.poisonedCount++; // R7: dashboard counter
|
|
462
|
-
const sig = errSig ||
|
|
473
|
+
const sig = errSig || "unknown";
|
|
463
474
|
// (a) dashboard + log
|
|
464
|
-
runtime.dashboard.event(
|
|
475
|
+
runtime.dashboard.event("poisoned_context", {
|
|
465
476
|
signature: sig,
|
|
466
477
|
repeatCount: runtime.rt.errorTextRepeatCount,
|
|
467
478
|
turnIndex: event.turnIndex,
|
|
468
479
|
sessionId: runtime.rt.sessionId,
|
|
469
480
|
});
|
|
470
|
-
runtime.logger.warn(
|
|
481
|
+
runtime.logger.warn("poisoned-context", {
|
|
471
482
|
sessionId: runtime.rt.sessionId,
|
|
472
483
|
turnIndex: event.turnIndex,
|
|
473
484
|
signature: sig,
|
|
@@ -477,7 +488,7 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
477
488
|
if (!runtime.rt.poisonedAdviseSent) {
|
|
478
489
|
runtime.rt.poisonedAdviseSent = true;
|
|
479
490
|
// PREVENT-PI-003: user-role sendUserMessage only.
|
|
480
|
-
await safeSendUserMessage(pi,
|
|
491
|
+
await safeSendUserMessage(pi, "[mega-compact] this session's context may be poisoned (the provider is rejecting every request). Run /clear or /new to start a fresh context.");
|
|
481
492
|
}
|
|
482
493
|
// (c) one guarded compact per error signature (attempt to remove
|
|
483
494
|
// the poisoned region). Race-guarded + deferred, mirroring the
|
|
@@ -497,7 +508,8 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
497
508
|
if (runtime.rt.sessionId !== liveSidP)
|
|
498
509
|
return; // session reset
|
|
499
510
|
const sinceP = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
500
|
-
if (runtime.rt.lastNativeCompactAt !== stampP &&
|
|
511
|
+
if (runtime.rt.lastNativeCompactAt !== stampP &&
|
|
512
|
+
sinceP < cooldownMsP)
|
|
501
513
|
return;
|
|
502
514
|
if (piCompactWouldNoop(ctx))
|
|
503
515
|
return;
|
|
@@ -515,29 +527,29 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
515
527
|
// S38.7: hard-stop switch — bypass ALL retry logic when set.
|
|
516
528
|
if (config.errorRetryHardStop) {
|
|
517
529
|
runtime.rt.errorRetryCount = 0;
|
|
518
|
-
runtime.dashboard.event(
|
|
530
|
+
runtime.dashboard.event("error_retry_disabled", {
|
|
519
531
|
category: effectiveCategory,
|
|
520
532
|
turnIndex: event.turnIndex,
|
|
521
|
-
reason:
|
|
533
|
+
reason: "hard-stop",
|
|
522
534
|
});
|
|
523
535
|
return; // early exit — no retry
|
|
524
536
|
}
|
|
525
537
|
// S38.6: circuit-breaker — stop retrying after too many consecutive errors.
|
|
526
538
|
runtime.rt.consecutiveErrors++;
|
|
527
539
|
if (runtime.rt.consecutiveErrors > config.maxConsecutiveErrors) {
|
|
528
|
-
runtime.dashboard.event(
|
|
540
|
+
runtime.dashboard.event("error_retry_circuit_open", {
|
|
529
541
|
consecutive: runtime.rt.consecutiveErrors,
|
|
530
542
|
max: config.maxConsecutiveErrors,
|
|
531
543
|
turnIndex: event.turnIndex,
|
|
532
544
|
});
|
|
533
|
-
runtime.logger.warn(
|
|
545
|
+
runtime.logger.warn("error-retry-circuit-open", {
|
|
534
546
|
sessionId: runtime.rt.sessionId,
|
|
535
547
|
consecutive: runtime.rt.consecutiveErrors,
|
|
536
548
|
max: config.maxConsecutiveErrors,
|
|
537
549
|
});
|
|
538
550
|
return; // early exit — circuit breaker tripped
|
|
539
551
|
}
|
|
540
|
-
const max = effectiveCategory ===
|
|
552
|
+
const max = effectiveCategory === "transient"
|
|
541
553
|
? config.autoRetryTransientMax
|
|
542
554
|
: config.autoRetryPermanentMax;
|
|
543
555
|
// max === 0 disables the category entirely (revert to S28-only).
|
|
@@ -548,13 +560,13 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
548
560
|
runtime.rt.errorRetryCount++;
|
|
549
561
|
if (runtime.rt.errorRetryCount > max) {
|
|
550
562
|
// Exhausted — surface the error, reset for the next burst.
|
|
551
|
-
runtime.dashboard.event(
|
|
563
|
+
runtime.dashboard.event("error_retry_exhausted", {
|
|
552
564
|
category: effectiveCategory,
|
|
553
565
|
count: runtime.rt.errorRetryCount,
|
|
554
566
|
max,
|
|
555
567
|
turnIndex: event.turnIndex,
|
|
556
568
|
});
|
|
557
|
-
runtime.logger.info(
|
|
569
|
+
runtime.logger.info("error-retry-exhausted", {
|
|
558
570
|
sessionId: runtime.rt.sessionId,
|
|
559
571
|
category: effectiveCategory,
|
|
560
572
|
count: runtime.rt.errorRetryCount,
|
|
@@ -570,13 +582,13 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
570
582
|
// per-burst + circuit-breaker only).
|
|
571
583
|
if (config.errorRetrySessionMax > 0 &&
|
|
572
584
|
runtime.rt.errorRetrySessionCount >= config.errorRetrySessionMax) {
|
|
573
|
-
runtime.dashboard.event(
|
|
585
|
+
runtime.dashboard.event("error_retry_session_exhausted", {
|
|
574
586
|
count: runtime.rt.errorRetrySessionCount,
|
|
575
587
|
max: config.errorRetrySessionMax,
|
|
576
588
|
category: effectiveCategory,
|
|
577
589
|
turnIndex: event.turnIndex,
|
|
578
590
|
});
|
|
579
|
-
runtime.logger.warn(
|
|
591
|
+
runtime.logger.warn("error-retry-session-exhausted", {
|
|
580
592
|
sessionId: runtime.rt.sessionId,
|
|
581
593
|
count: runtime.rt.errorRetrySessionCount,
|
|
582
594
|
max: config.errorRetrySessionMax,
|
|
@@ -595,7 +607,7 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
595
607
|
// (this IS an error turn), so the per-burst max + circuit
|
|
596
608
|
// breaker still bound the burst.
|
|
597
609
|
if (runtime.rt.retryNudgePending) {
|
|
598
|
-
runtime.dashboard.event(
|
|
610
|
+
runtime.dashboard.event("error_retry_dedup_skip", {
|
|
599
611
|
category: effectiveCategory,
|
|
600
612
|
count: runtime.rt.errorRetryCount,
|
|
601
613
|
max,
|
|
@@ -610,7 +622,7 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
610
622
|
// doesn't slam N nudges in <1s.
|
|
611
623
|
const now = Date.now();
|
|
612
624
|
if (now < runtime.rt.errorRetryUntil) {
|
|
613
|
-
runtime.dashboard.event(
|
|
625
|
+
runtime.dashboard.event("error_retry_backoff_skip", {
|
|
614
626
|
category: effectiveCategory,
|
|
615
627
|
count: runtime.rt.errorRetryCount,
|
|
616
628
|
max,
|
|
@@ -624,22 +636,23 @@ export function registerAgentHandlers(pi, runtime, config) {
|
|
|
624
636
|
runtime.rt.retryNudgePending = true;
|
|
625
637
|
runtime.rt.lastErrorRetryAt = now;
|
|
626
638
|
runtime.rt.errorRetryUntil =
|
|
627
|
-
now +
|
|
639
|
+
now +
|
|
640
|
+
errorRetryBackoffMs(runtime.rt.errorRetryCount, config.errorRetryBackoffMs);
|
|
628
641
|
runtime.rt.errorRetrySessionCount++;
|
|
629
|
-
runtime.dashboard.event(
|
|
642
|
+
runtime.dashboard.event("error_retry", {
|
|
630
643
|
category: effectiveCategory,
|
|
631
644
|
count: runtime.rt.errorRetryCount,
|
|
632
645
|
max,
|
|
633
646
|
turnIndex: event.turnIndex,
|
|
634
647
|
});
|
|
635
|
-
runtime.logger.info(
|
|
648
|
+
runtime.logger.info("error-retry", {
|
|
636
649
|
sessionId: runtime.rt.sessionId,
|
|
637
650
|
category: effectiveCategory,
|
|
638
651
|
count: runtime.rt.errorRetryCount,
|
|
639
652
|
max,
|
|
640
653
|
});
|
|
641
654
|
// PREVENT-PI-003: user-role sendUserMessage only (queued + catch-guarded).
|
|
642
|
-
await safeSendUserMessage(pi,
|
|
655
|
+
await safeSendUserMessage(pi, "[mega-compact] the last turn ended with an error; please retry.");
|
|
643
656
|
}
|
|
644
657
|
}
|
|
645
658
|
}
|
|
@@ -3,6 +3,10 @@ import { epochIdFor } from "../../src/mirror/epoch.js";
|
|
|
3
3
|
import { autoCompactCheck } from "../../src/compact.js";
|
|
4
4
|
import { estimateSessionTokens } from "../../src/tokens.js";
|
|
5
5
|
import { runCompact, piCompactWouldNoop } from "../mega-pipeline.js";
|
|
6
|
+
import { stampTurnsEpochFor } from "../mega-turn-store.js";
|
|
7
|
+
import { TurnsConfig } from "../../src/config/turns.js";
|
|
8
|
+
import { openTurnStore } from "../../src/store/turns/connection.js";
|
|
9
|
+
import { buildTopicModel, createTopicStore, bumpWikiCompactCounter, } from "../../src/topics/index.js";
|
|
6
10
|
import { computeLiveTrimCut, liveTrimSummaryMessage } from "../mega-trim.js";
|
|
7
11
|
import { pressureFromPct, pressureRatio, } from "../mega-config.js";
|
|
8
12
|
import { computeContentDigest } from "../../src/dedup/digest.js";
|
|
@@ -44,7 +48,7 @@ function canonicalize(value) {
|
|
|
44
48
|
* whitespace/case-variant content. F5 fix: recursive canonicalize replaces the
|
|
45
49
|
* broken shallow-sorted JSON.stringify replacer.
|
|
46
50
|
*/
|
|
47
|
-
function toRawTranscriptRow(msg, sessionId, epochId) {
|
|
51
|
+
function toRawTranscriptRow(msg, sessionId, epochId, currentTurn) {
|
|
48
52
|
// Narrow to Message union (has content + timestamp).
|
|
49
53
|
const m = msg;
|
|
50
54
|
const content = m.content;
|
|
@@ -69,6 +73,9 @@ function toRawTranscriptRow(msg, sessionId, epochId) {
|
|
|
69
73
|
toolName: m.toolName ?? null,
|
|
70
74
|
messageTimestamp: m.timestamp ?? null,
|
|
71
75
|
checkpointEpoch: epochId,
|
|
76
|
+
// S50: label the row with the turn that produced it (per-turn dedup /
|
|
77
|
+
// compression-by-turn metrics). Null when the writer omits it (back-compat).
|
|
78
|
+
turnIndex: currentTurn ?? null,
|
|
72
79
|
};
|
|
73
80
|
}
|
|
74
81
|
/** Register the context event handler (live-trim auto-trigger). */
|
|
@@ -122,7 +129,7 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
122
129
|
const db = openStore(runtime.currentStateDir);
|
|
123
130
|
const epochId = epochIdFor(runtime.rt.sessionId);
|
|
124
131
|
for (const msg of messages) {
|
|
125
|
-
const raw = toRawTranscriptRow(msg, runtime.rt.sessionId, epochId);
|
|
132
|
+
const raw = toRawTranscriptRow(msg, runtime.rt.sessionId, epochId, runtime.currentTurn);
|
|
126
133
|
if (raw)
|
|
127
134
|
appendRawTranscript(db, raw);
|
|
128
135
|
}
|
|
@@ -233,6 +240,45 @@ export function registerContextHandler(pi, runtime, config) {
|
|
|
233
240
|
createdAt: Date.now(),
|
|
234
241
|
};
|
|
235
242
|
writeCheckpointEpoch(db, epoch);
|
|
243
|
+
// S50B: link this session's turns to the epoch that just compacted
|
|
244
|
+
// them (compression-by-conversation-epoch metrics). Isolated-store
|
|
245
|
+
// only; best-effort + non-fatal.
|
|
246
|
+
try {
|
|
247
|
+
stampTurnsEpochFor(config, runtime.rt.sessionId, epoch.epochId, runtime.currentStateDir);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
/* non-fatal: epoch stamping never breaks compaction */
|
|
251
|
+
}
|
|
252
|
+
// S51B: auto-categorizing wiki rebuild — every Nth compaction, derived
|
|
253
|
+
// from real context_chunks embeddings. Isolated-store only, gated on
|
|
254
|
+
// AUTO_WIKI_ENABLED; best-effort + non-fatal (never breaks compaction).
|
|
255
|
+
try {
|
|
256
|
+
if (config.autoWikiEnabled && config.turnsDbEnabled) {
|
|
257
|
+
const every = Math.max(1, TurnsConfig.WIKI_REBUILD_EVERY_N_COMPACTS);
|
|
258
|
+
const tdb = openTurnStore(runtime.currentStateDir);
|
|
259
|
+
const n = bumpWikiCompactCounter(tdb);
|
|
260
|
+
if (n % every === 0) {
|
|
261
|
+
const model = buildTopicModel(db, {
|
|
262
|
+
kRange: [TurnsConfig.WIKI_K_MIN, TurnsConfig.WIKI_K_MAX],
|
|
263
|
+
labelTopTerms: TurnsConfig.WIKI_LABEL_TOP_TERMS,
|
|
264
|
+
restarts: 5,
|
|
265
|
+
seed: 0x9e3779b9,
|
|
266
|
+
});
|
|
267
|
+
createTopicStore(runtime.currentStateDir).replaceTopicModel(model);
|
|
268
|
+
runtime.logger.info("wiki_rebuild", {
|
|
269
|
+
clusterCount: model.k,
|
|
270
|
+
totalChunks: model.totalChunks,
|
|
271
|
+
method: "kmeans+tfidf",
|
|
272
|
+
criterion: model.criterion,
|
|
273
|
+
silhouetteScore: model.silhouetteScore,
|
|
274
|
+
uncalibrated: false,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch (wikiErr) {
|
|
280
|
+
runtime.logger.warn("wiki_rebuild_failed", { error: String(wikiErr) });
|
|
281
|
+
}
|
|
236
282
|
// S27 Task 6: Fire-and-forget dedup pipeline.
|
|
237
283
|
// Deduplicates raw_transcript rows for the compacted range.
|
|
238
284
|
try {
|
|
@@ -4,6 +4,8 @@ import { recentUserQuery, WIDGET_KEY, } from "../mega-runtime.js";
|
|
|
4
4
|
import { doRecall, doRecallAsync, } from "../mega-pipeline.js";
|
|
5
5
|
import { recallMemoriesAndInline } from "../../src/recall.js";
|
|
6
6
|
import { vectorStats } from "../../src/vectorStore.js";
|
|
7
|
+
import { openTurnStore } from "../../src/store/turns/connection.js";
|
|
8
|
+
import { openIntentQueue } from "../../src/intent.js";
|
|
7
9
|
/** Register session lifecycle event handlers. */
|
|
8
10
|
export function registerSessionHandlers(pi, runtime, config) {
|
|
9
11
|
// Capture model/provider whenever it changes (drives real cost estimation).
|
|
@@ -125,6 +127,28 @@ export function registerSessionHandlers(pi, runtime, config) {
|
|
|
125
127
|
// ---- Auto-inline injection point: prepend staged recall to systemPrompt ----
|
|
126
128
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
127
129
|
runtime.captureModel(ctx); // most reliable point ctx.model is populated
|
|
130
|
+
// S52A: consume any pending rewind intents the dashboard queued. The host
|
|
131
|
+
// polls the intent queue here (the only safe lifecycle point) and marks each
|
|
132
|
+
// consumed. Best-effort + non-fatal — the store never calls back into the
|
|
133
|
+
// host (ledger protocol). A real rewind (reloading pi's conversation to turn
|
|
134
|
+
// N) is host-specific; this consumes the intent so the round-trip completes.
|
|
135
|
+
try {
|
|
136
|
+
if (config.turnsDbEnabled) {
|
|
137
|
+
const tdb = openTurnStore(runtime.currentStateDir);
|
|
138
|
+
const q = openIntentQueue(tdb);
|
|
139
|
+
const pending = q.pendingIntents();
|
|
140
|
+
for (const intent of pending) {
|
|
141
|
+
q.consumeIntent(intent.id);
|
|
142
|
+
runtime.dashboard.event("rewind_intent_consumed", {
|
|
143
|
+
conversationId: intent.conversationId,
|
|
144
|
+
targetTurnIndex: intent.targetTurnIndex,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
/* non-fatal: intent polling never breaks the agent loop */
|
|
151
|
+
}
|
|
128
152
|
const cpBlock = runtime.pendingRecallBlock;
|
|
129
153
|
const memBlock = runtime.pendingMemoryRecallBlock;
|
|
130
154
|
if (!cpBlock && !memBlock)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mega-metrics-cmds.ts — S50C per-turn memory-quality /commands.
|
|
3
|
+
*
|
|
4
|
+
* Registers /mega-metrics and /mega-fork, backed by the pi-agnostic
|
|
5
|
+
* src/metrics/turns.ts rollups and src/fork.ts primitive over the isolated
|
|
6
|
+
* S49 turns.db. Read-only (PREVENT-PI-001/002 — these never mutate memory,
|
|
7
|
+
* drop ranges, or compaction); local SQLite only (PREVENT-PI-004);
|
|
8
|
+
* parameterized queries (PREVENT-002). Both require turnsDbEnabled (the
|
|
9
|
+
* isolated store); they no-op with guidance on the legacy main-db path.
|
|
10
|
+
*/
|
|
11
|
+
import { turnStoreFor, mainDbFor } from "./mega-turn-store.js";
|
|
12
|
+
import { turnMetrics, conversationMetrics } from "../src/metrics/index.js";
|
|
13
|
+
import { forkFromConversation, ForkError } from "../src/fork.js";
|
|
14
|
+
import { ensureConversationIdFor } from "./mega-turn-store.js";
|
|
15
|
+
function pct(r) {
|
|
16
|
+
return `${(r * 100).toFixed(1)}%`;
|
|
17
|
+
}
|
|
18
|
+
/** Register the /mega-metrics + /mega-fork commands. */
|
|
19
|
+
export function registerMetricsCommands(pi, runtime, config) {
|
|
20
|
+
pi.registerCommand("mega-metrics", {
|
|
21
|
+
description: "Show per-turn + per-conversation memory-quality metrics (recall reuse, dedup ratio, compression ratio). Usage: /mega-metrics [conversationId]",
|
|
22
|
+
handler: async (args, ctx) => {
|
|
23
|
+
try {
|
|
24
|
+
runtime.bindRepo(ctx.cwd);
|
|
25
|
+
const stateDir = runtime.currentStateDir;
|
|
26
|
+
const store = turnStoreFor(config, stateDir);
|
|
27
|
+
if (!store) {
|
|
28
|
+
ctx.ui.notify("[mega-compact] /mega-metrics requires the isolated turns.db (set turnsDbEnabled on).");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const conv = args.trim() ||
|
|
32
|
+
ensureConversationIdFor(config, runtime.rt.sessionId, stateDir);
|
|
33
|
+
const mainDb = mainDbFor(stateDir);
|
|
34
|
+
const perTurn = turnMetrics(store, mainDb, conv);
|
|
35
|
+
if (perTurn.length === 0) {
|
|
36
|
+
ctx.ui.notify(`[mega-compact] no turns recorded for conversation ${conv}.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const agg = conversationMetrics(store, mainDb, conv);
|
|
40
|
+
ctx.ui.notify(`[mega-compact] conversation ${conv} — ${agg.turnCount} turns, ${agg.epochCount} epoch(s), total recall ${agg.totalRecall}, total raw msgs ${agg.totalRawMessages}`);
|
|
41
|
+
ctx.ui.notify(` avg dedup unique ratio ${pct(agg.avgDedupUniqueRatio)} · avg compression ratio ${pct(agg.avgCompressionRatio)}`);
|
|
42
|
+
for (const t of perTurn) {
|
|
43
|
+
ctx.ui.notify(` turn ${t.turnIndex} (epoch ${t.epochId ?? "—"}): recall ${t.recallCount}, raw ${t.rawMessageCount}, dedup ${pct(t.dedupUniqueRatio)}, compress ${pct(t.compressionRatio)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
ctx.ui.notify(`[mega-compact] /mega-metrics failed: ${String(e)}`);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
pi.registerCommand("mega-fork", {
|
|
52
|
+
description: "Fork the current conversation at turn N into a new child conversation, rehydrating that turn's injected checkpoints (recall-to-point, not window replay). Usage: /mega-fork <turnIndex> [conversationId]",
|
|
53
|
+
handler: async (args, ctx) => {
|
|
54
|
+
try {
|
|
55
|
+
runtime.bindRepo(ctx.cwd);
|
|
56
|
+
const stateDir = runtime.currentStateDir;
|
|
57
|
+
const store = turnStoreFor(config, stateDir);
|
|
58
|
+
if (!store) {
|
|
59
|
+
ctx.ui.notify("[mega-compact] /mega-fork requires the isolated turns.db (set turnsDbEnabled on).");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
63
|
+
const turnIndex = Number.parseInt(parts[0] ?? "", 10);
|
|
64
|
+
if (!Number.isFinite(turnIndex)) {
|
|
65
|
+
ctx.ui.notify("[mega-compact] /mega-fork needs a turn index, e.g. /mega-fork 3");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const conv = parts[1] ||
|
|
69
|
+
ensureConversationIdFor(config, runtime.rt.sessionId, stateDir);
|
|
70
|
+
const out = forkFromConversation(store, conv, turnIndex);
|
|
71
|
+
runtime.logger.info("fork", {
|
|
72
|
+
parent: conv,
|
|
73
|
+
child: out.childConversationId,
|
|
74
|
+
turnIndex,
|
|
75
|
+
checkpoints: out.checkpointIds,
|
|
76
|
+
});
|
|
77
|
+
ctx.ui.notify(`[mega-compact] forked ${conv} @ turn ${turnIndex} → ${out.childConversationId}\n` +
|
|
78
|
+
` replay set (${out.checkpointIds.length} checkpoint(s)): ${out.checkpointIds.join(", ")}`);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (e instanceof ForkError) {
|
|
82
|
+
ctx.ui.notify(`[mega-compact] /mega-fork: ${e.message}`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
ctx.ui.notify(`[mega-compact] /mega-fork failed: ${String(e)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|