pi-mega-compact 0.4.19 → 0.4.21

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.
@@ -90,10 +90,7 @@ export class MegaRuntime {
90
90
  // on model_select + session_start; persisted to SQL so cost + the dashboard
91
91
  // can read it without a live ctx.
92
92
  currentModel: ModelSnapshot | undefined;
93
- // Live "what it's doing right now" line for the toolbar. Set on each
94
- // compaction; shown in teal while recent, then kept as the last-seen action so
95
- // the widget is never blank. Cleared on session reset.
96
- currentActivity: string | undefined;
93
+ // Live "what it's doing right now" timestamp, used for the fresh-window.
97
94
  lastActivityAt = 0;
98
95
  // Live per-tier dedup trace (Phase 1): e.g. "L0 ✓ → L1 ✓ → L2 0.91 → stored".
99
96
  // Built from the store's sync onTier callback during a compaction so the user
@@ -269,25 +266,25 @@ export class MegaRuntime {
269
266
  const bar = "▓".repeat(filled) + "░".repeat(10 - filled);
270
267
  lines.push(` ${C.green}saved ${fmt(this.rt.tokensSaved)} ${bar}${C.reset} ${pct}% of ${fmt(goal)}`);
271
268
  }
272
- // Live "now processing" line teal while fresh (≤4s), then the last-seen
273
- // action keeps the widget lively. Cleared on session reset.
269
+ // Live "now processing" line + why + recent deduped/compacted events,
270
+ // collapsed to ONE rotating line (fresh only). The ticker ring buffer
271
+ // (≤5 most-recent events) is cycled one-per-repaint so the line scrolls
272
+ // through recent files in real time while activity fires. We rotate on a
273
+ // 250ms step (same cadence as the pulse), using an event counter as the
274
+ // deterministic phase so consecutive repaints advance the visible entry.
274
275
  const fresh = Date.now() - this.lastActivityAt < 4000;
275
276
  if (this.tierTrace && fresh) {
276
277
  lines.push(` ${pulse}${this.tierTrace}`);
277
- } else if (this.currentActivity) {
278
- lines.push(` ${fresh ? C.teal : C.dim}${this.currentActivity}${C.reset}`);
278
+ } else if (this.ticker.length > 0) {
279
+ const step = Math.floor(Date.now() / 250);
280
+ const idx = this.ticker.length - 1 - (step % this.ticker.length);
281
+ const head = this.ticker[idx].text;
282
+ const why = this.lastWhy ? ` ${C.gray}· ${this.lastWhy}${C.reset}` : "";
283
+ const more = this.ticker.length > 1 ? ` ${C.dim}(+${this.ticker.length - 1} more)${C.reset}` : "";
284
+ lines.push(` ${fresh ? C.teal : C.dim}${head}${why}${more}${C.reset}`);
279
285
  } else if (this.pulsing) {
280
286
  lines.push(` ${pulse}${C.teal}compacting…${C.reset}`);
281
287
  }
282
- // Phase 3 — explain-why line (fresh only).
283
- if (this.lastWhy && fresh) lines.push(` ${C.gray}${this.lastWhy}${C.reset}`);
284
- // Phase 3 — recall/activity ticker (most-recent first), fresh only.
285
- if (fresh) {
286
- for (let i = this.ticker.length - 1; i >= 0; i--) {
287
- if (lines.length >= 9) break; // leave room for the hint line (MAX 10)
288
- lines.push(` ${i === this.ticker.length - 1 ? "" : C.dim}${this.ticker[i].text}${C.reset}`);
289
- }
290
- }
291
288
  // Plain-language hint so first-time users understand the widget. Always
292
289
  // last, dimmed. "/mega-help explains these terms."
293
290
  if (lines.length < 10) {
@@ -318,7 +315,6 @@ export class MegaRuntime {
318
315
  this.statusKey = undefined;
319
316
  this.activeAgents = 0;
320
317
  this.currentTurn = 0;
321
- this.currentActivity = undefined;
322
318
  this.lastActivityAt = 0;
323
319
  this.tierTrace = undefined;
324
320
  this.ticker.length = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.4.19",
3
+ "version": "0.4.21",
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",
@@ -415,6 +415,20 @@ function initSchema(db: Database.Database): void {
415
415
  ts INTEGER
416
416
  );
417
417
 
418
+ -- Durable "save to memory" store (taken over from memory extensions).
419
+ -- One row per saved memory; scoped by repo so memory travels with the
420
+ -- clone. All params are parameterized (PREVENT-002).
421
+ CREATE TABLE IF NOT EXISTS memories (
422
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
423
+ repo TEXT,
424
+ kind TEXT DEFAULT 'note', -- note | fact | decision | preference
425
+ content TEXT NOT NULL,
426
+ tags TEXT, -- JSON array of strings
427
+ created_at INTEGER,
428
+ last_recalled_at INTEGER
429
+ );
430
+ CREATE INDEX IF NOT EXISTS idx_memories_repo ON memories(repo);
431
+
418
432
  -- FTS5 trigram virtual table (Sprint 9+ pg_trgm-equivalent verification).
419
433
  CREATE VIRTUAL TABLE IF NOT EXISTS context_chunks_trgm USING fts5(
420
434
  id UNINDEXED,
@@ -582,6 +596,76 @@ export function addLesson(
582
596
  ).run(normalizeSessionId(sessionId), repo ?? null, lesson, now);
583
597
  }
584
598
 
599
+ // --- Durable memory (save-to-memory takeover) ---------------------------------
600
+ // One SQLite store for user-saved memories, scoped by repo. Mirrors the
601
+ // lessons/sessions pattern: all state lives in SQLite from day one.
602
+
603
+ export interface MemoryRecord {
604
+ id: number;
605
+ repo: string | null;
606
+ kind: string;
607
+ content: string;
608
+ tags: string[];
609
+ createdAt: number;
610
+ lastRecalledAt: number | null;
611
+ }
612
+
613
+ /** Save a memory to the current repo's store. Returns the new row id. */
614
+ export function addMemory(
615
+ memory: { kind?: string; content: string; tags?: string[] },
616
+ repo: string | null,
617
+ stateDir: string = getStateDir(),
618
+ ): number {
619
+ const db = openStore(stateDir);
620
+ const now = Math.floor(Date.now() / 1000);
621
+ const res = db
622
+ .prepare(
623
+ `INSERT INTO memories(repo, kind, content, tags, created_at, last_recalled_at)
624
+ VALUES(?, ?, ?, ?, ?, NULL)`,
625
+ )
626
+ .run(repo ?? null, memory.kind ?? "note", memory.content, JSON.stringify(memory.tags ?? []), now);
627
+ return Number(res.lastInsertRowid);
628
+ }
629
+
630
+ /** List recent memories for a repo (or all repos when repo is null). */
631
+ export function listMemories(repo: string | null, limit = 50, stateDir: string = getStateDir()): MemoryRecord[] {
632
+ const db = openStore(stateDir);
633
+ const rows = repo
634
+ ? db.prepare("SELECT * FROM memories WHERE repo = ? ORDER BY created_at DESC LIMIT ?").all(repo, limit)
635
+ : db.prepare("SELECT * FROM memories ORDER BY created_at DESC LIMIT ?").all(limit);
636
+ return (rows as any[]).map(mapMemoryRow);
637
+ }
638
+
639
+ /** Substring search across content + tags. */
640
+ export function searchMemories(query: string, repo: string | null = null, limit = 50, stateDir: string = getStateDir()): MemoryRecord[] {
641
+ const db = openStore(stateDir);
642
+ const like = `%${query}%`;
643
+ const rows = repo
644
+ ? db.prepare("SELECT * FROM memories WHERE repo = ? AND (content LIKE ? OR tags LIKE ?) ORDER BY created_at DESC LIMIT ?").all(repo, like, like, limit)
645
+ : db.prepare("SELECT * FROM memories WHERE content LIKE ? OR tags LIKE ? ORDER BY created_at DESC LIMIT ?").all(like, like, limit);
646
+ return (rows as any[]).map(mapMemoryRow);
647
+ }
648
+
649
+ /** Mark a memory as recalled (updates last_recalled_at). Returns true if found. */
650
+ export function recallMemory(id: number, stateDir: string = getStateDir()): boolean {
651
+ const db = openStore(stateDir);
652
+ const now = Math.floor(Date.now() / 1000);
653
+ const res = db.prepare("UPDATE memories SET last_recalled_at = ? WHERE id = ?").run(now, id);
654
+ return res.changes > 0;
655
+ }
656
+
657
+ function mapMemoryRow(row: any): MemoryRecord {
658
+ return {
659
+ id: row.id,
660
+ repo: row.repo ?? null,
661
+ kind: row.kind ?? "note",
662
+ content: row.content ?? "",
663
+ tags: row.tags ? JSON.parse(row.tags) : [],
664
+ createdAt: row.created_at ?? 0,
665
+ lastRecalledAt: row.last_recalled_at ?? null,
666
+ };
667
+ }
668
+
585
669
  /** Map a DB row to the public StoredCheckpoint shape. */
586
670
  function rowToCheckpoint(row: any): StoredCheckpoint {
587
671
  return {