pi-mega-compact 0.7.2 → 0.7.4
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/mega-config.js +1 -0
- package/dist/extensions/mega-events.js +117 -4
- package/dist/extensions/mega-events.test.js +47 -0
- package/dist/extensions/mega-runtime.js +225 -97
- package/dist/src/mirror/dedup.js +44 -0
- package/dist/src/mirror/epoch.js +36 -0
- package/dist/src/mirror/mirror.test.js +185 -0
- package/dist/src/recall.js +37 -0
- package/dist/src/store/sqlite.dbmirror.test.js +175 -0
- package/dist/src/store/sqlite.js +248 -1
- package/extensions/mega-config.ts +8 -0
- package/extensions/mega-events.test.ts +55 -0
- package/extensions/mega-events.ts +126 -4
- package/extensions/mega-runtime.ts +941 -737
- package/package.json +1 -1
- package/src/mirror/dedup.ts +57 -0
- package/src/mirror/epoch.ts +37 -0
- package/src/mirror/mirror.test.ts +240 -0
- package/src/recall.ts +38 -0
- package/src/store/sqlite.dbmirror.test.ts +219 -0
- package/src/store/sqlite.ts +378 -1
|
@@ -135,6 +135,7 @@ export function loadConfig() {
|
|
|
135
135
|
dedupSim: Number(process.env.MEGACOMPACT_DEDUP_SIM ?? "0.9"),
|
|
136
136
|
raptorEnabled: envBool("MEGACOMPACT_RAPTOR_ENABLED", true),
|
|
137
137
|
legacyDurableTrim: envBool("MEGACOMPACT_LEGACY_DURABLE_TRIM", false),
|
|
138
|
+
dbMirror: envBool("MEGACOMPACT_DB_MIRROR", false),
|
|
138
139
|
crossRepoEnabled: envBool("MEGACOMPACT_CROSSREPO_ENABLED", true),
|
|
139
140
|
crossRepoCosine: Number(process.env.MEGACOMPACT_CROSSREPO_COSINE ?? "0.90"),
|
|
140
141
|
memoryAutoReview: envBool("MEGACOMPACT_MEMORY_AUTO_REVIEW", true),
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* sync and delegates the heavy lifting to the pipeline + command modules.
|
|
8
8
|
*/
|
|
9
9
|
import { normalizeSessionId } from "../src/store.js";
|
|
10
|
+
import { openStore, appendRawTranscript, writeCheckpointEpoch } from "../src/store/sqlite.js";
|
|
11
|
+
import { epochIdFor } from "../src/mirror/epoch.js";
|
|
10
12
|
import { autoCompactCheck } from "../src/compact.js";
|
|
11
13
|
import { estimateSessionTokens, estimateBlockTokens } from "../src/tokens.js";
|
|
12
14
|
import { recentUserQuery, WIDGET_KEY, } from "./mega-runtime.js";
|
|
@@ -15,6 +17,34 @@ import { recallMemoriesAndInline } from "../src/recall.js";
|
|
|
15
17
|
import { driveNativeCompaction, } from "./mega-compact-driver.js";
|
|
16
18
|
import { computeLiveTrimCut, liveTrimSummaryMessage } from "./mega-trim.js";
|
|
17
19
|
import { pressureFromPct, memoryReviewCadence, } from "./mega-config.js";
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
/**
|
|
22
|
+
* Convert a pi AgentMessage to a RawTranscriptRow for the DB mirror.
|
|
23
|
+
* content_bytes is canonical JSON (sorted keys) for deterministic hashing.
|
|
24
|
+
* Returns null if the message has no usable content.
|
|
25
|
+
*/
|
|
26
|
+
function toRawTranscriptRow(msg, sessionId, epochId) {
|
|
27
|
+
// Narrow to Message union (has content + timestamp).
|
|
28
|
+
const m = msg;
|
|
29
|
+
const content = m.content;
|
|
30
|
+
if (content == null || content === "")
|
|
31
|
+
return null;
|
|
32
|
+
// Canonical form: sort object keys for deterministic hashing.
|
|
33
|
+
const contentBytes = typeof content === "string"
|
|
34
|
+
? content
|
|
35
|
+
: JSON.stringify(content, Object.keys(content).sort());
|
|
36
|
+
const contentHash = createHash("sha256").update(contentBytes).digest("hex");
|
|
37
|
+
return {
|
|
38
|
+
contentHash,
|
|
39
|
+
sessionId,
|
|
40
|
+
seq: 0, // assigned by appendRawTranscript (COALESCE(MAX(seq),0)+1)
|
|
41
|
+
role: m.role ?? "unknown",
|
|
42
|
+
contentBytes,
|
|
43
|
+
toolName: m.toolName ?? null,
|
|
44
|
+
messageTimestamp: m.timestamp ?? null,
|
|
45
|
+
checkpointEpoch: epochId,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
18
48
|
/**
|
|
19
49
|
* DIAG accessor for the headless test harness: the most recently constructed
|
|
20
50
|
* MegaRuntime, so a test that loads the compiled extension via its default
|
|
@@ -223,7 +253,21 @@ export function registerEventHandlers(pi, runtime, config) {
|
|
|
223
253
|
// trim we ALWAYS nudge so the agent reliably restarts. Debounced 30s.
|
|
224
254
|
let didDurableTrim = false;
|
|
225
255
|
if (idle && overThreshold && now >= runtime.debounceUntil) {
|
|
226
|
-
|
|
256
|
+
// COMPACT-DEDUP FIX: skip the manual durable-trim trigger when pi's
|
|
257
|
+
// NATIVE auto-compaction just fired (or is in-flight). pi emits
|
|
258
|
+
// agent_end BEFORE its own _checkCompaction (per its docstring:
|
|
259
|
+
// "Called after agent_end and before prompt submission"), so a
|
|
260
|
+
// synchronous `piCompactWouldNoop` branch check misses a native
|
|
261
|
+
// compaction that hasn't appended its entry yet — calling
|
|
262
|
+
// ctx.compact() then races with pi and throws "Already compacted"
|
|
263
|
+
// to the user. The `lastCompactAt` cooldown (updated by the
|
|
264
|
+
// session_compact listener for EVERY compaction, native or
|
|
265
|
+
// extension-supplied) closes that race window.
|
|
266
|
+
const sinceCompact = now - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
267
|
+
if (sinceCompact < 10_000) {
|
|
268
|
+
runtime.diagAgentEndDurableSkipRecent++;
|
|
269
|
+
}
|
|
270
|
+
else if (!piCompactWouldNoop(ctx)) {
|
|
227
271
|
runtime.debounceUntil = now + 2000;
|
|
228
272
|
runtime.diagAgentEndDurable++;
|
|
229
273
|
runtime.logger.info("agent-end-durable-trigger", {
|
|
@@ -232,7 +276,7 @@ export function registerEventHandlers(pi, runtime, config) {
|
|
|
232
276
|
thresholdTokens: config.thresholdTokens,
|
|
233
277
|
queued,
|
|
234
278
|
});
|
|
235
|
-
ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; agent settled so no in-flight abort
|
|
279
|
+
ctx.compact({ customInstructions: undefined }); // guardrails-allow PREVENT-PI-004: local ctx.compact() — no network; agent settled so no in-flight abort. Race-guarded by lastCompactAt cooldown above (ctx.compact returns void → throw is surfaced by pi as compaction_end; the cooldown prevents the call entirely).
|
|
236
280
|
didDurableTrim = true;
|
|
237
281
|
}
|
|
238
282
|
}
|
|
@@ -309,6 +353,23 @@ export function registerEventHandlers(pi, runtime, config) {
|
|
|
309
353
|
const currentTokens = usage?.tokens ??
|
|
310
354
|
estimateSessionTokens(view) ??
|
|
311
355
|
Math.round((pct / 100) * (usage?.contextWindow ?? 0));
|
|
356
|
+
// S27 DB-mirror: append ALL incoming messages to raw_transcript.
|
|
357
|
+
// Runs BEFORE fast-gate so every message is captured, even if we
|
|
358
|
+
// don't compact this turn. Append is idempotent (content_hash PK).
|
|
359
|
+
if (config.dbMirror) {
|
|
360
|
+
try {
|
|
361
|
+
const db = openStore(runtime.currentStateDir);
|
|
362
|
+
const epochId = epochIdFor(runtime.rt.sessionId);
|
|
363
|
+
for (const msg of messages) {
|
|
364
|
+
const raw = toRawTranscriptRow(msg, runtime.rt.sessionId, epochId);
|
|
365
|
+
if (raw)
|
|
366
|
+
appendRawTranscript(db, raw);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
runtime.logger.warn("db-mirror-append-fail", { error: String(e) });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
312
373
|
// FAST GATE: token-based (tier% of the window), not a static amount.
|
|
313
374
|
if (currentTokens < runtime.effectiveThreshold) {
|
|
314
375
|
runtime.diagCtxFastGate++;
|
|
@@ -336,6 +397,37 @@ export function registerEventHandlers(pi, runtime, config) {
|
|
|
336
397
|
runtime.diagCtxRunSkipped++;
|
|
337
398
|
return;
|
|
338
399
|
}
|
|
400
|
+
// S27 DB-mirror: write checkpoint_epoch with deterministic nonce.
|
|
401
|
+
// This makes the cache key stable across identical compactions.
|
|
402
|
+
if (config.dbMirror) {
|
|
403
|
+
try {
|
|
404
|
+
const db = openStore(runtime.currentStateDir);
|
|
405
|
+
const cpId = ran.result.checkpointId ?? `epoch-${Date.now()}`;
|
|
406
|
+
const epoch = {
|
|
407
|
+
epochId: epochIdFor(cpId),
|
|
408
|
+
sessionId: runtime.rt.sessionId,
|
|
409
|
+
startedSeq: 0,
|
|
410
|
+
committedSeq: ran.result.compactedFrom,
|
|
411
|
+
checkpointId: cpId,
|
|
412
|
+
cutIndex: ran.result.compactedFrom,
|
|
413
|
+
summaryMessageText: ran.result.summary,
|
|
414
|
+
createdAt: Date.now(),
|
|
415
|
+
};
|
|
416
|
+
writeCheckpointEpoch(db, epoch);
|
|
417
|
+
// S27 Task 6: Fire-and-forget dedup pipeline.
|
|
418
|
+
// Deduplicates raw_transcript rows for the compacted range.
|
|
419
|
+
try {
|
|
420
|
+
const { dedupTranscript } = await import("../src/mirror/dedup.js");
|
|
421
|
+
dedupTranscript(db, runtime.rt.sessionId, 0, ran.result.compactedFrom);
|
|
422
|
+
}
|
|
423
|
+
catch (_dedupErr) {
|
|
424
|
+
// Fire-and-forget: dedup failure is non-fatal
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch (e) {
|
|
428
|
+
runtime.logger.warn("db-mirror-epoch-fail", { error: String(e) });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
339
431
|
// LEGACY path (rollback): v0.4.28 ctx.compact() + the no-op gate. The
|
|
340
432
|
// manual compact path aborts the in-flight turn — only used behind the flag.
|
|
341
433
|
// Read live from env (in addition to the load-time config) so the flag can be
|
|
@@ -345,9 +437,14 @@ export function registerEventHandlers(pi, runtime, config) {
|
|
|
345
437
|
process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM === "true" ||
|
|
346
438
|
process.env.MEGACOMPACT_LEGACY_DURABLE_TRIM === "1";
|
|
347
439
|
if (legacy) {
|
|
348
|
-
|
|
440
|
+
// COMPACT-DEDUP FIX: same race guard as the agent_end path. Skip when a
|
|
441
|
+
// NATIVE compaction just fired (avoids racing pi and surfacing a spurious
|
|
442
|
+
// "Already compacted" / "Nothing to compact" toast). Uses lastNativeCompactAt
|
|
443
|
+
// (NOT lastCompactAt, which runCompact also stamps for our own checkpoint).
|
|
444
|
+
const sinceCompact = Date.now() - (runtime.rt.lastNativeCompactAt ?? 0);
|
|
445
|
+
if (sinceCompact < 10_000 || piCompactWouldNoop(ctx))
|
|
349
446
|
return;
|
|
350
|
-
ctx.compact({ customInstructions: undefined });
|
|
447
|
+
ctx.compact({ customInstructions: undefined }); // race-guarded by lastNativeCompactAt cooldown (ctx.compact returns void → not catchable; the cooldown prevents the call)
|
|
351
448
|
return;
|
|
352
449
|
}
|
|
353
450
|
// S16 LIVE trim: collapse the compacted region to a summary + recent anchor.
|
|
@@ -482,6 +579,22 @@ export function registerEventHandlers(pi, runtime, config) {
|
|
|
482
579
|
// Absolute last resort: let pi run its own (may throw "Nothing to compact").
|
|
483
580
|
return {};
|
|
484
581
|
});
|
|
582
|
+
// COMPACT-DEDUP FIX: track EVERY compaction (native + extension-supplied)
|
|
583
|
+
// so the agent_end durable-trim guard can skip a redundant ctx.compact()
|
|
584
|
+
// when pi just compacted. Without this, agent_end fires ctx.compact()
|
|
585
|
+
// synchronously AFTER pi's native auto-compaction appended a compaction
|
|
586
|
+
// entry but BEFORE our branch read sees it on the next tick — racing
|
|
587
|
+
// into a user-facing "Already compacted" throw. `lastCompactAt` is the
|
|
588
|
+
// race-closing signal: any compaction (manual/threshold/overflow, ours
|
|
589
|
+
// or pi's own) stamps it, and the agent_end guard skips for 10s.
|
|
590
|
+
pi.on("session_compact", async (_event, _ctx) => {
|
|
591
|
+
runtime.rt.lastNativeCompactAt = Date.now();
|
|
592
|
+
runtime.rt.lastCompactAt = Date.now();
|
|
593
|
+
runtime.logger.info("session-compacted", {
|
|
594
|
+
sessionId: runtime.rt.sessionId,
|
|
595
|
+
at: runtime.rt.lastCompactAt,
|
|
596
|
+
});
|
|
597
|
+
});
|
|
485
598
|
/**
|
|
486
599
|
* Build a minimal fallback compaction so pi never runs its throwing compact().
|
|
487
600
|
*
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for mega-events extension — DB-mirror event wiring.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { openStore, listCheckpointEpochs, countRawTranscript } from "../src/store/sqlite.js";
|
|
10
|
+
function makeTmp() {
|
|
11
|
+
return mkdtempSync(join(tmpdir(), "mega-events-test-"));
|
|
12
|
+
}
|
|
13
|
+
describe("mega-events: DB-mirror integration", () => {
|
|
14
|
+
let dir;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
dir = makeTmp();
|
|
17
|
+
// openStore creates the tables as a side effect
|
|
18
|
+
openStore(dir);
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(dir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
it("openStore creates checkpoint_epochs and raw_transcript tables", () => {
|
|
24
|
+
const db = openStore(dir);
|
|
25
|
+
// Should not throw — tables exist
|
|
26
|
+
const epochs = listCheckpointEpochs(db);
|
|
27
|
+
assert.ok(Array.isArray(epochs));
|
|
28
|
+
const count = countRawTranscript(db);
|
|
29
|
+
assert.equal(count, 0);
|
|
30
|
+
});
|
|
31
|
+
it("DB-mirror flag defaults to false when env is unset", () => {
|
|
32
|
+
delete process.env.MEGACOMPACT_DB_MIRROR;
|
|
33
|
+
// Re-import to pick up env
|
|
34
|
+
// The extension checks env at load time, so just verify the env is absent
|
|
35
|
+
assert.equal(process.env.MEGACOMPACT_DB_MIRROR, undefined);
|
|
36
|
+
});
|
|
37
|
+
it("DB-mirror flag is enabled when env is '1'", () => {
|
|
38
|
+
process.env.MEGACOMPACT_DB_MIRROR = "1";
|
|
39
|
+
assert.equal(process.env.MEGACOMPACT_DB_MIRROR, "1");
|
|
40
|
+
delete process.env.MEGACOMPACT_DB_MIRROR;
|
|
41
|
+
});
|
|
42
|
+
it("DB-mirror flag is enabled when env is 'true'", () => {
|
|
43
|
+
process.env.MEGACOMPACT_DB_MIRROR = "true";
|
|
44
|
+
assert.equal(process.env.MEGACOMPACT_DB_MIRROR, "true");
|
|
45
|
+
delete process.env.MEGACOMPACT_DB_MIRROR;
|
|
46
|
+
});
|
|
47
|
+
});
|