@cortexkit/opencode-magic-context 0.3.0 → 0.3.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/dist/index.js CHANGED
@@ -15037,6 +15037,8 @@ function releaseLease(db, holderId) {
15037
15037
  function enqueueDream(db, projectIdentity, reason) {
15038
15038
  const now = Date.now();
15039
15039
  return db.transaction(() => {
15040
+ const staleThresholdMs = 10 * 60 * 1000;
15041
+ db.run("DELETE FROM dream_queue WHERE project_path = ? AND started_at IS NOT NULL AND started_at < ?", [projectIdentity, now - staleThresholdMs]);
15040
15042
  const existing = db.query("SELECT id FROM dream_queue WHERE project_path = ?").get(projectIdentity);
15041
15043
  if (existing) {
15042
15044
  return null;
@@ -15323,7 +15325,7 @@ async function runDream(args) {
15323
15325
  log(`[dreamer] lease released: ${holderId}`);
15324
15326
  }
15325
15327
  result.finishedAt = Date.now();
15326
- const hasSuccessfulTask = result.tasks.some((t) => !t.error);
15328
+ const hasSuccessfulTask = result.tasks.some((t) => !t.error && t.name !== "smart-notes");
15327
15329
  if (hasSuccessfulTask) {
15328
15330
  setDreamState(args.db, `last_dream_at:${args.projectIdentity}`, String(result.finishedAt));
15329
15331
  setDreamState(args.db, "last_dream_at", String(result.finishedAt));
@@ -15569,9 +15571,20 @@ function checkScheduleAndEnqueue(db, schedule) {
15569
15571
  }
15570
15572
  return enqueued;
15571
15573
  }
15572
- // src/features/magic-context/storage-db.ts
15574
+ // src/shared/internal-initiator-marker.ts
15575
+ var OMO_INTERNAL_INITIATOR_MARKER = "<!-- OMO_INTERNAL_INITIATOR -->";
15576
+
15577
+ // src/shared/system-directive.ts
15578
+ var SYSTEM_DIRECTIVE_PREFIX = "[SYSTEM DIRECTIVE: MAGIC-CONTEXT";
15579
+ function isSystemDirective(text) {
15580
+ return text.trimStart().startsWith(SYSTEM_DIRECTIVE_PREFIX);
15581
+ }
15582
+ function removeSystemReminders(text) {
15583
+ return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, "").trim();
15584
+ }
15585
+
15586
+ // src/hooks/magic-context/read-session-db.ts
15573
15587
  import { Database } from "bun:sqlite";
15574
- import { mkdirSync } from "fs";
15575
15588
  import { join as join5 } from "path";
15576
15589
 
15577
15590
  // src/shared/data-path.ts
@@ -15584,1012 +15597,1049 @@ function getOpenCodeStorageDir() {
15584
15597
  return path2.join(getDataDir(), "opencode", "storage");
15585
15598
  }
15586
15599
 
15587
- // src/features/magic-context/storage-db.ts
15588
- var databases = new Map;
15589
- var FALLBACK_DATABASE_KEY = "__fallback__:memory:";
15590
- var persistenceByDatabase = new WeakMap;
15591
- var persistenceErrorByDatabase = new WeakMap;
15592
- function resolveDatabasePath() {
15593
- const dbDir = join5(getOpenCodeStorageDir(), "plugin", "magic-context");
15594
- return { dbDir, dbPath: join5(dbDir, "context.db") };
15600
+ // src/hooks/magic-context/read-session-db.ts
15601
+ function getOpenCodeDbPath() {
15602
+ return join5(getDataDir(), "opencode", "opencode.db");
15603
+ }
15604
+ var cachedReadOnlyDb = null;
15605
+ function closeCachedReadOnlyDb() {
15606
+ if (!cachedReadOnlyDb) {
15607
+ return;
15608
+ }
15609
+ try {
15610
+ cachedReadOnlyDb.db.close(false);
15611
+ } catch (error48) {
15612
+ log("[magic-context] failed to close cached OpenCode read-only DB:", error48);
15613
+ } finally {
15614
+ cachedReadOnlyDb = null;
15615
+ }
15616
+ }
15617
+ function getReadOnlySessionDb() {
15618
+ const dbPath = getOpenCodeDbPath();
15619
+ if (cachedReadOnlyDb?.path === dbPath) {
15620
+ return cachedReadOnlyDb.db;
15621
+ }
15622
+ closeCachedReadOnlyDb();
15623
+ const db = new Database(dbPath, { readonly: true });
15624
+ cachedReadOnlyDb = { path: dbPath, db };
15625
+ return db;
15626
+ }
15627
+ function withReadOnlySessionDb(fn) {
15628
+ return fn(getReadOnlySessionDb());
15629
+ }
15630
+ function getRawSessionMessageCountFromDb(db, sessionId) {
15631
+ const row = db.prepare("SELECT COUNT(*) as count FROM message WHERE session_id = ?").get(sessionId);
15632
+ return typeof row?.count === "number" ? row.count : 0;
15595
15633
  }
15596
- function initializeDatabase(db) {
15597
- db.run("PRAGMA journal_mode=WAL");
15598
- db.run("PRAGMA busy_timeout=5000");
15599
- db.run("PRAGMA foreign_keys=ON");
15600
- db.run(`
15601
- CREATE TABLE IF NOT EXISTS tags (
15602
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15603
- session_id TEXT,
15604
- message_id TEXT,
15605
- type TEXT,
15606
- status TEXT DEFAULT 'active',
15607
- byte_size INTEGER,
15608
- tag_number INTEGER,
15609
- UNIQUE(session_id, tag_number)
15610
- );
15611
-
15612
- CREATE TABLE IF NOT EXISTS pending_ops (
15613
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15614
- session_id TEXT,
15615
- tag_id INTEGER,
15616
- operation TEXT,
15617
- queued_at INTEGER
15618
- );
15619
15634
 
15620
- CREATE TABLE IF NOT EXISTS source_contents (
15621
- tag_id INTEGER,
15622
- session_id TEXT,
15623
- content TEXT,
15624
- created_at INTEGER,
15625
- PRIMARY KEY(session_id, tag_id)
15626
- );
15635
+ // src/hooks/magic-context/read-session-formatting.ts
15636
+ var COMMIT_HASH_PATTERN = /`?\b([0-9a-f]{6,12})\b`?/gi;
15637
+ var COMMIT_HINT_PATTERN = /\b(commit(?:ted)?|cherry-?pick(?:ed)?|hash(?:es)?|sha)\b/i;
15638
+ var MAX_COMMITS_PER_BLOCK = 5;
15639
+ function hasMeaningfulUserText(parts) {
15640
+ for (const part of parts) {
15641
+ if (part === null || typeof part !== "object")
15642
+ continue;
15643
+ const candidate = part;
15644
+ if (candidate.type !== "text" || typeof candidate.text !== "string")
15645
+ continue;
15646
+ if (candidate.ignored === true)
15647
+ continue;
15648
+ const cleaned = removeSystemReminders(candidate.text).replace(OMO_INTERNAL_INITIATOR_MARKER, "").trim();
15649
+ if (!cleaned)
15650
+ continue;
15651
+ if (isSystemDirective(cleaned))
15652
+ continue;
15653
+ return true;
15654
+ }
15655
+ return false;
15656
+ }
15657
+ function extractTexts(parts) {
15658
+ const texts = [];
15659
+ for (const part of parts) {
15660
+ if (part === null || typeof part !== "object")
15661
+ continue;
15662
+ const p = part;
15663
+ if (p.type === "text" && typeof p.text === "string" && p.text.trim().length > 0) {
15664
+ texts.push(p.text.trim());
15665
+ }
15666
+ }
15667
+ return texts;
15668
+ }
15669
+ function estimateTokens(text) {
15670
+ return Math.ceil(text.length / 3.5);
15671
+ }
15672
+ function normalizeText(text) {
15673
+ return text.replace(/\s+/g, " ").trim();
15674
+ }
15675
+ function compactRole(role) {
15676
+ if (role === "assistant")
15677
+ return "A";
15678
+ if (role === "user")
15679
+ return "U";
15680
+ return role.slice(0, 1).toUpperCase() || "M";
15681
+ }
15682
+ function formatBlock(block) {
15683
+ const range = block.startOrdinal === block.endOrdinal ? `[${block.startOrdinal}]` : `[${block.startOrdinal}-${block.endOrdinal}]`;
15684
+ const commitSuffix = block.commitHashes.length > 0 ? ` commits: ${block.commitHashes.join(", ")}` : "";
15685
+ return `${range} ${block.role}:${commitSuffix} ${block.parts.join(" / ")}`;
15686
+ }
15687
+ function extractCommitHashes(text) {
15688
+ const hashes = [];
15689
+ const seen = new Set;
15690
+ for (const match of text.matchAll(COMMIT_HASH_PATTERN)) {
15691
+ const hash2 = match[1]?.toLowerCase();
15692
+ if (!hash2 || seen.has(hash2))
15693
+ continue;
15694
+ seen.add(hash2);
15695
+ hashes.push(hash2);
15696
+ if (hashes.length >= MAX_COMMITS_PER_BLOCK)
15697
+ break;
15698
+ }
15699
+ return hashes;
15700
+ }
15701
+ function compactTextForSummary(text, role) {
15702
+ const commitHashes = role === "assistant" ? extractCommitHashes(text) : [];
15703
+ if (commitHashes.length === 0 || !COMMIT_HINT_PATTERN.test(text)) {
15704
+ return { text, commitHashes };
15705
+ }
15706
+ const withoutHashes = text.replace(COMMIT_HASH_PATTERN, "").replace(/\(\s*\)/g, "").replace(/\s+,/g, ",").replace(/,\s*,+/g, ", ").replace(/\s{2,}/g, " ").replace(/\s+([,.;:])/g, "$1").trim();
15707
+ return {
15708
+ text: withoutHashes.length > 0 ? withoutHashes : text,
15709
+ commitHashes
15710
+ };
15711
+ }
15712
+ function mergeCommitHashes(existing, next) {
15713
+ if (next.length === 0)
15714
+ return existing;
15715
+ const merged = [...existing];
15716
+ for (const hash2 of next) {
15717
+ if (merged.includes(hash2))
15718
+ continue;
15719
+ merged.push(hash2);
15720
+ if (merged.length >= MAX_COMMITS_PER_BLOCK)
15721
+ break;
15722
+ }
15723
+ return merged;
15724
+ }
15627
15725
 
15628
- CREATE TABLE IF NOT EXISTS compartments (
15629
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15630
- session_id TEXT NOT NULL,
15631
- sequence INTEGER NOT NULL,
15632
- start_message INTEGER NOT NULL,
15633
- end_message INTEGER NOT NULL,
15634
- start_message_id TEXT DEFAULT '',
15635
- end_message_id TEXT DEFAULT '',
15636
- title TEXT NOT NULL,
15637
- content TEXT NOT NULL,
15638
- created_at INTEGER NOT NULL,
15639
- UNIQUE(session_id, sequence)
15640
- );
15726
+ // src/hooks/magic-context/read-session-raw.ts
15727
+ function isRawMessageRow(row) {
15728
+ if (row === null || typeof row !== "object")
15729
+ return false;
15730
+ const candidate = row;
15731
+ return typeof candidate.id === "string" && typeof candidate.data === "string";
15732
+ }
15733
+ function isRawPartRow(row) {
15734
+ if (row === null || typeof row !== "object")
15735
+ return false;
15736
+ const candidate = row;
15737
+ return typeof candidate.message_id === "string" && typeof candidate.data === "string";
15738
+ }
15739
+ function parseJsonRecord(value) {
15740
+ const parsed = JSON.parse(value);
15741
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
15742
+ throw new Error("Expected JSON object");
15743
+ }
15744
+ return parsed;
15745
+ }
15746
+ function parseJsonUnknown(value) {
15747
+ return JSON.parse(value);
15748
+ }
15749
+ function readRawSessionMessagesFromDb(db, sessionId) {
15750
+ const messageRows = db.prepare("SELECT id, data FROM message WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId).filter(isRawMessageRow);
15751
+ const partRows = db.prepare("SELECT message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId).filter(isRawPartRow);
15752
+ const partsByMessageId = new Map;
15753
+ for (const part of partRows) {
15754
+ const list = partsByMessageId.get(part.message_id) ?? [];
15755
+ list.push(parseJsonUnknown(part.data));
15756
+ partsByMessageId.set(part.message_id, list);
15757
+ }
15758
+ return messageRows.map((row, index) => {
15759
+ const info = parseJsonRecord(row.data);
15760
+ const role = typeof info.role === "string" ? info.role : "unknown";
15761
+ return {
15762
+ ordinal: index + 1,
15763
+ id: row.id,
15764
+ role,
15765
+ parts: partsByMessageId.get(row.id) ?? []
15766
+ };
15767
+ });
15768
+ }
15641
15769
 
15642
- CREATE TABLE IF NOT EXISTS session_facts (
15643
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15644
- session_id TEXT NOT NULL,
15645
- category TEXT NOT NULL,
15646
- content TEXT NOT NULL,
15647
- created_at INTEGER NOT NULL,
15648
- updated_at INTEGER NOT NULL
15649
- );
15770
+ // src/hooks/magic-context/tag-content-primitives.ts
15771
+ var encoder = new TextEncoder;
15772
+ var TAG_PREFIX_REGEX = /^\u00A7\d+\u00A7\s*/;
15773
+ function byteSize(value) {
15774
+ return encoder.encode(value).length;
15775
+ }
15776
+ function stripTagPrefix(value) {
15777
+ return value.replace(TAG_PREFIX_REGEX, "");
15778
+ }
15779
+ function prependTag(tagId, value) {
15780
+ const stripped = stripTagPrefix(value);
15781
+ return `\xA7${tagId}\xA7 ${stripped}`;
15782
+ }
15783
+ function isThinkingPart(part) {
15784
+ if (part === null || typeof part !== "object")
15785
+ return false;
15786
+ const candidate = part;
15787
+ return candidate.type === "thinking" || candidate.type === "reasoning";
15788
+ }
15650
15789
 
15651
- CREATE TABLE IF NOT EXISTS session_notes (
15652
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15653
- session_id TEXT NOT NULL,
15654
- content TEXT NOT NULL,
15655
- created_at INTEGER NOT NULL
15656
- );
15657
-
15658
- CREATE TABLE IF NOT EXISTS memories (
15659
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15660
- project_path TEXT NOT NULL,
15661
- category TEXT NOT NULL,
15662
- content TEXT NOT NULL,
15663
- normalized_hash TEXT NOT NULL,
15664
- source_session_id TEXT,
15665
- source_type TEXT DEFAULT 'historian',
15666
- seen_count INTEGER DEFAULT 1,
15667
- retrieval_count INTEGER DEFAULT 0,
15668
- first_seen_at INTEGER NOT NULL,
15669
- created_at INTEGER NOT NULL,
15670
- updated_at INTEGER NOT NULL,
15671
- last_seen_at INTEGER NOT NULL,
15672
- last_retrieved_at INTEGER,
15673
- status TEXT DEFAULT 'active',
15674
- expires_at INTEGER,
15675
- verification_status TEXT DEFAULT 'unverified',
15676
- verified_at INTEGER,
15677
- superseded_by_memory_id INTEGER,
15678
- merged_from TEXT,
15679
- metadata_json TEXT,
15680
- UNIQUE(project_path, category, normalized_hash)
15681
- );
15682
-
15683
- CREATE TABLE IF NOT EXISTS memory_embeddings (
15684
- memory_id INTEGER PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
15685
- embedding BLOB NOT NULL,
15686
- model_id TEXT
15687
- );
15688
-
15689
- CREATE TABLE IF NOT EXISTS dream_state (
15690
- key TEXT PRIMARY KEY,
15691
- value TEXT NOT NULL
15692
- );
15693
-
15694
- CREATE TABLE IF NOT EXISTS dream_queue (
15695
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15696
- project_path TEXT NOT NULL,
15697
- reason TEXT NOT NULL,
15698
- enqueued_at INTEGER NOT NULL,
15699
- started_at INTEGER,
15700
- retry_count INTEGER DEFAULT 0
15701
- );
15702
- CREATE INDEX IF NOT EXISTS idx_dream_queue_project ON dream_queue(project_path);
15703
- CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, enqueued_at);
15704
-
15705
- CREATE TABLE IF NOT EXISTS smart_notes (
15706
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15707
- project_path TEXT NOT NULL,
15708
- content TEXT NOT NULL,
15709
- surface_condition TEXT NOT NULL,
15710
- status TEXT NOT NULL DEFAULT 'pending',
15711
- created_session_id TEXT,
15712
- created_at INTEGER NOT NULL,
15713
- updated_at INTEGER NOT NULL,
15714
- last_checked_at INTEGER,
15715
- ready_at INTEGER,
15716
- ready_reason TEXT
15717
- );
15718
- CREATE INDEX IF NOT EXISTS idx_smart_notes_project_status ON smart_notes(project_path, status);
15719
-
15720
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
15721
- content,
15722
- category,
15723
- content='memories',
15724
- content_rowid='id',
15725
- tokenize='porter unicode61'
15726
- );
15727
-
15728
- CREATE VIRTUAL TABLE IF NOT EXISTS message_history_fts USING fts5(
15729
- session_id UNINDEXED,
15730
- message_ordinal UNINDEXED,
15731
- message_id UNINDEXED,
15732
- role,
15733
- content,
15734
- tokenize='porter unicode61'
15735
- );
15736
-
15737
- CREATE TABLE IF NOT EXISTS message_history_index (
15738
- session_id TEXT PRIMARY KEY,
15739
- last_indexed_ordinal INTEGER NOT NULL DEFAULT 0,
15740
- updated_at INTEGER NOT NULL
15741
- );
15742
-
15743
- CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
15744
- INSERT INTO memories_fts(rowid, content, category) VALUES (new.id, new.content, new.category);
15745
- END;
15746
-
15747
- CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
15748
- INSERT INTO memories_fts(memories_fts, rowid, content, category) VALUES ('delete', old.id, old.content, old.category);
15749
- END;
15750
-
15751
- CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
15752
- INSERT INTO memories_fts(memories_fts, rowid, content, category) VALUES ('delete', old.id, old.content, old.category);
15753
- INSERT INTO memories_fts(rowid, content, category) VALUES (new.id, new.content, new.category);
15754
- END;
15755
-
15756
- CREATE TABLE IF NOT EXISTS session_meta (
15757
- session_id TEXT PRIMARY KEY,
15758
- last_response_time INTEGER,
15759
- cache_ttl TEXT,
15760
- counter INTEGER DEFAULT 0,
15761
- last_nudge_tokens INTEGER DEFAULT 0,
15762
- last_nudge_band TEXT DEFAULT '',
15763
- last_transform_error TEXT DEFAULT '',
15764
- nudge_anchor_message_id TEXT DEFAULT '',
15765
- nudge_anchor_text TEXT DEFAULT '',
15766
- sticky_turn_reminder_text TEXT DEFAULT '',
15767
- sticky_turn_reminder_message_id TEXT DEFAULT '',
15768
- note_nudge_trigger_pending INTEGER DEFAULT 0,
15769
- note_nudge_trigger_message_id TEXT DEFAULT '',
15770
- note_nudge_sticky_text TEXT DEFAULT '',
15771
- note_nudge_sticky_message_id TEXT DEFAULT '',
15772
- is_subagent INTEGER DEFAULT 0,
15773
- last_context_percentage REAL DEFAULT 0,
15774
- last_input_tokens INTEGER DEFAULT 0,
15775
- times_execute_threshold_reached INTEGER DEFAULT 0,
15776
- compartment_in_progress INTEGER DEFAULT 0,
15777
- system_prompt_hash TEXT DEFAULT '',
15778
- memory_block_cache TEXT DEFAULT '',
15779
- memory_block_count INTEGER DEFAULT 0
15780
- );
15781
-
15782
- CREATE INDEX IF NOT EXISTS idx_tags_session_tag_number ON tags(session_id, tag_number);
15783
- CREATE INDEX IF NOT EXISTS idx_pending_ops_session ON pending_ops(session_id);
15784
- CREATE INDEX IF NOT EXISTS idx_pending_ops_session_tag_id ON pending_ops(session_id, tag_id);
15785
- CREATE INDEX IF NOT EXISTS idx_source_contents_session ON source_contents(session_id);
15786
-
15787
- CREATE TABLE IF NOT EXISTS recomp_compartments (
15788
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15789
- session_id TEXT NOT NULL,
15790
- sequence INTEGER NOT NULL,
15791
- start_message INTEGER NOT NULL,
15792
- end_message INTEGER NOT NULL,
15793
- start_message_id TEXT DEFAULT '',
15794
- end_message_id TEXT DEFAULT '',
15795
- title TEXT NOT NULL,
15796
- content TEXT NOT NULL,
15797
- pass_number INTEGER NOT NULL,
15798
- created_at INTEGER NOT NULL,
15799
- UNIQUE(session_id, sequence)
15800
- );
15801
-
15802
- CREATE TABLE IF NOT EXISTS recomp_facts (
15803
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15804
- session_id TEXT NOT NULL,
15805
- category TEXT NOT NULL,
15806
- content TEXT NOT NULL,
15807
- pass_number INTEGER NOT NULL,
15808
- created_at INTEGER NOT NULL
15809
- );
15790
+ // src/hooks/magic-context/tag-part-guards.ts
15791
+ function isTextPart(part) {
15792
+ if (part === null || typeof part !== "object")
15793
+ return false;
15794
+ const p = part;
15795
+ return p.type === "text" && typeof p.text === "string";
15796
+ }
15797
+ function isToolPartWithOutput(part) {
15798
+ if (part === null || typeof part !== "object")
15799
+ return false;
15800
+ const p = part;
15801
+ if (p.type !== "tool" || typeof p.callID !== "string")
15802
+ return false;
15803
+ if (p.state === null || typeof p.state !== "object")
15804
+ return false;
15805
+ return typeof p.state.output === "string";
15806
+ }
15807
+ function isFilePart(part) {
15808
+ if (part === null || typeof part !== "object")
15809
+ return false;
15810
+ const p = part;
15811
+ return p.type === "file" && typeof p.url === "string";
15812
+ }
15813
+ function buildFileSourceContent(parts) {
15814
+ const content = parts.filter(isTextPart).map((part) => stripTagPrefix(part.text)).join(`
15815
+ `).trim();
15816
+ return content.length > 0 ? content : null;
15817
+ }
15810
15818
 
15811
- CREATE INDEX IF NOT EXISTS idx_compartments_session ON compartments(session_id);
15812
- CREATE INDEX IF NOT EXISTS idx_session_facts_session ON session_facts(session_id);
15813
- CREATE INDEX IF NOT EXISTS idx_recomp_compartments_session ON recomp_compartments(session_id);
15814
- CREATE INDEX IF NOT EXISTS idx_recomp_facts_session ON recomp_facts(session_id);
15815
- CREATE INDEX IF NOT EXISTS idx_session_notes_session ON session_notes(session_id);
15816
- CREATE INDEX IF NOT EXISTS idx_memories_project_status_category ON memories(project_path, status, category);
15817
- CREATE INDEX IF NOT EXISTS idx_memories_project_status_expires ON memories(project_path, status, expires_at);
15818
- CREATE INDEX IF NOT EXISTS idx_memories_project_category_hash ON memories(project_path, category, normalized_hash);
15819
- CREATE INDEX IF NOT EXISTS idx_message_history_index_updated_at ON message_history_index(updated_at);
15820
- `);
15821
- ensureColumn(db, "session_meta", "last_nudge_band", "TEXT DEFAULT ''");
15822
- ensureColumn(db, "session_meta", "last_transform_error", "TEXT DEFAULT ''");
15823
- ensureColumn(db, "session_meta", "nudge_anchor_message_id", "TEXT DEFAULT ''");
15824
- ensureColumn(db, "session_meta", "nudge_anchor_text", "TEXT DEFAULT ''");
15825
- ensureColumn(db, "session_meta", "sticky_turn_reminder_text", "TEXT DEFAULT ''");
15826
- ensureColumn(db, "session_meta", "sticky_turn_reminder_message_id", "TEXT DEFAULT ''");
15827
- ensureColumn(db, "session_meta", "note_nudge_trigger_pending", "INTEGER DEFAULT 0");
15828
- ensureColumn(db, "session_meta", "note_nudge_trigger_message_id", "TEXT DEFAULT ''");
15829
- ensureColumn(db, "session_meta", "note_nudge_sticky_text", "TEXT DEFAULT ''");
15830
- ensureColumn(db, "session_meta", "note_nudge_sticky_message_id", "TEXT DEFAULT ''");
15831
- ensureColumn(db, "session_meta", "times_execute_threshold_reached", "INTEGER DEFAULT 0");
15832
- ensureColumn(db, "session_meta", "compartment_in_progress", "INTEGER DEFAULT 0");
15833
- ensureColumn(db, "session_meta", "system_prompt_hash", "TEXT DEFAULT ''");
15834
- ensureColumn(db, "session_meta", "cleared_reasoning_through_tag", "INTEGER DEFAULT 0");
15835
- ensureColumn(db, "session_meta", "stripped_placeholder_ids", "TEXT DEFAULT ''");
15836
- ensureColumn(db, "compartments", "start_message_id", "TEXT DEFAULT ''");
15837
- ensureColumn(db, "compartments", "end_message_id", "TEXT DEFAULT ''");
15838
- ensureColumn(db, "memory_embeddings", "model_id", "TEXT");
15839
- ensureColumn(db, "session_meta", "memory_block_cache", "TEXT DEFAULT ''");
15840
- ensureColumn(db, "session_meta", "memory_block_count", "INTEGER DEFAULT 0");
15841
- ensureColumn(db, "dream_queue", "retry_count", "INTEGER DEFAULT 0");
15819
+ // src/hooks/magic-context/read-session-chunk.ts
15820
+ var activeRawMessageCache = null;
15821
+ function cleanUserText(text) {
15822
+ return removeSystemReminders(text).replace(OMO_INTERNAL_INITIATOR_MARKER, "").trim();
15842
15823
  }
15843
- function ensureColumn(db, table, column, definition) {
15844
- if (!/^[a-z_]+$/.test(table) || !/^[a-z_]+$/.test(column) || !/^[A-Z0-9_'(),\s]+$/i.test(definition)) {
15845
- throw new Error(`Unsafe schema identifier: ${table}.${column} ${definition}`);
15824
+ function withRawSessionMessageCache(fn) {
15825
+ const outerCache = activeRawMessageCache;
15826
+ if (!outerCache) {
15827
+ activeRawMessageCache = new Map;
15846
15828
  }
15847
- const rows = db.prepare(`PRAGMA table_info(${table})`).all();
15848
- if (rows.some((row) => row.name === column)) {
15849
- return;
15829
+ try {
15830
+ return fn();
15831
+ } finally {
15832
+ if (!outerCache) {
15833
+ activeRawMessageCache = null;
15834
+ }
15850
15835
  }
15851
- db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
15852
15836
  }
15853
- function createFallbackDatabase() {
15854
- try {
15855
- const fallback = new Database(":memory:");
15856
- initializeDatabase(fallback);
15857
- return fallback;
15858
- } catch (error48) {
15859
- throw new Error(`[magic-context] storage fatal: failed to initialize fallback database: ${getErrorMessage(error48)}`);
15837
+ function readRawSessionMessages(sessionId) {
15838
+ if (activeRawMessageCache) {
15839
+ const cached2 = activeRawMessageCache.get(sessionId);
15840
+ if (cached2) {
15841
+ return cached2;
15842
+ }
15843
+ const messages = withReadOnlySessionDb((db) => readRawSessionMessagesFromDb(db, sessionId));
15844
+ activeRawMessageCache.set(sessionId, messages);
15845
+ return messages;
15860
15846
  }
15847
+ return withReadOnlySessionDb((db) => readRawSessionMessagesFromDb(db, sessionId));
15861
15848
  }
15862
- function openDatabase() {
15863
- try {
15864
- const { dbDir, dbPath } = resolveDatabasePath();
15865
- const existing = databases.get(dbPath);
15866
- if (existing) {
15867
- if (!persistenceByDatabase.has(existing)) {
15868
- persistenceByDatabase.set(existing, true);
15849
+ function getRawSessionMessageCount(sessionId) {
15850
+ return withReadOnlySessionDb((db) => getRawSessionMessageCountFromDb(db, sessionId));
15851
+ }
15852
+ function getRawSessionTagKeysThrough(sessionId, upToMessageIndex) {
15853
+ const messages = readRawSessionMessages(sessionId);
15854
+ const keys = [];
15855
+ for (const message of messages) {
15856
+ if (message.ordinal > upToMessageIndex)
15857
+ break;
15858
+ for (const [partIndex, part] of message.parts.entries()) {
15859
+ if (isTextPart(part)) {
15860
+ keys.push(`${message.id}:p${partIndex}`);
15869
15861
  }
15870
- return existing;
15871
- }
15872
- mkdirSync(dbDir, { recursive: true });
15873
- const db = new Database(dbPath);
15874
- initializeDatabase(db);
15875
- databases.set(dbPath, db);
15876
- persistenceByDatabase.set(db, true);
15877
- persistenceErrorByDatabase.delete(db);
15878
- return db;
15879
- } catch (error48) {
15880
- log("[magic-context] storage error:", error48);
15881
- const errorMessage = getErrorMessage(error48);
15882
- const existingFallback = databases.get(FALLBACK_DATABASE_KEY);
15883
- if (existingFallback) {
15884
- if (!persistenceByDatabase.has(existingFallback)) {
15885
- persistenceByDatabase.set(existingFallback, false);
15886
- persistenceErrorByDatabase.set(existingFallback, errorMessage);
15862
+ if (isFilePart(part)) {
15863
+ keys.push(`${message.id}:file${partIndex}`);
15864
+ }
15865
+ if (isToolPartWithOutput(part)) {
15866
+ keys.push(part.callID);
15887
15867
  }
15888
- return existingFallback;
15889
15868
  }
15890
- const fallback = createFallbackDatabase();
15891
- databases.set(FALLBACK_DATABASE_KEY, fallback);
15892
- persistenceByDatabase.set(fallback, false);
15893
- persistenceErrorByDatabase.set(fallback, errorMessage);
15894
- return fallback;
15895
15869
  }
15870
+ return keys;
15896
15871
  }
15897
- function isDatabasePersisted(db) {
15898
- return persistenceByDatabase.get(db) ?? false;
15899
- }
15900
- function getDatabasePersistenceError(db) {
15901
- return persistenceErrorByDatabase.get(db) ?? null;
15902
- }
15903
- // src/features/magic-context/storage-meta-shared.ts
15904
- var META_COLUMNS = {
15905
- lastResponseTime: "last_response_time",
15906
- cacheTtl: "cache_ttl",
15907
- counter: "counter",
15908
- lastNudgeTokens: "last_nudge_tokens",
15909
- lastNudgeBand: "last_nudge_band",
15910
- lastTransformError: "last_transform_error",
15911
- isSubagent: "is_subagent",
15912
- lastContextPercentage: "last_context_percentage",
15913
- lastInputTokens: "last_input_tokens",
15914
- timesExecuteThresholdReached: "times_execute_threshold_reached",
15915
- compartmentInProgress: "compartment_in_progress",
15916
- systemPromptHash: "system_prompt_hash",
15917
- clearedReasoningThroughTag: "cleared_reasoning_through_tag"
15918
- };
15919
- var BOOLEAN_META_KEYS = new Set(["isSubagent", "compartmentInProgress"]);
15920
- function isSessionMetaRow(row) {
15921
- if (row === null || typeof row !== "object")
15922
- return false;
15923
- const r = row;
15924
- return typeof r.session_id === "string" && typeof r.last_response_time === "number" && typeof r.cache_ttl === "string" && typeof r.counter === "number" && typeof r.last_nudge_tokens === "number" && typeof r.last_nudge_band === "string" && typeof r.last_transform_error === "string" && typeof r.is_subagent === "number" && typeof r.last_context_percentage === "number" && typeof r.last_input_tokens === "number" && typeof r.times_execute_threshold_reached === "number" && typeof r.compartment_in_progress === "number" && (typeof r.system_prompt_hash === "string" || typeof r.system_prompt_hash === "number") && typeof r.cleared_reasoning_through_tag === "number";
15925
- }
15926
- function getDefaultSessionMeta(sessionId) {
15927
- return {
15928
- sessionId,
15929
- lastResponseTime: 0,
15930
- cacheTtl: "5m",
15931
- counter: 0,
15932
- lastNudgeTokens: 0,
15933
- lastNudgeBand: null,
15934
- lastTransformError: null,
15935
- isSubagent: false,
15936
- lastContextPercentage: 0,
15937
- lastInputTokens: 0,
15938
- timesExecuteThresholdReached: 0,
15939
- compartmentInProgress: false,
15940
- systemPromptHash: "",
15941
- clearedReasoningThroughTag: 0
15942
- };
15943
- }
15944
- function ensureSessionMetaRow(db, sessionId) {
15945
- const defaults = getDefaultSessionMeta(sessionId);
15946
- db.prepare("INSERT OR IGNORE INTO session_meta (session_id, last_response_time, cache_ttl, counter, last_nudge_tokens, last_nudge_band, last_transform_error, is_subagent, last_context_percentage, last_input_tokens, times_execute_threshold_reached, compartment_in_progress, system_prompt_hash, cleared_reasoning_through_tag) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(sessionId, defaults.lastResponseTime, defaults.cacheTtl, defaults.counter, defaults.lastNudgeTokens, defaults.lastNudgeBand ?? "", defaults.lastTransformError ?? "", defaults.isSubagent ? 1 : 0, defaults.lastContextPercentage, defaults.lastInputTokens, defaults.timesExecuteThresholdReached, defaults.compartmentInProgress ? 1 : 0, defaults.systemPromptHash ?? "", defaults.clearedReasoningThroughTag);
15872
+ var PROTECTED_TAIL_USER_TURNS = 5;
15873
+ function getProtectedTailStartOrdinal(sessionId) {
15874
+ const messages = readRawSessionMessages(sessionId);
15875
+ const userOrdinals = messages.filter((m) => m.role === "user" && hasMeaningfulUserText(m.parts)).map((m) => m.ordinal);
15876
+ if (userOrdinals.length < PROTECTED_TAIL_USER_TURNS) {
15877
+ return 1;
15878
+ }
15879
+ return userOrdinals[userOrdinals.length - PROTECTED_TAIL_USER_TURNS];
15947
15880
  }
15948
- function toSessionMeta(row) {
15881
+ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal) {
15882
+ const messages = readRawSessionMessages(sessionId);
15883
+ const startOrdinal = Math.max(1, offset);
15884
+ const lines = [];
15885
+ const lineMeta = [];
15886
+ let totalTokens = 0;
15887
+ let messagesProcessed = 0;
15888
+ let lastOrdinal = startOrdinal - 1;
15889
+ let lastMessageId = "";
15890
+ let firstMessageId = "";
15891
+ let currentBlock = null;
15892
+ let pendingNoiseMeta = [];
15893
+ let commitClusters = 0;
15894
+ let lastFlushedRole = "";
15895
+ function flushCurrentBlock() {
15896
+ if (!currentBlock)
15897
+ return true;
15898
+ const blockText = formatBlock(currentBlock);
15899
+ const blockTokens = estimateTokens(blockText);
15900
+ if (totalTokens + blockTokens > tokenBudget && totalTokens > 0) {
15901
+ return false;
15902
+ }
15903
+ if (currentBlock.role === "A" && currentBlock.commitHashes.length > 0 && lastFlushedRole !== "A") {
15904
+ commitClusters++;
15905
+ }
15906
+ lastFlushedRole = currentBlock.role;
15907
+ if (!firstMessageId)
15908
+ firstMessageId = currentBlock.meta[0]?.messageId ?? "";
15909
+ lastOrdinal = currentBlock.meta[currentBlock.meta.length - 1]?.ordinal ?? currentBlock.endOrdinal;
15910
+ lastMessageId = currentBlock.meta[currentBlock.meta.length - 1]?.messageId ?? "";
15911
+ messagesProcessed += currentBlock.meta.length;
15912
+ lines.push(blockText);
15913
+ lineMeta.push(...currentBlock.meta);
15914
+ totalTokens += blockTokens;
15915
+ currentBlock = null;
15916
+ return true;
15917
+ }
15918
+ for (const msg of messages) {
15919
+ if (eligibleEndOrdinal !== undefined && msg.ordinal >= eligibleEndOrdinal)
15920
+ break;
15921
+ if (msg.ordinal < startOrdinal)
15922
+ continue;
15923
+ const meta3 = { ordinal: msg.ordinal, messageId: msg.id };
15924
+ if (msg.role === "user" && !hasMeaningfulUserText(msg.parts)) {
15925
+ pendingNoiseMeta.push(meta3);
15926
+ continue;
15927
+ }
15928
+ const role = compactRole(msg.role);
15929
+ const compacted = compactTextForSummary(extractTexts(msg.parts).map((t) => msg.role === "user" ? cleanUserText(t) : t).map(normalizeText).filter((value) => value.length > 0).join(" / "), msg.role);
15930
+ const text = compacted.text;
15931
+ if (!text) {
15932
+ pendingNoiseMeta.push(meta3);
15933
+ continue;
15934
+ }
15935
+ if (currentBlock && currentBlock.role === role) {
15936
+ currentBlock.endOrdinal = msg.ordinal;
15937
+ currentBlock.parts.push(text);
15938
+ currentBlock.meta.push(...pendingNoiseMeta, meta3);
15939
+ currentBlock.commitHashes = mergeCommitHashes(currentBlock.commitHashes, compacted.commitHashes);
15940
+ pendingNoiseMeta = [];
15941
+ continue;
15942
+ }
15943
+ if (!flushCurrentBlock())
15944
+ break;
15945
+ currentBlock = {
15946
+ role,
15947
+ startOrdinal: pendingNoiseMeta[0]?.ordinal ?? msg.ordinal,
15948
+ endOrdinal: msg.ordinal,
15949
+ parts: [text],
15950
+ meta: [...pendingNoiseMeta, meta3],
15951
+ commitHashes: [...compacted.commitHashes]
15952
+ };
15953
+ pendingNoiseMeta = [];
15954
+ }
15955
+ flushCurrentBlock();
15949
15956
  return {
15950
- sessionId: row.session_id,
15951
- lastResponseTime: row.last_response_time,
15952
- cacheTtl: row.cache_ttl,
15953
- counter: row.counter,
15954
- lastNudgeTokens: row.last_nudge_tokens,
15955
- lastNudgeBand: row.last_nudge_band.length > 0 ? row.last_nudge_band : null,
15956
- lastTransformError: row.last_transform_error.length > 0 ? row.last_transform_error : null,
15957
- isSubagent: row.is_subagent === 1,
15958
- lastContextPercentage: row.last_context_percentage,
15959
- lastInputTokens: row.last_input_tokens,
15960
- timesExecuteThresholdReached: row.times_execute_threshold_reached,
15961
- compartmentInProgress: row.compartment_in_progress === 1,
15962
- systemPromptHash: String(row.system_prompt_hash),
15963
- clearedReasoningThroughTag: row.cleared_reasoning_through_tag
15957
+ startIndex: startOrdinal,
15958
+ endIndex: lastOrdinal,
15959
+ startMessageId: firstMessageId,
15960
+ endMessageId: lastMessageId,
15961
+ messageCount: messagesProcessed,
15962
+ tokenEstimate: totalTokens,
15963
+ hasMore: lastOrdinal < (eligibleEndOrdinal !== undefined ? Math.min(eligibleEndOrdinal - 1, messages.length) : messages.length),
15964
+ text: lines.join(`
15965
+ `),
15966
+ lines: lineMeta,
15967
+ commitClusterCount: commitClusters
15964
15968
  };
15965
15969
  }
15966
15970
 
15967
- // src/features/magic-context/storage-meta-persisted.ts
15968
- function isPersistedUsageRow(row) {
15969
- if (row === null || typeof row !== "object")
15970
- return false;
15971
- const r = row;
15972
- return typeof r.last_context_percentage === "number" && typeof r.last_input_tokens === "number" && typeof r.last_response_time === "number";
15971
+ // src/features/magic-context/message-index.ts
15972
+ var lastIndexedStatements = new WeakMap;
15973
+ var insertMessageStatements = new WeakMap;
15974
+ var upsertIndexStatements = new WeakMap;
15975
+ var deleteFtsStatements = new WeakMap;
15976
+ var deleteIndexStatements = new WeakMap;
15977
+ var countIndexedMessageStatements = new WeakMap;
15978
+ var deleteIndexedMessageStatements = new WeakMap;
15979
+ function normalizeIndexText(text) {
15980
+ return text.replace(/\s+/g, " ").trim();
15973
15981
  }
15974
- function isPersistedNudgePlacementRow(row) {
15975
- if (row === null || typeof row !== "object")
15976
- return false;
15977
- const r = row;
15978
- return typeof r.nudge_anchor_message_id === "string" && typeof r.nudge_anchor_text === "string";
15982
+ function getLastIndexedStatement(db) {
15983
+ let stmt = lastIndexedStatements.get(db);
15984
+ if (!stmt) {
15985
+ stmt = db.prepare("SELECT last_indexed_ordinal FROM message_history_index WHERE session_id = ?");
15986
+ lastIndexedStatements.set(db, stmt);
15987
+ }
15988
+ return stmt;
15979
15989
  }
15980
- function isPersistedStickyTurnReminderRow(row) {
15981
- if (row === null || typeof row !== "object")
15982
- return false;
15983
- const r = row;
15984
- return typeof r.sticky_turn_reminder_text === "string" && typeof r.sticky_turn_reminder_message_id === "string";
15990
+ function getInsertMessageStatement(db) {
15991
+ let stmt = insertMessageStatements.get(db);
15992
+ if (!stmt) {
15993
+ stmt = db.prepare("INSERT INTO message_history_fts (session_id, message_ordinal, message_id, role, content) VALUES (?, ?, ?, ?, ?)");
15994
+ insertMessageStatements.set(db, stmt);
15995
+ }
15996
+ return stmt;
15985
15997
  }
15986
- function isPersistedNoteNudgeRow(row) {
15987
- if (row === null || typeof row !== "object")
15988
- return false;
15989
- const r = row;
15990
- return typeof r.note_nudge_trigger_pending === "number" && typeof r.note_nudge_trigger_message_id === "string" && typeof r.note_nudge_sticky_text === "string" && typeof r.note_nudge_sticky_message_id === "string";
15998
+ function getUpsertIndexStatement(db) {
15999
+ let stmt = upsertIndexStatements.get(db);
16000
+ if (!stmt) {
16001
+ stmt = db.prepare("INSERT INTO message_history_index (session_id, last_indexed_ordinal, updated_at) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET last_indexed_ordinal = excluded.last_indexed_ordinal, updated_at = excluded.updated_at");
16002
+ upsertIndexStatements.set(db, stmt);
16003
+ }
16004
+ return stmt;
15991
16005
  }
15992
- function getDefaultPersistedNoteNudge() {
15993
- return {
15994
- triggerPending: false,
15995
- triggerMessageId: null,
15996
- stickyText: null,
15997
- stickyMessageId: null
15998
- };
15999
- }
16000
- function loadPersistedUsage(db, sessionId) {
16001
- const result = db.prepare("SELECT last_context_percentage, last_input_tokens, last_response_time FROM session_meta WHERE session_id = ?").get(sessionId);
16002
- if (!isPersistedUsageRow(result) || result.last_context_percentage === 0 && result.last_input_tokens === 0) {
16003
- return null;
16006
+ function getDeleteFtsStatement(db) {
16007
+ let stmt = deleteFtsStatements.get(db);
16008
+ if (!stmt) {
16009
+ stmt = db.prepare("DELETE FROM message_history_fts WHERE session_id = ?");
16010
+ deleteFtsStatements.set(db, stmt);
16004
16011
  }
16005
- return {
16006
- usage: {
16007
- percentage: result.last_context_percentage,
16008
- inputTokens: result.last_input_tokens
16009
- },
16010
- updatedAt: result.last_response_time || Date.now()
16011
- };
16012
+ return stmt;
16012
16013
  }
16013
- function getPersistedNudgePlacement(db, sessionId) {
16014
- const result = db.prepare("SELECT nudge_anchor_message_id, nudge_anchor_text FROM session_meta WHERE session_id = ?").get(sessionId);
16015
- if (!isPersistedNudgePlacementRow(result)) {
16016
- return null;
16017
- }
16018
- if (result.nudge_anchor_message_id.length === 0 || result.nudge_anchor_text.length === 0) {
16019
- return null;
16014
+ function getDeleteIndexStatement(db) {
16015
+ let stmt = deleteIndexStatements.get(db);
16016
+ if (!stmt) {
16017
+ stmt = db.prepare("DELETE FROM message_history_index WHERE session_id = ?");
16018
+ deleteIndexStatements.set(db, stmt);
16020
16019
  }
16021
- return {
16022
- messageId: result.nudge_anchor_message_id,
16023
- nudgeText: result.nudge_anchor_text
16024
- };
16025
- }
16026
- function setPersistedNudgePlacement(db, sessionId, messageId, nudgeText) {
16027
- db.transaction(() => {
16028
- ensureSessionMetaRow(db, sessionId);
16029
- db.prepare("UPDATE session_meta SET nudge_anchor_message_id = ?, nudge_anchor_text = ? WHERE session_id = ?").run(messageId, nudgeText, sessionId);
16030
- })();
16031
- }
16032
- function clearPersistedNudgePlacement(db, sessionId) {
16033
- db.prepare("UPDATE session_meta SET nudge_anchor_message_id = '', nudge_anchor_text = '' WHERE session_id = ?").run(sessionId);
16020
+ return stmt;
16034
16021
  }
16035
- function getPersistedStickyTurnReminder(db, sessionId) {
16036
- const result = db.prepare("SELECT sticky_turn_reminder_text, sticky_turn_reminder_message_id FROM session_meta WHERE session_id = ?").get(sessionId);
16037
- if (!isPersistedStickyTurnReminderRow(result)) {
16038
- return null;
16039
- }
16040
- if (result.sticky_turn_reminder_text.length === 0) {
16041
- return null;
16022
+ function getCountIndexedMessageStatement(db) {
16023
+ let stmt = countIndexedMessageStatements.get(db);
16024
+ if (!stmt) {
16025
+ stmt = db.prepare("SELECT COUNT(*) AS count FROM message_history_fts WHERE session_id = ? AND message_id = ?");
16026
+ countIndexedMessageStatements.set(db, stmt);
16042
16027
  }
16043
- return {
16044
- text: result.sticky_turn_reminder_text,
16045
- messageId: result.sticky_turn_reminder_message_id.length > 0 ? result.sticky_turn_reminder_message_id : null
16046
- };
16028
+ return stmt;
16047
16029
  }
16048
- function setPersistedStickyTurnReminder(db, sessionId, text, messageId = "") {
16049
- db.transaction(() => {
16050
- ensureSessionMetaRow(db, sessionId);
16051
- db.prepare("UPDATE session_meta SET sticky_turn_reminder_text = ?, sticky_turn_reminder_message_id = ? WHERE session_id = ?").run(text, messageId, sessionId);
16052
- })();
16030
+ function getDeleteIndexedMessageStatement(db) {
16031
+ let stmt = deleteIndexedMessageStatements.get(db);
16032
+ if (!stmt) {
16033
+ stmt = db.prepare("DELETE FROM message_history_fts WHERE session_id = ? AND message_id = ?");
16034
+ deleteIndexedMessageStatements.set(db, stmt);
16035
+ }
16036
+ return stmt;
16053
16037
  }
16054
- function clearPersistedStickyTurnReminder(db, sessionId) {
16055
- db.prepare("UPDATE session_meta SET sticky_turn_reminder_text = '', sticky_turn_reminder_message_id = '' WHERE session_id = ?").run(sessionId);
16038
+ function getLastIndexedOrdinal(db, sessionId) {
16039
+ const row = getLastIndexedStatement(db).get(sessionId);
16040
+ return typeof row?.last_indexed_ordinal === "number" ? row.last_indexed_ordinal : 0;
16056
16041
  }
16057
- function getPersistedNoteNudge(db, sessionId) {
16058
- const result = db.prepare("SELECT note_nudge_trigger_pending, note_nudge_trigger_message_id, note_nudge_sticky_text, note_nudge_sticky_message_id FROM session_meta WHERE session_id = ?").get(sessionId);
16059
- if (!isPersistedNoteNudgeRow(result)) {
16060
- return getDefaultPersistedNoteNudge();
16042
+ function deleteIndexedMessage(db, sessionId, messageId) {
16043
+ const row = getCountIndexedMessageStatement(db).get(sessionId, messageId);
16044
+ const count = typeof row?.count === "number" ? row.count : 0;
16045
+ if (count > 0) {
16046
+ getDeleteIndexedMessageStatement(db).run(sessionId, messageId);
16061
16047
  }
16062
- return {
16063
- triggerPending: result.note_nudge_trigger_pending === 1,
16064
- triggerMessageId: result.note_nudge_trigger_message_id.length > 0 ? result.note_nudge_trigger_message_id : null,
16065
- stickyText: result.note_nudge_sticky_text.length > 0 ? result.note_nudge_sticky_text : null,
16066
- stickyMessageId: result.note_nudge_sticky_message_id.length > 0 ? result.note_nudge_sticky_message_id : null
16067
- };
16048
+ getDeleteIndexStatement(db).run(sessionId);
16049
+ return count;
16068
16050
  }
16069
- function setPersistedNoteNudgeTrigger(db, sessionId, triggerMessageId = "") {
16051
+ function clearIndexedMessages(db, sessionId) {
16070
16052
  db.transaction(() => {
16071
- ensureSessionMetaRow(db, sessionId);
16072
- db.prepare("UPDATE session_meta SET note_nudge_trigger_pending = 1, note_nudge_trigger_message_id = ?, note_nudge_sticky_text = '', note_nudge_sticky_message_id = '' WHERE session_id = ?").run(triggerMessageId, sessionId);
16053
+ getDeleteFtsStatement(db).run(sessionId);
16054
+ getDeleteIndexStatement(db).run(sessionId);
16073
16055
  })();
16074
16056
  }
16075
- function setPersistedNoteNudgeTriggerMessageId(db, sessionId, triggerMessageId) {
16076
- db.transaction(() => {
16077
- ensureSessionMetaRow(db, sessionId);
16078
- db.prepare("UPDATE session_meta SET note_nudge_trigger_message_id = ? WHERE session_id = ?").run(triggerMessageId, sessionId);
16079
- })();
16057
+ function getIndexableContent(role, parts) {
16058
+ if (role === "user") {
16059
+ if (!hasMeaningfulUserText(parts)) {
16060
+ return "";
16061
+ }
16062
+ return extractTexts(parts).map(cleanUserText).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
16063
+ }
16064
+ if (role === "assistant") {
16065
+ return extractTexts(parts).map(removeSystemReminders).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
16066
+ }
16067
+ return "";
16080
16068
  }
16081
- function setPersistedDeliveredNoteNudge(db, sessionId, text, messageId = "") {
16069
+ function ensureMessagesIndexed(db, sessionId, readMessages) {
16070
+ const messages = readMessages(sessionId);
16071
+ if (messages.length === 0) {
16072
+ db.transaction(() => clearIndexedMessages(db, sessionId))();
16073
+ return;
16074
+ }
16075
+ let lastIndexedOrdinal = getLastIndexedOrdinal(db, sessionId);
16076
+ if (lastIndexedOrdinal > messages.length) {
16077
+ db.transaction(() => clearIndexedMessages(db, sessionId))();
16078
+ lastIndexedOrdinal = 0;
16079
+ }
16080
+ if (lastIndexedOrdinal >= messages.length) {
16081
+ return;
16082
+ }
16083
+ const messagesToInsert = messages.filter((message) => message.ordinal > lastIndexedOrdinal).filter((message) => message.role === "user" || message.role === "assistant").map((message) => ({
16084
+ ordinal: message.ordinal,
16085
+ id: message.id,
16086
+ role: message.role,
16087
+ content: getIndexableContent(message.role, message.parts)
16088
+ })).filter((message) => message.content.length > 0);
16089
+ const now = Date.now();
16082
16090
  db.transaction(() => {
16083
- ensureSessionMetaRow(db, sessionId);
16084
- db.prepare("UPDATE session_meta SET note_nudge_trigger_pending = 0, note_nudge_trigger_message_id = '', note_nudge_sticky_text = ?, note_nudge_sticky_message_id = ? WHERE session_id = ?").run(text, messageId, sessionId);
16091
+ const insertMessage = getInsertMessageStatement(db);
16092
+ for (const message of messagesToInsert) {
16093
+ insertMessage.run(sessionId, message.ordinal, message.id, message.role, message.content);
16094
+ }
16095
+ getUpsertIndexStatement(db).run(sessionId, messages.length, now);
16085
16096
  })();
16086
16097
  }
16087
- function clearPersistedNoteNudge(db, sessionId) {
16088
- db.prepare("UPDATE session_meta SET note_nudge_trigger_pending = 0, note_nudge_trigger_message_id = '', note_nudge_sticky_text = '', note_nudge_sticky_message_id = '' WHERE session_id = ?").run(sessionId);
16089
- }
16090
- function getStrippedPlaceholderIds(db, sessionId) {
16091
- const row = db.prepare("SELECT stripped_placeholder_ids FROM session_meta WHERE session_id = ?").get(sessionId);
16092
- const raw = row?.stripped_placeholder_ids;
16093
- if (!raw || raw.length === 0)
16094
- return new Set;
16095
- try {
16096
- const parsed = JSON.parse(raw);
16097
- if (Array.isArray(parsed))
16098
- return new Set(parsed.filter((v) => typeof v === "string"));
16099
- } catch {}
16100
- return new Set;
16101
- }
16102
- function setStrippedPlaceholderIds(db, sessionId, ids) {
16103
- ensureSessionMetaRow(db, sessionId);
16104
- const json2 = ids.size > 0 ? JSON.stringify([...ids]) : "";
16105
- db.prepare("UPDATE session_meta SET stripped_placeholder_ids = ? WHERE session_id = ?").run(json2, sessionId);
16106
- }
16107
- // src/shared/internal-initiator-marker.ts
16108
- var OMO_INTERNAL_INITIATOR_MARKER = "<!-- OMO_INTERNAL_INITIATOR -->";
16109
-
16110
- // src/shared/system-directive.ts
16111
- var SYSTEM_DIRECTIVE_PREFIX = "[SYSTEM DIRECTIVE: MAGIC-CONTEXT";
16112
- function isSystemDirective(text) {
16113
- return text.trimStart().startsWith(SYSTEM_DIRECTIVE_PREFIX);
16114
- }
16115
- function removeSystemReminders(text) {
16116
- return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, "").trim();
16117
- }
16118
-
16119
- // src/hooks/magic-context/read-session-db.ts
16098
+ // src/features/magic-context/storage-db.ts
16120
16099
  import { Database as Database2 } from "bun:sqlite";
16100
+ import { mkdirSync } from "fs";
16121
16101
  import { join as join6 } from "path";
16122
- function getOpenCodeDbPath() {
16123
- return join6(getDataDir(), "opencode", "opencode.db");
16102
+ var databases = new Map;
16103
+ var FALLBACK_DATABASE_KEY = "__fallback__:memory:";
16104
+ var persistenceByDatabase = new WeakMap;
16105
+ var persistenceErrorByDatabase = new WeakMap;
16106
+ function resolveDatabasePath() {
16107
+ const dbDir = join6(getOpenCodeStorageDir(), "plugin", "magic-context");
16108
+ return { dbDir, dbPath: join6(dbDir, "context.db") };
16124
16109
  }
16125
- var cachedReadOnlyDb = null;
16126
- function closeCachedReadOnlyDb() {
16127
- if (!cachedReadOnlyDb) {
16128
- return;
16129
- }
16130
- try {
16131
- cachedReadOnlyDb.db.close(false);
16132
- } catch (error48) {
16133
- log("[magic-context] failed to close cached OpenCode read-only DB:", error48);
16134
- } finally {
16135
- cachedReadOnlyDb = null;
16136
- }
16110
+ function initializeDatabase(db) {
16111
+ db.run("PRAGMA journal_mode=WAL");
16112
+ db.run("PRAGMA busy_timeout=5000");
16113
+ db.run("PRAGMA foreign_keys=ON");
16114
+ db.run(`
16115
+ CREATE TABLE IF NOT EXISTS tags (
16116
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16117
+ session_id TEXT,
16118
+ message_id TEXT,
16119
+ type TEXT,
16120
+ status TEXT DEFAULT 'active',
16121
+ byte_size INTEGER,
16122
+ tag_number INTEGER,
16123
+ UNIQUE(session_id, tag_number)
16124
+ );
16125
+
16126
+ CREATE TABLE IF NOT EXISTS pending_ops (
16127
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16128
+ session_id TEXT,
16129
+ tag_id INTEGER,
16130
+ operation TEXT,
16131
+ queued_at INTEGER
16132
+ );
16133
+
16134
+ CREATE TABLE IF NOT EXISTS source_contents (
16135
+ tag_id INTEGER,
16136
+ session_id TEXT,
16137
+ content TEXT,
16138
+ created_at INTEGER,
16139
+ PRIMARY KEY(session_id, tag_id)
16140
+ );
16141
+
16142
+ CREATE TABLE IF NOT EXISTS compartments (
16143
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16144
+ session_id TEXT NOT NULL,
16145
+ sequence INTEGER NOT NULL,
16146
+ start_message INTEGER NOT NULL,
16147
+ end_message INTEGER NOT NULL,
16148
+ start_message_id TEXT DEFAULT '',
16149
+ end_message_id TEXT DEFAULT '',
16150
+ title TEXT NOT NULL,
16151
+ content TEXT NOT NULL,
16152
+ created_at INTEGER NOT NULL,
16153
+ UNIQUE(session_id, sequence)
16154
+ );
16155
+
16156
+ CREATE TABLE IF NOT EXISTS session_facts (
16157
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16158
+ session_id TEXT NOT NULL,
16159
+ category TEXT NOT NULL,
16160
+ content TEXT NOT NULL,
16161
+ created_at INTEGER NOT NULL,
16162
+ updated_at INTEGER NOT NULL
16163
+ );
16164
+
16165
+ CREATE TABLE IF NOT EXISTS session_notes (
16166
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16167
+ session_id TEXT NOT NULL,
16168
+ content TEXT NOT NULL,
16169
+ created_at INTEGER NOT NULL
16170
+ );
16171
+
16172
+ CREATE TABLE IF NOT EXISTS memories (
16173
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16174
+ project_path TEXT NOT NULL,
16175
+ category TEXT NOT NULL,
16176
+ content TEXT NOT NULL,
16177
+ normalized_hash TEXT NOT NULL,
16178
+ source_session_id TEXT,
16179
+ source_type TEXT DEFAULT 'historian',
16180
+ seen_count INTEGER DEFAULT 1,
16181
+ retrieval_count INTEGER DEFAULT 0,
16182
+ first_seen_at INTEGER NOT NULL,
16183
+ created_at INTEGER NOT NULL,
16184
+ updated_at INTEGER NOT NULL,
16185
+ last_seen_at INTEGER NOT NULL,
16186
+ last_retrieved_at INTEGER,
16187
+ status TEXT DEFAULT 'active',
16188
+ expires_at INTEGER,
16189
+ verification_status TEXT DEFAULT 'unverified',
16190
+ verified_at INTEGER,
16191
+ superseded_by_memory_id INTEGER,
16192
+ merged_from TEXT,
16193
+ metadata_json TEXT,
16194
+ UNIQUE(project_path, category, normalized_hash)
16195
+ );
16196
+
16197
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
16198
+ memory_id INTEGER PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
16199
+ embedding BLOB NOT NULL,
16200
+ model_id TEXT
16201
+ );
16202
+
16203
+ CREATE TABLE IF NOT EXISTS dream_state (
16204
+ key TEXT PRIMARY KEY,
16205
+ value TEXT NOT NULL
16206
+ );
16207
+
16208
+ CREATE TABLE IF NOT EXISTS dream_queue (
16209
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16210
+ project_path TEXT NOT NULL,
16211
+ reason TEXT NOT NULL,
16212
+ enqueued_at INTEGER NOT NULL,
16213
+ started_at INTEGER,
16214
+ retry_count INTEGER DEFAULT 0
16215
+ );
16216
+ CREATE INDEX IF NOT EXISTS idx_dream_queue_project ON dream_queue(project_path);
16217
+ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, enqueued_at);
16218
+
16219
+ CREATE TABLE IF NOT EXISTS smart_notes (
16220
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16221
+ project_path TEXT NOT NULL,
16222
+ content TEXT NOT NULL,
16223
+ surface_condition TEXT NOT NULL,
16224
+ status TEXT NOT NULL DEFAULT 'pending',
16225
+ created_session_id TEXT,
16226
+ created_at INTEGER NOT NULL,
16227
+ updated_at INTEGER NOT NULL,
16228
+ last_checked_at INTEGER,
16229
+ ready_at INTEGER,
16230
+ ready_reason TEXT
16231
+ );
16232
+ CREATE INDEX IF NOT EXISTS idx_smart_notes_project_status ON smart_notes(project_path, status);
16233
+
16234
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
16235
+ content,
16236
+ category,
16237
+ content='memories',
16238
+ content_rowid='id',
16239
+ tokenize='porter unicode61'
16240
+ );
16241
+
16242
+ CREATE VIRTUAL TABLE IF NOT EXISTS message_history_fts USING fts5(
16243
+ session_id UNINDEXED,
16244
+ message_ordinal UNINDEXED,
16245
+ message_id UNINDEXED,
16246
+ role,
16247
+ content,
16248
+ tokenize='porter unicode61'
16249
+ );
16250
+
16251
+ CREATE TABLE IF NOT EXISTS message_history_index (
16252
+ session_id TEXT PRIMARY KEY,
16253
+ last_indexed_ordinal INTEGER NOT NULL DEFAULT 0,
16254
+ updated_at INTEGER NOT NULL
16255
+ );
16256
+
16257
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
16258
+ INSERT INTO memories_fts(rowid, content, category) VALUES (new.id, new.content, new.category);
16259
+ END;
16260
+
16261
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
16262
+ INSERT INTO memories_fts(memories_fts, rowid, content, category) VALUES ('delete', old.id, old.content, old.category);
16263
+ END;
16264
+
16265
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
16266
+ INSERT INTO memories_fts(memories_fts, rowid, content, category) VALUES ('delete', old.id, old.content, old.category);
16267
+ INSERT INTO memories_fts(rowid, content, category) VALUES (new.id, new.content, new.category);
16268
+ END;
16269
+
16270
+ CREATE TABLE IF NOT EXISTS session_meta (
16271
+ session_id TEXT PRIMARY KEY,
16272
+ last_response_time INTEGER,
16273
+ cache_ttl TEXT,
16274
+ counter INTEGER DEFAULT 0,
16275
+ last_nudge_tokens INTEGER DEFAULT 0,
16276
+ last_nudge_band TEXT DEFAULT '',
16277
+ last_transform_error TEXT DEFAULT '',
16278
+ nudge_anchor_message_id TEXT DEFAULT '',
16279
+ nudge_anchor_text TEXT DEFAULT '',
16280
+ sticky_turn_reminder_text TEXT DEFAULT '',
16281
+ sticky_turn_reminder_message_id TEXT DEFAULT '',
16282
+ note_nudge_trigger_pending INTEGER DEFAULT 0,
16283
+ note_nudge_trigger_message_id TEXT DEFAULT '',
16284
+ note_nudge_sticky_text TEXT DEFAULT '',
16285
+ note_nudge_sticky_message_id TEXT DEFAULT '',
16286
+ is_subagent INTEGER DEFAULT 0,
16287
+ last_context_percentage REAL DEFAULT 0,
16288
+ last_input_tokens INTEGER DEFAULT 0,
16289
+ times_execute_threshold_reached INTEGER DEFAULT 0,
16290
+ compartment_in_progress INTEGER DEFAULT 0,
16291
+ system_prompt_hash TEXT DEFAULT '',
16292
+ memory_block_cache TEXT DEFAULT '',
16293
+ memory_block_count INTEGER DEFAULT 0
16294
+ );
16295
+
16296
+ CREATE INDEX IF NOT EXISTS idx_tags_session_tag_number ON tags(session_id, tag_number);
16297
+ CREATE INDEX IF NOT EXISTS idx_pending_ops_session ON pending_ops(session_id);
16298
+ CREATE INDEX IF NOT EXISTS idx_pending_ops_session_tag_id ON pending_ops(session_id, tag_id);
16299
+ CREATE INDEX IF NOT EXISTS idx_source_contents_session ON source_contents(session_id);
16300
+
16301
+ CREATE TABLE IF NOT EXISTS recomp_compartments (
16302
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16303
+ session_id TEXT NOT NULL,
16304
+ sequence INTEGER NOT NULL,
16305
+ start_message INTEGER NOT NULL,
16306
+ end_message INTEGER NOT NULL,
16307
+ start_message_id TEXT DEFAULT '',
16308
+ end_message_id TEXT DEFAULT '',
16309
+ title TEXT NOT NULL,
16310
+ content TEXT NOT NULL,
16311
+ pass_number INTEGER NOT NULL,
16312
+ created_at INTEGER NOT NULL,
16313
+ UNIQUE(session_id, sequence)
16314
+ );
16315
+
16316
+ CREATE TABLE IF NOT EXISTS recomp_facts (
16317
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16318
+ session_id TEXT NOT NULL,
16319
+ category TEXT NOT NULL,
16320
+ content TEXT NOT NULL,
16321
+ pass_number INTEGER NOT NULL,
16322
+ created_at INTEGER NOT NULL
16323
+ );
16324
+
16325
+ CREATE INDEX IF NOT EXISTS idx_compartments_session ON compartments(session_id);
16326
+ CREATE INDEX IF NOT EXISTS idx_session_facts_session ON session_facts(session_id);
16327
+ CREATE INDEX IF NOT EXISTS idx_recomp_compartments_session ON recomp_compartments(session_id);
16328
+ CREATE INDEX IF NOT EXISTS idx_recomp_facts_session ON recomp_facts(session_id);
16329
+ CREATE INDEX IF NOT EXISTS idx_session_notes_session ON session_notes(session_id);
16330
+ CREATE INDEX IF NOT EXISTS idx_memories_project_status_category ON memories(project_path, status, category);
16331
+ CREATE INDEX IF NOT EXISTS idx_memories_project_status_expires ON memories(project_path, status, expires_at);
16332
+ CREATE INDEX IF NOT EXISTS idx_memories_project_category_hash ON memories(project_path, category, normalized_hash);
16333
+ CREATE INDEX IF NOT EXISTS idx_message_history_index_updated_at ON message_history_index(updated_at);
16334
+ `);
16335
+ ensureColumn(db, "session_meta", "last_nudge_band", "TEXT DEFAULT ''");
16336
+ ensureColumn(db, "session_meta", "last_transform_error", "TEXT DEFAULT ''");
16337
+ ensureColumn(db, "session_meta", "nudge_anchor_message_id", "TEXT DEFAULT ''");
16338
+ ensureColumn(db, "session_meta", "nudge_anchor_text", "TEXT DEFAULT ''");
16339
+ ensureColumn(db, "session_meta", "sticky_turn_reminder_text", "TEXT DEFAULT ''");
16340
+ ensureColumn(db, "session_meta", "sticky_turn_reminder_message_id", "TEXT DEFAULT ''");
16341
+ ensureColumn(db, "session_meta", "note_nudge_trigger_pending", "INTEGER DEFAULT 0");
16342
+ ensureColumn(db, "session_meta", "note_nudge_trigger_message_id", "TEXT DEFAULT ''");
16343
+ ensureColumn(db, "session_meta", "note_nudge_sticky_text", "TEXT DEFAULT ''");
16344
+ ensureColumn(db, "session_meta", "note_nudge_sticky_message_id", "TEXT DEFAULT ''");
16345
+ ensureColumn(db, "session_meta", "times_execute_threshold_reached", "INTEGER DEFAULT 0");
16346
+ ensureColumn(db, "session_meta", "compartment_in_progress", "INTEGER DEFAULT 0");
16347
+ ensureColumn(db, "session_meta", "system_prompt_hash", "TEXT DEFAULT ''");
16348
+ ensureColumn(db, "session_meta", "cleared_reasoning_through_tag", "INTEGER DEFAULT 0");
16349
+ ensureColumn(db, "session_meta", "stripped_placeholder_ids", "TEXT DEFAULT ''");
16350
+ ensureColumn(db, "compartments", "start_message_id", "TEXT DEFAULT ''");
16351
+ ensureColumn(db, "compartments", "end_message_id", "TEXT DEFAULT ''");
16352
+ ensureColumn(db, "memory_embeddings", "model_id", "TEXT");
16353
+ ensureColumn(db, "session_meta", "memory_block_cache", "TEXT DEFAULT ''");
16354
+ ensureColumn(db, "session_meta", "memory_block_count", "INTEGER DEFAULT 0");
16355
+ ensureColumn(db, "dream_queue", "retry_count", "INTEGER DEFAULT 0");
16137
16356
  }
16138
- function getReadOnlySessionDb() {
16139
- const dbPath = getOpenCodeDbPath();
16140
- if (cachedReadOnlyDb?.path === dbPath) {
16141
- return cachedReadOnlyDb.db;
16357
+ function ensureColumn(db, table, column, definition) {
16358
+ if (!/^[a-z_]+$/.test(table) || !/^[a-z_]+$/.test(column) || !/^[A-Z0-9_'(),\s]+$/i.test(definition)) {
16359
+ throw new Error(`Unsafe schema identifier: ${table}.${column} ${definition}`);
16142
16360
  }
16143
- closeCachedReadOnlyDb();
16144
- const db = new Database2(dbPath, { readonly: true });
16145
- cachedReadOnlyDb = { path: dbPath, db };
16146
- return db;
16147
- }
16148
- function withReadOnlySessionDb(fn) {
16149
- return fn(getReadOnlySessionDb());
16150
- }
16151
- function getRawSessionMessageCountFromDb(db, sessionId) {
16152
- const row = db.prepare("SELECT COUNT(*) as count FROM message WHERE session_id = ?").get(sessionId);
16153
- return typeof row?.count === "number" ? row.count : 0;
16154
- }
16155
-
16156
- // src/hooks/magic-context/read-session-formatting.ts
16157
- var COMMIT_HASH_PATTERN = /`?\b([0-9a-f]{6,12})\b`?/gi;
16158
- var COMMIT_HINT_PATTERN = /\b(commit(?:ted)?|cherry-?pick(?:ed)?|hash(?:es)?|sha)\b/i;
16159
- var MAX_COMMITS_PER_BLOCK = 5;
16160
- function hasMeaningfulUserText(parts) {
16161
- for (const part of parts) {
16162
- if (part === null || typeof part !== "object")
16163
- continue;
16164
- const candidate = part;
16165
- if (candidate.type !== "text" || typeof candidate.text !== "string")
16166
- continue;
16167
- if (candidate.ignored === true)
16168
- continue;
16169
- const cleaned = removeSystemReminders(candidate.text).replace(OMO_INTERNAL_INITIATOR_MARKER, "").trim();
16170
- if (!cleaned)
16171
- continue;
16172
- if (isSystemDirective(cleaned))
16173
- continue;
16174
- return true;
16361
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all();
16362
+ if (rows.some((row) => row.name === column)) {
16363
+ return;
16175
16364
  }
16176
- return false;
16365
+ db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
16177
16366
  }
16178
- function extractTexts(parts) {
16179
- const texts = [];
16180
- for (const part of parts) {
16181
- if (part === null || typeof part !== "object")
16182
- continue;
16183
- const p = part;
16184
- if (p.type === "text" && typeof p.text === "string" && p.text.trim().length > 0) {
16185
- texts.push(p.text.trim());
16367
+ function createFallbackDatabase() {
16368
+ try {
16369
+ const fallback = new Database2(":memory:");
16370
+ initializeDatabase(fallback);
16371
+ return fallback;
16372
+ } catch (error48) {
16373
+ throw new Error(`[magic-context] storage fatal: failed to initialize fallback database: ${getErrorMessage(error48)}`);
16374
+ }
16375
+ }
16376
+ function openDatabase() {
16377
+ try {
16378
+ const { dbDir, dbPath } = resolveDatabasePath();
16379
+ const existing = databases.get(dbPath);
16380
+ if (existing) {
16381
+ if (!persistenceByDatabase.has(existing)) {
16382
+ persistenceByDatabase.set(existing, true);
16383
+ }
16384
+ return existing;
16385
+ }
16386
+ mkdirSync(dbDir, { recursive: true });
16387
+ const db = new Database2(dbPath);
16388
+ initializeDatabase(db);
16389
+ databases.set(dbPath, db);
16390
+ persistenceByDatabase.set(db, true);
16391
+ persistenceErrorByDatabase.delete(db);
16392
+ return db;
16393
+ } catch (error48) {
16394
+ log("[magic-context] storage error:", error48);
16395
+ const errorMessage = getErrorMessage(error48);
16396
+ const existingFallback = databases.get(FALLBACK_DATABASE_KEY);
16397
+ if (existingFallback) {
16398
+ if (!persistenceByDatabase.has(existingFallback)) {
16399
+ persistenceByDatabase.set(existingFallback, false);
16400
+ persistenceErrorByDatabase.set(existingFallback, errorMessage);
16401
+ }
16402
+ return existingFallback;
16186
16403
  }
16404
+ const fallback = createFallbackDatabase();
16405
+ databases.set(FALLBACK_DATABASE_KEY, fallback);
16406
+ persistenceByDatabase.set(fallback, false);
16407
+ persistenceErrorByDatabase.set(fallback, errorMessage);
16408
+ return fallback;
16187
16409
  }
16188
- return texts;
16189
16410
  }
16190
- function estimateTokens(text) {
16191
- return Math.ceil(text.length / 3.5);
16411
+ function isDatabasePersisted(db) {
16412
+ return persistenceByDatabase.get(db) ?? false;
16192
16413
  }
16193
- function normalizeText(text) {
16194
- return text.replace(/\s+/g, " ").trim();
16414
+ function getDatabasePersistenceError(db) {
16415
+ return persistenceErrorByDatabase.get(db) ?? null;
16195
16416
  }
16196
- function compactRole(role) {
16197
- if (role === "assistant")
16198
- return "A";
16199
- if (role === "user")
16200
- return "U";
16201
- return role.slice(0, 1).toUpperCase() || "M";
16417
+ // src/features/magic-context/storage-meta-shared.ts
16418
+ var META_COLUMNS = {
16419
+ lastResponseTime: "last_response_time",
16420
+ cacheTtl: "cache_ttl",
16421
+ counter: "counter",
16422
+ lastNudgeTokens: "last_nudge_tokens",
16423
+ lastNudgeBand: "last_nudge_band",
16424
+ lastTransformError: "last_transform_error",
16425
+ isSubagent: "is_subagent",
16426
+ lastContextPercentage: "last_context_percentage",
16427
+ lastInputTokens: "last_input_tokens",
16428
+ timesExecuteThresholdReached: "times_execute_threshold_reached",
16429
+ compartmentInProgress: "compartment_in_progress",
16430
+ systemPromptHash: "system_prompt_hash",
16431
+ clearedReasoningThroughTag: "cleared_reasoning_through_tag"
16432
+ };
16433
+ var BOOLEAN_META_KEYS = new Set(["isSubagent", "compartmentInProgress"]);
16434
+ function isSessionMetaRow(row) {
16435
+ if (row === null || typeof row !== "object")
16436
+ return false;
16437
+ const r = row;
16438
+ return typeof r.session_id === "string" && typeof r.last_response_time === "number" && typeof r.cache_ttl === "string" && typeof r.counter === "number" && typeof r.last_nudge_tokens === "number" && typeof r.last_nudge_band === "string" && typeof r.last_transform_error === "string" && typeof r.is_subagent === "number" && typeof r.last_context_percentage === "number" && typeof r.last_input_tokens === "number" && typeof r.times_execute_threshold_reached === "number" && typeof r.compartment_in_progress === "number" && (typeof r.system_prompt_hash === "string" || typeof r.system_prompt_hash === "number") && typeof r.cleared_reasoning_through_tag === "number";
16202
16439
  }
16203
- function formatBlock(block) {
16204
- const range = block.startOrdinal === block.endOrdinal ? `[${block.startOrdinal}]` : `[${block.startOrdinal}-${block.endOrdinal}]`;
16205
- const commitSuffix = block.commitHashes.length > 0 ? ` commits: ${block.commitHashes.join(", ")}` : "";
16206
- return `${range} ${block.role}:${commitSuffix} ${block.parts.join(" / ")}`;
16440
+ function getDefaultSessionMeta(sessionId) {
16441
+ return {
16442
+ sessionId,
16443
+ lastResponseTime: 0,
16444
+ cacheTtl: "5m",
16445
+ counter: 0,
16446
+ lastNudgeTokens: 0,
16447
+ lastNudgeBand: null,
16448
+ lastTransformError: null,
16449
+ isSubagent: false,
16450
+ lastContextPercentage: 0,
16451
+ lastInputTokens: 0,
16452
+ timesExecuteThresholdReached: 0,
16453
+ compartmentInProgress: false,
16454
+ systemPromptHash: "",
16455
+ clearedReasoningThroughTag: 0
16456
+ };
16207
16457
  }
16208
- function extractCommitHashes(text) {
16209
- const hashes = [];
16210
- const seen = new Set;
16211
- for (const match of text.matchAll(COMMIT_HASH_PATTERN)) {
16212
- const hash2 = match[1]?.toLowerCase();
16213
- if (!hash2 || seen.has(hash2))
16214
- continue;
16215
- seen.add(hash2);
16216
- hashes.push(hash2);
16217
- if (hashes.length >= MAX_COMMITS_PER_BLOCK)
16218
- break;
16219
- }
16220
- return hashes;
16458
+ function ensureSessionMetaRow(db, sessionId) {
16459
+ const defaults = getDefaultSessionMeta(sessionId);
16460
+ db.prepare("INSERT OR IGNORE INTO session_meta (session_id, last_response_time, cache_ttl, counter, last_nudge_tokens, last_nudge_band, last_transform_error, is_subagent, last_context_percentage, last_input_tokens, times_execute_threshold_reached, compartment_in_progress, system_prompt_hash, cleared_reasoning_through_tag) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(sessionId, defaults.lastResponseTime, defaults.cacheTtl, defaults.counter, defaults.lastNudgeTokens, defaults.lastNudgeBand ?? "", defaults.lastTransformError ?? "", defaults.isSubagent ? 1 : 0, defaults.lastContextPercentage, defaults.lastInputTokens, defaults.timesExecuteThresholdReached, defaults.compartmentInProgress ? 1 : 0, defaults.systemPromptHash ?? "", defaults.clearedReasoningThroughTag);
16221
16461
  }
16222
- function compactTextForSummary(text, role) {
16223
- const commitHashes = role === "assistant" ? extractCommitHashes(text) : [];
16224
- if (commitHashes.length === 0 || !COMMIT_HINT_PATTERN.test(text)) {
16225
- return { text, commitHashes };
16226
- }
16227
- const withoutHashes = text.replace(COMMIT_HASH_PATTERN, "").replace(/\(\s*\)/g, "").replace(/\s+,/g, ",").replace(/,\s*,+/g, ", ").replace(/\s{2,}/g, " ").replace(/\s+([,.;:])/g, "$1").trim();
16462
+ function toSessionMeta(row) {
16228
16463
  return {
16229
- text: withoutHashes.length > 0 ? withoutHashes : text,
16230
- commitHashes
16464
+ sessionId: row.session_id,
16465
+ lastResponseTime: row.last_response_time,
16466
+ cacheTtl: row.cache_ttl,
16467
+ counter: row.counter,
16468
+ lastNudgeTokens: row.last_nudge_tokens,
16469
+ lastNudgeBand: row.last_nudge_band.length > 0 ? row.last_nudge_band : null,
16470
+ lastTransformError: row.last_transform_error.length > 0 ? row.last_transform_error : null,
16471
+ isSubagent: row.is_subagent === 1,
16472
+ lastContextPercentage: row.last_context_percentage,
16473
+ lastInputTokens: row.last_input_tokens,
16474
+ timesExecuteThresholdReached: row.times_execute_threshold_reached,
16475
+ compartmentInProgress: row.compartment_in_progress === 1,
16476
+ systemPromptHash: String(row.system_prompt_hash),
16477
+ clearedReasoningThroughTag: row.cleared_reasoning_through_tag
16231
16478
  };
16232
16479
  }
16233
- function mergeCommitHashes(existing, next) {
16234
- if (next.length === 0)
16235
- return existing;
16236
- const merged = [...existing];
16237
- for (const hash2 of next) {
16238
- if (merged.includes(hash2))
16239
- continue;
16240
- merged.push(hash2);
16241
- if (merged.length >= MAX_COMMITS_PER_BLOCK)
16242
- break;
16243
- }
16244
- return merged;
16245
- }
16246
16480
 
16247
- // src/hooks/magic-context/read-session-raw.ts
16248
- function isRawMessageRow(row) {
16481
+ // src/features/magic-context/storage-meta-persisted.ts
16482
+ function isPersistedUsageRow(row) {
16249
16483
  if (row === null || typeof row !== "object")
16250
16484
  return false;
16251
- const candidate = row;
16252
- return typeof candidate.id === "string" && typeof candidate.data === "string";
16485
+ const r = row;
16486
+ return typeof r.last_context_percentage === "number" && typeof r.last_input_tokens === "number" && typeof r.last_response_time === "number";
16253
16487
  }
16254
- function isRawPartRow(row) {
16488
+ function isPersistedReasoningWatermarkRow(row) {
16255
16489
  if (row === null || typeof row !== "object")
16256
16490
  return false;
16257
- const candidate = row;
16258
- return typeof candidate.message_id === "string" && typeof candidate.data === "string";
16259
- }
16260
- function parseJsonRecord(value) {
16261
- const parsed = JSON.parse(value);
16262
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
16263
- throw new Error("Expected JSON object");
16264
- }
16265
- return parsed;
16266
- }
16267
- function parseJsonUnknown(value) {
16268
- return JSON.parse(value);
16269
- }
16270
- function readRawSessionMessagesFromDb(db, sessionId) {
16271
- const messageRows = db.prepare("SELECT id, data FROM message WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId).filter(isRawMessageRow);
16272
- const partRows = db.prepare("SELECT message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId).filter(isRawPartRow);
16273
- const partsByMessageId = new Map;
16274
- for (const part of partRows) {
16275
- const list = partsByMessageId.get(part.message_id) ?? [];
16276
- list.push(parseJsonUnknown(part.data));
16277
- partsByMessageId.set(part.message_id, list);
16278
- }
16279
- return messageRows.map((row, index) => {
16280
- const info = parseJsonRecord(row.data);
16281
- const role = typeof info.role === "string" ? info.role : "unknown";
16282
- return {
16283
- ordinal: index + 1,
16284
- id: row.id,
16285
- role,
16286
- parts: partsByMessageId.get(row.id) ?? []
16287
- };
16288
- });
16289
- }
16290
-
16291
- // src/hooks/magic-context/tag-content-primitives.ts
16292
- var encoder = new TextEncoder;
16293
- var TAG_PREFIX_REGEX = /^\u00A7\d+\u00A7\s*/;
16294
- function byteSize(value) {
16295
- return encoder.encode(value).length;
16296
- }
16297
- function stripTagPrefix(value) {
16298
- return value.replace(TAG_PREFIX_REGEX, "");
16299
- }
16300
- function prependTag(tagId, value) {
16301
- const stripped = stripTagPrefix(value);
16302
- return `\xA7${tagId}\xA7 ${stripped}`;
16303
- }
16304
- function isThinkingPart(part) {
16305
- if (part === null || typeof part !== "object")
16306
- return false;
16307
- const candidate = part;
16308
- return candidate.type === "thinking" || candidate.type === "reasoning";
16309
- }
16310
-
16311
- // src/hooks/magic-context/tag-part-guards.ts
16312
- function isTextPart(part) {
16313
- if (part === null || typeof part !== "object")
16314
- return false;
16315
- const p = part;
16316
- return p.type === "text" && typeof p.text === "string";
16317
- }
16318
- function isToolPartWithOutput(part) {
16319
- if (part === null || typeof part !== "object")
16320
- return false;
16321
- const p = part;
16322
- if (p.type !== "tool" || typeof p.callID !== "string")
16323
- return false;
16324
- if (p.state === null || typeof p.state !== "object")
16325
- return false;
16326
- return typeof p.state.output === "string";
16327
- }
16328
- function isFilePart(part) {
16329
- if (part === null || typeof part !== "object")
16330
- return false;
16331
- const p = part;
16332
- return p.type === "file" && typeof p.url === "string";
16333
- }
16334
- function buildFileSourceContent(parts) {
16335
- const content = parts.filter(isTextPart).map((part) => stripTagPrefix(part.text)).join(`
16336
- `).trim();
16337
- return content.length > 0 ? content : null;
16491
+ const r = row;
16492
+ return typeof r.cleared_reasoning_through_tag === "number";
16338
16493
  }
16339
-
16340
- // src/hooks/magic-context/read-session-chunk.ts
16341
- var activeRawMessageCache = null;
16342
- function cleanUserText(text) {
16343
- return removeSystemReminders(text).replace(OMO_INTERNAL_INITIATOR_MARKER, "").trim();
16494
+ function isPersistedNudgePlacementRow(row) {
16495
+ if (row === null || typeof row !== "object")
16496
+ return false;
16497
+ const r = row;
16498
+ return typeof r.nudge_anchor_message_id === "string" && typeof r.nudge_anchor_text === "string";
16344
16499
  }
16345
- function withRawSessionMessageCache(fn) {
16346
- const outerCache = activeRawMessageCache;
16347
- if (!outerCache) {
16348
- activeRawMessageCache = new Map;
16349
- }
16350
- try {
16351
- return fn();
16352
- } finally {
16353
- if (!outerCache) {
16354
- activeRawMessageCache = null;
16355
- }
16356
- }
16500
+ function isPersistedStickyTurnReminderRow(row) {
16501
+ if (row === null || typeof row !== "object")
16502
+ return false;
16503
+ const r = row;
16504
+ return typeof r.sticky_turn_reminder_text === "string" && typeof r.sticky_turn_reminder_message_id === "string";
16357
16505
  }
16358
- function readRawSessionMessages(sessionId) {
16359
- if (activeRawMessageCache) {
16360
- const cached2 = activeRawMessageCache.get(sessionId);
16361
- if (cached2) {
16362
- return cached2;
16363
- }
16364
- const messages = withReadOnlySessionDb((db) => readRawSessionMessagesFromDb(db, sessionId));
16365
- activeRawMessageCache.set(sessionId, messages);
16366
- return messages;
16367
- }
16368
- return withReadOnlySessionDb((db) => readRawSessionMessagesFromDb(db, sessionId));
16506
+ function isPersistedNoteNudgeRow(row) {
16507
+ if (row === null || typeof row !== "object")
16508
+ return false;
16509
+ const r = row;
16510
+ return typeof r.note_nudge_trigger_pending === "number" && typeof r.note_nudge_trigger_message_id === "string" && typeof r.note_nudge_sticky_text === "string" && typeof r.note_nudge_sticky_message_id === "string";
16369
16511
  }
16370
- function getRawSessionMessageCount(sessionId) {
16371
- return withReadOnlySessionDb((db) => getRawSessionMessageCountFromDb(db, sessionId));
16512
+ function getDefaultPersistedNoteNudge() {
16513
+ return {
16514
+ triggerPending: false,
16515
+ triggerMessageId: null,
16516
+ stickyText: null,
16517
+ stickyMessageId: null
16518
+ };
16372
16519
  }
16373
- function getRawSessionTagKeysThrough(sessionId, upToMessageIndex) {
16374
- const messages = readRawSessionMessages(sessionId);
16375
- const keys = [];
16376
- for (const message of messages) {
16377
- if (message.ordinal > upToMessageIndex)
16378
- break;
16379
- for (const [partIndex, part] of message.parts.entries()) {
16380
- if (isTextPart(part)) {
16381
- keys.push(`${message.id}:p${partIndex}`);
16382
- }
16383
- if (isFilePart(part)) {
16384
- keys.push(`${message.id}:file${partIndex}`);
16385
- }
16386
- if (isToolPartWithOutput(part)) {
16387
- keys.push(part.callID);
16388
- }
16389
- }
16520
+ function loadPersistedUsage(db, sessionId) {
16521
+ const result = db.prepare("SELECT last_context_percentage, last_input_tokens, last_response_time FROM session_meta WHERE session_id = ?").get(sessionId);
16522
+ if (!isPersistedUsageRow(result) || result.last_context_percentage === 0 && result.last_input_tokens === 0) {
16523
+ return null;
16390
16524
  }
16391
- return keys;
16525
+ return {
16526
+ usage: {
16527
+ percentage: result.last_context_percentage,
16528
+ inputTokens: result.last_input_tokens
16529
+ },
16530
+ updatedAt: result.last_response_time || Date.now()
16531
+ };
16392
16532
  }
16393
- var PROTECTED_TAIL_USER_TURNS = 5;
16394
- function getProtectedTailStartOrdinal(sessionId) {
16395
- const messages = readRawSessionMessages(sessionId);
16396
- const userOrdinals = messages.filter((m) => m.role === "user" && hasMeaningfulUserText(m.parts)).map((m) => m.ordinal);
16397
- if (userOrdinals.length < PROTECTED_TAIL_USER_TURNS) {
16398
- return 1;
16399
- }
16400
- return userOrdinals[userOrdinals.length - PROTECTED_TAIL_USER_TURNS];
16533
+ function getPersistedReasoningWatermark(db, sessionId) {
16534
+ const result = db.prepare("SELECT cleared_reasoning_through_tag FROM session_meta WHERE session_id = ?").get(sessionId);
16535
+ return isPersistedReasoningWatermarkRow(result) ? result.cleared_reasoning_through_tag : 0;
16401
16536
  }
16402
- function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal) {
16403
- const messages = readRawSessionMessages(sessionId);
16404
- const startOrdinal = Math.max(1, offset);
16405
- const lines = [];
16406
- const lineMeta = [];
16407
- let totalTokens = 0;
16408
- let messagesProcessed = 0;
16409
- let lastOrdinal = startOrdinal - 1;
16410
- let lastMessageId = "";
16411
- let firstMessageId = "";
16412
- let currentBlock = null;
16413
- let pendingNoiseMeta = [];
16414
- let commitClusters = 0;
16415
- let lastFlushedRole = "";
16416
- function flushCurrentBlock() {
16417
- if (!currentBlock)
16418
- return true;
16419
- const blockText = formatBlock(currentBlock);
16420
- const blockTokens = estimateTokens(blockText);
16421
- if (totalTokens + blockTokens > tokenBudget && totalTokens > 0) {
16422
- return false;
16423
- }
16424
- if (currentBlock.role === "A" && currentBlock.commitHashes.length > 0 && lastFlushedRole !== "A") {
16425
- commitClusters++;
16426
- }
16427
- lastFlushedRole = currentBlock.role;
16428
- if (!firstMessageId)
16429
- firstMessageId = currentBlock.meta[0]?.messageId ?? "";
16430
- lastOrdinal = currentBlock.meta[currentBlock.meta.length - 1]?.ordinal ?? currentBlock.endOrdinal;
16431
- lastMessageId = currentBlock.meta[currentBlock.meta.length - 1]?.messageId ?? "";
16432
- messagesProcessed += currentBlock.meta.length;
16433
- lines.push(blockText);
16434
- lineMeta.push(...currentBlock.meta);
16435
- totalTokens += blockTokens;
16436
- currentBlock = null;
16437
- return true;
16537
+ function setPersistedReasoningWatermark(db, sessionId, tagNumber) {
16538
+ ensureSessionMetaRow(db, sessionId);
16539
+ db.prepare("UPDATE session_meta SET cleared_reasoning_through_tag = ? WHERE session_id = ?").run(tagNumber, sessionId);
16540
+ }
16541
+ function getPersistedNudgePlacement(db, sessionId) {
16542
+ const result = db.prepare("SELECT nudge_anchor_message_id, nudge_anchor_text FROM session_meta WHERE session_id = ?").get(sessionId);
16543
+ if (!isPersistedNudgePlacementRow(result)) {
16544
+ return null;
16438
16545
  }
16439
- for (const msg of messages) {
16440
- if (eligibleEndOrdinal !== undefined && msg.ordinal >= eligibleEndOrdinal)
16441
- break;
16442
- if (msg.ordinal < startOrdinal)
16443
- continue;
16444
- const meta3 = { ordinal: msg.ordinal, messageId: msg.id };
16445
- if (msg.role === "user" && !hasMeaningfulUserText(msg.parts)) {
16446
- pendingNoiseMeta.push(meta3);
16447
- continue;
16448
- }
16449
- const role = compactRole(msg.role);
16450
- const compacted = compactTextForSummary(extractTexts(msg.parts).map((t) => msg.role === "user" ? cleanUserText(t) : t).map(normalizeText).filter((value) => value.length > 0).join(" / "), msg.role);
16451
- const text = compacted.text;
16452
- if (!text) {
16453
- pendingNoiseMeta.push(meta3);
16454
- continue;
16455
- }
16456
- if (currentBlock && currentBlock.role === role) {
16457
- currentBlock.endOrdinal = msg.ordinal;
16458
- currentBlock.parts.push(text);
16459
- currentBlock.meta.push(...pendingNoiseMeta, meta3);
16460
- currentBlock.commitHashes = mergeCommitHashes(currentBlock.commitHashes, compacted.commitHashes);
16461
- pendingNoiseMeta = [];
16462
- continue;
16463
- }
16464
- if (!flushCurrentBlock())
16465
- break;
16466
- currentBlock = {
16467
- role,
16468
- startOrdinal: pendingNoiseMeta[0]?.ordinal ?? msg.ordinal,
16469
- endOrdinal: msg.ordinal,
16470
- parts: [text],
16471
- meta: [...pendingNoiseMeta, meta3],
16472
- commitHashes: [...compacted.commitHashes]
16473
- };
16474
- pendingNoiseMeta = [];
16546
+ if (result.nudge_anchor_message_id.length === 0 || result.nudge_anchor_text.length === 0) {
16547
+ return null;
16475
16548
  }
16476
- flushCurrentBlock();
16477
16549
  return {
16478
- startIndex: startOrdinal,
16479
- endIndex: lastOrdinal,
16480
- startMessageId: firstMessageId,
16481
- endMessageId: lastMessageId,
16482
- messageCount: messagesProcessed,
16483
- tokenEstimate: totalTokens,
16484
- hasMore: lastOrdinal < (eligibleEndOrdinal !== undefined ? Math.min(eligibleEndOrdinal - 1, messages.length) : messages.length),
16485
- text: lines.join(`
16486
- `),
16487
- lines: lineMeta,
16488
- commitClusterCount: commitClusters
16550
+ messageId: result.nudge_anchor_message_id,
16551
+ nudgeText: result.nudge_anchor_text
16489
16552
  };
16490
16553
  }
16491
-
16492
- // src/features/magic-context/message-index.ts
16493
- var lastIndexedStatements = new WeakMap;
16494
- var insertMessageStatements = new WeakMap;
16495
- var upsertIndexStatements = new WeakMap;
16496
- var deleteFtsStatements = new WeakMap;
16497
- var deleteIndexStatements = new WeakMap;
16498
- function normalizeIndexText(text) {
16499
- return text.replace(/\s+/g, " ").trim();
16554
+ function setPersistedNudgePlacement(db, sessionId, messageId, nudgeText) {
16555
+ db.transaction(() => {
16556
+ ensureSessionMetaRow(db, sessionId);
16557
+ db.prepare("UPDATE session_meta SET nudge_anchor_message_id = ?, nudge_anchor_text = ? WHERE session_id = ?").run(messageId, nudgeText, sessionId);
16558
+ })();
16500
16559
  }
16501
- function getLastIndexedStatement(db) {
16502
- let stmt = lastIndexedStatements.get(db);
16503
- if (!stmt) {
16504
- stmt = db.prepare("SELECT last_indexed_ordinal FROM message_history_index WHERE session_id = ?");
16505
- lastIndexedStatements.set(db, stmt);
16506
- }
16507
- return stmt;
16560
+ function clearPersistedNudgePlacement(db, sessionId) {
16561
+ db.prepare("UPDATE session_meta SET nudge_anchor_message_id = '', nudge_anchor_text = '' WHERE session_id = ?").run(sessionId);
16508
16562
  }
16509
- function getInsertMessageStatement(db) {
16510
- let stmt = insertMessageStatements.get(db);
16511
- if (!stmt) {
16512
- stmt = db.prepare("INSERT INTO message_history_fts (session_id, message_ordinal, message_id, role, content) VALUES (?, ?, ?, ?, ?)");
16513
- insertMessageStatements.set(db, stmt);
16563
+ function getPersistedStickyTurnReminder(db, sessionId) {
16564
+ const result = db.prepare("SELECT sticky_turn_reminder_text, sticky_turn_reminder_message_id FROM session_meta WHERE session_id = ?").get(sessionId);
16565
+ if (!isPersistedStickyTurnReminderRow(result)) {
16566
+ return null;
16514
16567
  }
16515
- return stmt;
16516
- }
16517
- function getUpsertIndexStatement(db) {
16518
- let stmt = upsertIndexStatements.get(db);
16519
- if (!stmt) {
16520
- stmt = db.prepare("INSERT INTO message_history_index (session_id, last_indexed_ordinal, updated_at) VALUES (?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET last_indexed_ordinal = excluded.last_indexed_ordinal, updated_at = excluded.updated_at");
16521
- upsertIndexStatements.set(db, stmt);
16568
+ if (result.sticky_turn_reminder_text.length === 0) {
16569
+ return null;
16522
16570
  }
16523
- return stmt;
16571
+ return {
16572
+ text: result.sticky_turn_reminder_text,
16573
+ messageId: result.sticky_turn_reminder_message_id.length > 0 ? result.sticky_turn_reminder_message_id : null
16574
+ };
16524
16575
  }
16525
- function getDeleteFtsStatement(db) {
16526
- let stmt = deleteFtsStatements.get(db);
16527
- if (!stmt) {
16528
- stmt = db.prepare("DELETE FROM message_history_fts WHERE session_id = ?");
16529
- deleteFtsStatements.set(db, stmt);
16530
- }
16531
- return stmt;
16576
+ function setPersistedStickyTurnReminder(db, sessionId, text, messageId = "") {
16577
+ db.transaction(() => {
16578
+ ensureSessionMetaRow(db, sessionId);
16579
+ db.prepare("UPDATE session_meta SET sticky_turn_reminder_text = ?, sticky_turn_reminder_message_id = ? WHERE session_id = ?").run(text, messageId, sessionId);
16580
+ })();
16532
16581
  }
16533
- function getDeleteIndexStatement(db) {
16534
- let stmt = deleteIndexStatements.get(db);
16535
- if (!stmt) {
16536
- stmt = db.prepare("DELETE FROM message_history_index WHERE session_id = ?");
16537
- deleteIndexStatements.set(db, stmt);
16538
- }
16539
- return stmt;
16582
+ function clearPersistedStickyTurnReminder(db, sessionId) {
16583
+ db.prepare("UPDATE session_meta SET sticky_turn_reminder_text = '', sticky_turn_reminder_message_id = '' WHERE session_id = ?").run(sessionId);
16540
16584
  }
16541
- function getLastIndexedOrdinal(db, sessionId) {
16542
- const row = getLastIndexedStatement(db).get(sessionId);
16543
- return typeof row?.last_indexed_ordinal === "number" ? row.last_indexed_ordinal : 0;
16585
+ function getPersistedNoteNudge(db, sessionId) {
16586
+ const result = db.prepare("SELECT note_nudge_trigger_pending, note_nudge_trigger_message_id, note_nudge_sticky_text, note_nudge_sticky_message_id FROM session_meta WHERE session_id = ?").get(sessionId);
16587
+ if (!isPersistedNoteNudgeRow(result)) {
16588
+ return getDefaultPersistedNoteNudge();
16589
+ }
16590
+ return {
16591
+ triggerPending: result.note_nudge_trigger_pending === 1,
16592
+ triggerMessageId: result.note_nudge_trigger_message_id.length > 0 ? result.note_nudge_trigger_message_id : null,
16593
+ stickyText: result.note_nudge_sticky_text.length > 0 ? result.note_nudge_sticky_text : null,
16594
+ stickyMessageId: result.note_nudge_sticky_message_id.length > 0 ? result.note_nudge_sticky_message_id : null
16595
+ };
16544
16596
  }
16545
- function clearIndexedMessages(db, sessionId) {
16597
+ function setPersistedNoteNudgeTrigger(db, sessionId, triggerMessageId = "") {
16546
16598
  db.transaction(() => {
16547
- getDeleteFtsStatement(db).run(sessionId);
16548
- getDeleteIndexStatement(db).run(sessionId);
16599
+ ensureSessionMetaRow(db, sessionId);
16600
+ db.prepare("UPDATE session_meta SET note_nudge_trigger_pending = 1, note_nudge_trigger_message_id = ?, note_nudge_sticky_text = '', note_nudge_sticky_message_id = '' WHERE session_id = ?").run(triggerMessageId, sessionId);
16549
16601
  })();
16550
16602
  }
16551
- function getIndexableContent(role, parts) {
16552
- if (role === "user") {
16553
- if (!hasMeaningfulUserText(parts)) {
16554
- return "";
16555
- }
16556
- return extractTexts(parts).map(cleanUserText).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
16557
- }
16558
- if (role === "assistant") {
16559
- return extractTexts(parts).map(removeSystemReminders).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
16560
- }
16561
- return "";
16603
+ function setPersistedNoteNudgeTriggerMessageId(db, sessionId, triggerMessageId) {
16604
+ db.transaction(() => {
16605
+ ensureSessionMetaRow(db, sessionId);
16606
+ db.prepare("UPDATE session_meta SET note_nudge_trigger_message_id = ? WHERE session_id = ?").run(triggerMessageId, sessionId);
16607
+ })();
16562
16608
  }
16563
- function ensureMessagesIndexed(db, sessionId, readMessages) {
16564
- const messages = readMessages(sessionId);
16565
- if (messages.length === 0) {
16566
- db.transaction(() => clearIndexedMessages(db, sessionId))();
16567
- return;
16568
- }
16569
- let lastIndexedOrdinal = getLastIndexedOrdinal(db, sessionId);
16570
- if (lastIndexedOrdinal > messages.length) {
16571
- db.transaction(() => clearIndexedMessages(db, sessionId))();
16572
- lastIndexedOrdinal = 0;
16573
- }
16574
- if (lastIndexedOrdinal >= messages.length) {
16575
- return;
16576
- }
16577
- const messagesToInsert = messages.filter((message) => message.ordinal > lastIndexedOrdinal).filter((message) => message.role === "user" || message.role === "assistant").map((message) => ({
16578
- ordinal: message.ordinal,
16579
- id: message.id,
16580
- role: message.role,
16581
- content: getIndexableContent(message.role, message.parts)
16582
- })).filter((message) => message.content.length > 0);
16583
- const now = Date.now();
16609
+ function setPersistedDeliveredNoteNudge(db, sessionId, text, messageId = "") {
16584
16610
  db.transaction(() => {
16585
- const insertMessage = getInsertMessageStatement(db);
16586
- for (const message of messagesToInsert) {
16587
- insertMessage.run(sessionId, message.ordinal, message.id, message.role, message.content);
16588
- }
16589
- getUpsertIndexStatement(db).run(sessionId, messages.length, now);
16611
+ ensureSessionMetaRow(db, sessionId);
16612
+ db.prepare("UPDATE session_meta SET note_nudge_trigger_pending = 0, note_nudge_trigger_message_id = '', note_nudge_sticky_text = ?, note_nudge_sticky_message_id = ? WHERE session_id = ?").run(text, messageId, sessionId);
16590
16613
  })();
16591
16614
  }
16592
-
16615
+ function clearPersistedNoteNudge(db, sessionId) {
16616
+ db.prepare("UPDATE session_meta SET note_nudge_trigger_pending = 0, note_nudge_trigger_message_id = '', note_nudge_sticky_text = '', note_nudge_sticky_message_id = '' WHERE session_id = ?").run(sessionId);
16617
+ }
16618
+ function getStrippedPlaceholderIds(db, sessionId) {
16619
+ const row = db.prepare("SELECT stripped_placeholder_ids FROM session_meta WHERE session_id = ?").get(sessionId);
16620
+ const raw = row?.stripped_placeholder_ids;
16621
+ if (!raw || raw.length === 0)
16622
+ return new Set;
16623
+ try {
16624
+ const parsed = JSON.parse(raw);
16625
+ if (Array.isArray(parsed))
16626
+ return new Set(parsed.filter((v) => typeof v === "string"));
16627
+ } catch {}
16628
+ return new Set;
16629
+ }
16630
+ function setStrippedPlaceholderIds(db, sessionId, ids) {
16631
+ ensureSessionMetaRow(db, sessionId);
16632
+ const json2 = ids.size > 0 ? JSON.stringify([...ids]) : "";
16633
+ db.prepare("UPDATE session_meta SET stripped_placeholder_ids = ? WHERE session_id = ?").run(json2, sessionId);
16634
+ }
16635
+ function removeStrippedPlaceholderId(db, sessionId, messageId) {
16636
+ const ids = getStrippedPlaceholderIds(db, sessionId);
16637
+ if (!ids.delete(messageId)) {
16638
+ return false;
16639
+ }
16640
+ setStrippedPlaceholderIds(db, sessionId, ids);
16641
+ return true;
16642
+ }
16593
16643
  // src/features/magic-context/storage-meta-session.ts
16594
16644
  function getOrCreateSessionMeta(db, sessionId) {
16595
16645
  const result = db.prepare("SELECT session_id, last_response_time, cache_ttl, counter, last_nudge_tokens, last_nudge_band, last_transform_error, is_subagent, last_context_percentage, last_input_tokens, times_execute_threshold_reached, compartment_in_progress, system_prompt_hash, cleared_reasoning_through_tag FROM session_meta WHERE session_id = ?").get(sessionId);
@@ -16755,6 +16805,9 @@ function getSourceContents(db, sessionId, tagIds) {
16755
16805
  var insertTagStatements = new WeakMap;
16756
16806
  var updateTagStatusStatements = new WeakMap;
16757
16807
  var updateTagMessageIdStatements = new WeakMap;
16808
+ var getTagNumbersByMessageIdStatements = new WeakMap;
16809
+ var deleteTagsByMessageIdStatements = new WeakMap;
16810
+ var getMaxTagNumberBySessionStatements = new WeakMap;
16758
16811
  function getInsertTagStatement(db) {
16759
16812
  let stmt = insertTagStatements.get(db);
16760
16813
  if (!stmt) {
@@ -16779,6 +16832,30 @@ function getUpdateTagMessageIdStatement(db) {
16779
16832
  }
16780
16833
  return stmt;
16781
16834
  }
16835
+ function getTagNumbersByMessageIdStatement(db) {
16836
+ let stmt = getTagNumbersByMessageIdStatements.get(db);
16837
+ if (!stmt) {
16838
+ stmt = db.prepare("SELECT tag_number FROM tags WHERE session_id = ? AND (message_id = ? OR message_id LIKE ? ESCAPE '\\' OR message_id LIKE ? ESCAPE '\\') ORDER BY tag_number ASC");
16839
+ getTagNumbersByMessageIdStatements.set(db, stmt);
16840
+ }
16841
+ return stmt;
16842
+ }
16843
+ function getDeleteTagsByMessageIdStatement(db) {
16844
+ let stmt = deleteTagsByMessageIdStatements.get(db);
16845
+ if (!stmt) {
16846
+ stmt = db.prepare("DELETE FROM tags WHERE session_id = ? AND (message_id = ? OR message_id LIKE ? ESCAPE '\\' OR message_id LIKE ? ESCAPE '\\')");
16847
+ deleteTagsByMessageIdStatements.set(db, stmt);
16848
+ }
16849
+ return stmt;
16850
+ }
16851
+ function getMaxTagNumberBySessionStatement(db) {
16852
+ let stmt = getMaxTagNumberBySessionStatements.get(db);
16853
+ if (!stmt) {
16854
+ stmt = db.prepare("SELECT COALESCE(MAX(tag_number), 0) AS max_tag_number FROM tags WHERE session_id = ?");
16855
+ getMaxTagNumberBySessionStatements.set(db, stmt);
16856
+ }
16857
+ return stmt;
16858
+ }
16782
16859
  function isTagRow(row) {
16783
16860
  if (row === null || typeof row !== "object")
16784
16861
  return false;
@@ -16797,6 +16874,21 @@ function toTagEntry(row) {
16797
16874
  sessionId: row.session_id
16798
16875
  };
16799
16876
  }
16877
+ function isTagNumberRow(row) {
16878
+ if (row === null || typeof row !== "object")
16879
+ return false;
16880
+ const r = row;
16881
+ return typeof r.tag_number === "number";
16882
+ }
16883
+ function isMaxTagNumberRow(row) {
16884
+ if (row === null || typeof row !== "object")
16885
+ return false;
16886
+ const r = row;
16887
+ return typeof r.max_tag_number === "number";
16888
+ }
16889
+ function escapeLikePattern(value) {
16890
+ return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
16891
+ }
16800
16892
  function insertTag(db, sessionId, messageId, type, byteSize2, tagNumber) {
16801
16893
  getInsertTagStatement(db).run(sessionId, messageId, type, byteSize2, tagNumber);
16802
16894
  return tagNumber;
@@ -16807,6 +16899,21 @@ function updateTagStatus(db, sessionId, tagId, status) {
16807
16899
  function updateTagMessageId(db, sessionId, tagId, messageId) {
16808
16900
  getUpdateTagMessageIdStatement(db).run(messageId, sessionId, tagId);
16809
16901
  }
16902
+ function deleteTagsByMessageId(db, sessionId, messageId) {
16903
+ const escapedMessageId = escapeLikePattern(messageId);
16904
+ const textPartPattern = `${escapedMessageId}:p%`;
16905
+ const filePartPattern = `${escapedMessageId}:file%`;
16906
+ const tagNumbers = getTagNumbersByMessageIdStatement(db).all(sessionId, messageId, textPartPattern, filePartPattern).filter(isTagNumberRow).map((row) => row.tag_number);
16907
+ if (tagNumbers.length === 0) {
16908
+ return [];
16909
+ }
16910
+ getDeleteTagsByMessageIdStatement(db).run(sessionId, messageId, textPartPattern, filePartPattern);
16911
+ return tagNumbers;
16912
+ }
16913
+ function getMaxTagNumberBySession(db, sessionId) {
16914
+ const row = getMaxTagNumberBySessionStatement(db).get(sessionId);
16915
+ return isMaxTagNumberRow(row) ? row.max_tag_number : 0;
16916
+ }
16810
16917
  function getTagsBySession(db, sessionId) {
16811
16918
  const rows = db.prepare("SELECT id, message_id, type, status, byte_size, session_id, tag_number FROM tags WHERE session_id = ? ORDER BY tag_number ASC, id ASC").all(sessionId).filter(isTagRow);
16812
16919
  return rows.map(toTagEntry);
@@ -17679,6 +17786,85 @@ function getMessageUpdatedAssistantInfo(properties) {
17679
17786
  }
17680
17787
  };
17681
17788
  }
17789
+ function getMessageRemovedInfo(properties) {
17790
+ if (!isRecord(properties)) {
17791
+ return null;
17792
+ }
17793
+ if (typeof properties.sessionID !== "string" || typeof properties.messageID !== "string") {
17794
+ return null;
17795
+ }
17796
+ return {
17797
+ sessionID: properties.sessionID,
17798
+ messageID: properties.messageID
17799
+ };
17800
+ }
17801
+
17802
+ // src/hooks/magic-context/note-nudger.ts
17803
+ var NOTE_NUDGE_COOLDOWN_MS = 15 * 60 * 1000;
17804
+ var lastDeliveredAt = new Map;
17805
+ function getPersistedNoteNudgeDeliveredAt(_db, sessionId) {
17806
+ return lastDeliveredAt.get(sessionId) ?? 0;
17807
+ }
17808
+ function recordNoteNudgeDeliveryTime(sessionId) {
17809
+ lastDeliveredAt.set(sessionId, Date.now());
17810
+ }
17811
+ function onNoteTrigger(db, sessionId, trigger) {
17812
+ setPersistedNoteNudgeTrigger(db, sessionId);
17813
+ sessionLog(sessionId, `note-nudge: trigger fired (${trigger}), triggerPending=true`);
17814
+ }
17815
+ function peekNoteNudgeText(db, sessionId, currentUserMessageId, projectIdentity) {
17816
+ const state = getPersistedNoteNudge(db, sessionId);
17817
+ if (!state.triggerPending)
17818
+ return null;
17819
+ if (!state.triggerMessageId && currentUserMessageId) {
17820
+ setPersistedNoteNudgeTriggerMessageId(db, sessionId, currentUserMessageId);
17821
+ state.triggerMessageId = currentUserMessageId;
17822
+ }
17823
+ if (state.triggerMessageId && currentUserMessageId && state.triggerMessageId === currentUserMessageId) {
17824
+ sessionLog(sessionId, `note-nudge: deferring \u2014 current user message ${currentUserMessageId} is same as trigger-time message`);
17825
+ return null;
17826
+ }
17827
+ const deliveredAt = getPersistedNoteNudgeDeliveredAt(db, sessionId);
17828
+ if (deliveredAt > 0 && Date.now() - deliveredAt < NOTE_NUDGE_COOLDOWN_MS) {
17829
+ sessionLog(sessionId, `note-nudge: suppressing \u2014 last delivered ${Math.round((Date.now() - deliveredAt) / 1000)}s ago (cooldown ${NOTE_NUDGE_COOLDOWN_MS / 60000}m)`);
17830
+ clearPersistedNoteNudge(db, sessionId);
17831
+ return null;
17832
+ }
17833
+ const notes = getSessionNotes(db, sessionId);
17834
+ const readySmartCount = projectIdentity ? getReadySmartNotes(db, projectIdentity).length : 0;
17835
+ const totalCount = notes.length + readySmartCount;
17836
+ if (totalCount === 0) {
17837
+ sessionLog(sessionId, "note-nudge: triggerPending but no notes found, skipping");
17838
+ clearPersistedNoteNudge(db, sessionId);
17839
+ return null;
17840
+ }
17841
+ const parts = [];
17842
+ if (notes.length > 0) {
17843
+ parts.push(`${notes.length} deferred note${notes.length === 1 ? "" : "s"}`);
17844
+ }
17845
+ if (readySmartCount > 0) {
17846
+ parts.push(`${readySmartCount} ready smart note${readySmartCount === 1 ? "" : "s"}`);
17847
+ }
17848
+ sessionLog(sessionId, `note-nudge: delivering nudge for ${parts.join(" and ")}`);
17849
+ return `You have ${parts.join(" and ")}. Review with ctx_note read \u2014 some may be actionable now.`;
17850
+ }
17851
+ function markNoteNudgeDelivered(db, sessionId, text, messageId) {
17852
+ setPersistedDeliveredNoteNudge(db, sessionId, messageId ? text : "", messageId ?? "");
17853
+ recordNoteNudgeDeliveryTime(sessionId);
17854
+ sessionLog(sessionId, messageId ? `note-nudge: marked delivered, sticky anchor=${messageId}` : "note-nudge: marked delivered without anchor");
17855
+ }
17856
+ function getStickyNoteNudge(db, sessionId) {
17857
+ const state = getPersistedNoteNudge(db, sessionId);
17858
+ if (!state.stickyText || !state.stickyMessageId)
17859
+ return null;
17860
+ return { text: state.stickyText, messageId: state.stickyMessageId };
17861
+ }
17862
+ function clearNoteNudgeState(db, sessionId, options) {
17863
+ if (options?.persist !== false) {
17864
+ clearPersistedNoteNudge(db, sessionId);
17865
+ }
17866
+ lastDeliveredAt.delete(sessionId);
17867
+ }
17682
17868
 
17683
17869
  // src/hooks/magic-context/event-handler.ts
17684
17870
  var CONTEXT_USAGE_TTL_MS = 60 * 60 * 1000;
@@ -17690,6 +17876,46 @@ function evictExpiredUsageEntries(contextUsageMap) {
17690
17876
  }
17691
17877
  }
17692
17878
  }
17879
+ function cleanupRemovedMessageState(deps, sessionId, messageId) {
17880
+ return deps.db.transaction(() => {
17881
+ const removedTagNumbers = deleteTagsByMessageId(deps.db, sessionId, messageId);
17882
+ sessionLog(sessionId, `event message.removed: deleted ${removedTagNumbers.length} tag(s) for message ${messageId}`);
17883
+ const strippedPlaceholderRemoved = removeStrippedPlaceholderId(deps.db, sessionId, messageId);
17884
+ sessionLog(sessionId, strippedPlaceholderRemoved ? `event message.removed: removed ${messageId} from stripped placeholder ids` : `event message.removed: stripped placeholder ids unchanged for ${messageId}`);
17885
+ const persistedNudgePlacement = getPersistedNudgePlacement(deps.db, sessionId);
17886
+ const clearedNudgePlacement = persistedNudgePlacement?.messageId === messageId;
17887
+ if (clearedNudgePlacement) {
17888
+ clearPersistedNudgePlacement(deps.db, sessionId);
17889
+ }
17890
+ sessionLog(sessionId, clearedNudgePlacement ? `event message.removed: cleared nudge anchor for ${messageId}` : `event message.removed: nudge anchor unchanged for ${messageId}`);
17891
+ const persistedNoteNudge = getPersistedNoteNudge(deps.db, sessionId);
17892
+ const clearedNoteNudge = persistedNoteNudge.triggerMessageId === messageId || persistedNoteNudge.stickyMessageId === messageId;
17893
+ if (clearedNoteNudge) {
17894
+ clearPersistedNoteNudge(deps.db, sessionId);
17895
+ }
17896
+ sessionLog(sessionId, clearedNoteNudge ? `event message.removed: cleared note nudge state for ${messageId}` : `event message.removed: note nudge state unchanged for ${messageId}`);
17897
+ const persistedStickyTurnReminder = getPersistedStickyTurnReminder(deps.db, sessionId);
17898
+ const clearedStickyTurnReminder = persistedStickyTurnReminder?.messageId === messageId;
17899
+ if (clearedStickyTurnReminder) {
17900
+ clearPersistedStickyTurnReminder(deps.db, sessionId);
17901
+ }
17902
+ sessionLog(sessionId, clearedStickyTurnReminder ? `event message.removed: cleared sticky turn reminder for ${messageId}` : `event message.removed: sticky turn reminder unchanged for ${messageId}`);
17903
+ const currentWatermark = getPersistedReasoningWatermark(deps.db, sessionId);
17904
+ const maxRemainingTag = getMaxTagNumberBySession(deps.db, sessionId);
17905
+ if (currentWatermark > maxRemainingTag) {
17906
+ setPersistedReasoningWatermark(deps.db, sessionId, maxRemainingTag);
17907
+ sessionLog(sessionId, `event message.removed: reset reasoning watermark ${currentWatermark}\u2192${maxRemainingTag}`);
17908
+ } else {
17909
+ sessionLog(sessionId, `event message.removed: reasoning watermark unchanged at ${currentWatermark} (max tag ${maxRemainingTag})`);
17910
+ }
17911
+ const removedIndexedMessages = deleteIndexedMessage(deps.db, sessionId, messageId);
17912
+ sessionLog(sessionId, `event message.removed: deleted ${removedIndexedMessages} indexed message row(s) for ${messageId}`);
17913
+ return {
17914
+ clearedNudgePlacement,
17915
+ clearedNoteNudge
17916
+ };
17917
+ })();
17918
+ }
17693
17919
  function createEventHandler2(deps) {
17694
17920
  return async (input) => {
17695
17921
  evictExpiredUsageEntries(deps.contextUsageMap);
@@ -17778,6 +18004,37 @@ function createEventHandler2(deps) {
17778
18004
  }
17779
18005
  return;
17780
18006
  }
18007
+ if (input.event.type === "message.removed") {
18008
+ const info = getMessageRemovedInfo(input.event.properties);
18009
+ if (!info) {
18010
+ const sessionId = properties ? resolveSessionId(properties) : null;
18011
+ if (sessionId) {
18012
+ sessionLog(sessionId, "event message.removed: no message removal info extracted from event");
18013
+ } else {
18014
+ log("[magic-context] event message.removed: no message removal info extracted from event");
18015
+ }
18016
+ return;
18017
+ }
18018
+ sessionLog(info.sessionID, `event message.removed: invalidating state for message ${info.messageID}`);
18019
+ try {
18020
+ const cleanup = cleanupRemovedMessageState(deps, info.sessionID, info.messageID);
18021
+ deps.tagger.cleanup(info.sessionID);
18022
+ sessionLog(info.sessionID, "event message.removed: invalidated tagger session cache");
18023
+ if (cleanup.clearedNudgePlacement) {
18024
+ deps.nudgePlacements.clear(info.sessionID, { persist: false });
18025
+ sessionLog(info.sessionID, "event message.removed: cleared in-memory nudge placement cache");
18026
+ }
18027
+ if (cleanup.clearedNoteNudge) {
18028
+ clearNoteNudgeState(deps.db, info.sessionID, { persist: false });
18029
+ sessionLog(info.sessionID, "event message.removed: cleared in-memory note nudge state");
18030
+ }
18031
+ deps.onSessionCacheInvalidated?.(info.sessionID);
18032
+ sessionLog(info.sessionID, "event message.removed: cleared session injection cache");
18033
+ } catch (error48) {
18034
+ sessionLog(info.sessionID, "event message.removed cleanup failed:", error48);
18035
+ }
18036
+ return;
18037
+ }
17781
18038
  if (input.event.type === "session.compacted") {
17782
18039
  const sessionId = resolveSessionId(properties);
17783
18040
  if (!sessionId) {
@@ -18622,72 +18879,6 @@ function createTextCompleteHandler() {
18622
18879
  };
18623
18880
  }
18624
18881
 
18625
- // src/hooks/magic-context/note-nudger.ts
18626
- var NOTE_NUDGE_COOLDOWN_MS = 15 * 60 * 1000;
18627
- var lastDeliveredAt = new Map;
18628
- function getPersistedNoteNudgeDeliveredAt(_db, sessionId) {
18629
- return lastDeliveredAt.get(sessionId) ?? 0;
18630
- }
18631
- function recordNoteNudgeDeliveryTime(sessionId) {
18632
- lastDeliveredAt.set(sessionId, Date.now());
18633
- }
18634
- function onNoteTrigger(db, sessionId, trigger) {
18635
- setPersistedNoteNudgeTrigger(db, sessionId);
18636
- sessionLog(sessionId, `note-nudge: trigger fired (${trigger}), triggerPending=true`);
18637
- }
18638
- function peekNoteNudgeText(db, sessionId, currentUserMessageId, projectIdentity) {
18639
- const state = getPersistedNoteNudge(db, sessionId);
18640
- if (!state.triggerPending)
18641
- return null;
18642
- if (!state.triggerMessageId && currentUserMessageId) {
18643
- setPersistedNoteNudgeTriggerMessageId(db, sessionId, currentUserMessageId);
18644
- state.triggerMessageId = currentUserMessageId;
18645
- }
18646
- if (state.triggerMessageId && currentUserMessageId && state.triggerMessageId === currentUserMessageId) {
18647
- sessionLog(sessionId, `note-nudge: deferring \u2014 current user message ${currentUserMessageId} is same as trigger-time message`);
18648
- return null;
18649
- }
18650
- if (state.stickyText && state.stickyMessageId) {
18651
- const deliveredAt = getPersistedNoteNudgeDeliveredAt(db, sessionId);
18652
- if (deliveredAt > 0 && Date.now() - deliveredAt < NOTE_NUDGE_COOLDOWN_MS) {
18653
- sessionLog(sessionId, `note-nudge: suppressing \u2014 last delivered ${Math.round((Date.now() - deliveredAt) / 1000)}s ago (cooldown ${NOTE_NUDGE_COOLDOWN_MS / 60000}m)`);
18654
- clearPersistedNoteNudge(db, sessionId);
18655
- return null;
18656
- }
18657
- }
18658
- const notes = getSessionNotes(db, sessionId);
18659
- const readySmartCount = projectIdentity ? getReadySmartNotes(db, projectIdentity).length : 0;
18660
- const totalCount = notes.length + readySmartCount;
18661
- if (totalCount === 0) {
18662
- sessionLog(sessionId, "note-nudge: triggerPending but no notes found, skipping");
18663
- clearPersistedNoteNudge(db, sessionId);
18664
- return null;
18665
- }
18666
- const parts = [];
18667
- if (notes.length > 0) {
18668
- parts.push(`${notes.length} deferred note${notes.length === 1 ? "" : "s"}`);
18669
- }
18670
- if (readySmartCount > 0) {
18671
- parts.push(`${readySmartCount} ready smart note${readySmartCount === 1 ? "" : "s"}`);
18672
- }
18673
- sessionLog(sessionId, `note-nudge: delivering nudge for ${parts.join(" and ")}`);
18674
- return `You have ${parts.join(" and ")}. Review with ctx_note read \u2014 some may be actionable now.`;
18675
- }
18676
- function markNoteNudgeDelivered(db, sessionId, text, messageId) {
18677
- setPersistedDeliveredNoteNudge(db, sessionId, messageId ? text : "", messageId ?? "");
18678
- recordNoteNudgeDeliveryTime(sessionId);
18679
- sessionLog(sessionId, messageId ? `note-nudge: marked delivered, sticky anchor=${messageId}` : "note-nudge: marked delivered without anchor");
18680
- }
18681
- function getStickyNoteNudge(db, sessionId) {
18682
- const state = getPersistedNoteNudge(db, sessionId);
18683
- if (!state.stickyText || !state.stickyMessageId)
18684
- return null;
18685
- return { text: state.stickyText, messageId: state.stickyMessageId };
18686
- }
18687
- function clearNoteNudgeState(db, sessionId) {
18688
- clearPersistedNoteNudge(db, sessionId);
18689
- }
18690
-
18691
18882
  // src/hooks/magic-context/strip-content.ts
18692
18883
  var DROPPED_PLACEHOLDER_PATTERN = /^\[dropped \u00A7\d+\u00A7\]$/;
18693
18884
  var TAG_PREFIX_PATTERN = /^\u00A7\d+\u00A7\s*/;
@@ -21676,10 +21867,10 @@ function createNudgePlacementStore(db) {
21676
21867
  store.set(sessionId, persisted);
21677
21868
  return persisted;
21678
21869
  },
21679
- clear(sessionId) {
21870
+ clear(sessionId, options) {
21680
21871
  store.delete(sessionId);
21681
21872
  missingSessions.add(sessionId);
21682
- if (db) {
21873
+ if (db && options?.persist !== false) {
21683
21874
  clearPersistedNudgePlacement(db, sessionId);
21684
21875
  }
21685
21876
  }
@@ -21906,6 +22097,9 @@ function createEventHook(args) {
21906
22097
  args.commitSeenLastPass?.delete(sessionId);
21907
22098
  clearNoteNudgeState(args.db, sessionId);
21908
22099
  }
22100
+ if (input.event.type === "message.removed") {
22101
+ return;
22102
+ }
21909
22103
  const entry = args.contextUsageMap.get(sessionId);
21910
22104
  if (!entry)
21911
22105
  return;
@@ -21998,6 +22192,13 @@ Use \`ctx_note\` for deferred intentions \u2014 things to tackle later, not righ
21998
22192
  Use \`ctx_memory\` to manage cross-session project memories. Write new memories or delete stale ones. Memories persist across sessions and are automatically injected into new sessions.
21999
22193
  Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
22000
22194
  Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start=N end=M>\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
22195
+ **Search before asking the user**: If you can't remember or don't know something that might have been discussed before or stored in project memory, use \`ctx_search\` before asking the user. Examples:
22196
+ - Can't remember where a related codebase or dependency lives \u2192 \`ctx_search(query="opencode source code path")\`
22197
+ - Forgot a prior architectural decision or constraint \u2192 \`ctx_search(query="why did we choose SQLite over postgres")\`
22198
+ - Need a config value, API key location, or environment detail \u2192 \`ctx_search(query="embedding provider configuration")\`
22199
+ - Looking for how something was implemented previously \u2192 \`ctx_search(query="how does the dreamer lease work")\`
22200
+ - Want to recall what was decided in an earlier conversation \u2192 \`ctx_search(query="dashboard release signing setup")\`
22201
+ \`ctx_search\` returns ranked results from memories, session facts, and raw message history. Use message ordinals from results with \`ctx_expand\` to retrieve surrounding conversation context.
22001
22202
  NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
22002
22203
  NEVER drop user messages \u2014 they are short and will be summarized by compartmentalization automatically. Dropping them loses context the historian needs.
22003
22204
  NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
@@ -22006,7 +22207,14 @@ var BASE_INTRO_NO_REDUCE = `Messages and tool outputs are tagged with \xA7N\xA7
22006
22207
  Use \`ctx_note\` for deferred intentions \u2014 things to tackle later, not right now. NOT for task tracking (use todos). Notes survive context compression and you'll be reminded at natural work boundaries (after commits, historian runs, todo completion).
22007
22208
  Use \`ctx_memory\` to manage cross-session project memories. Write new memories or delete stale ones. Memories persist across sessions and are automatically injected into new sessions.
22008
22209
  Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
22009
- Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start=N end=M>\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.`;
22210
+ Use \`ctx_expand\` to decompress a compartment range to see the original conversation transcript. Use \`start\`/\`end\` from \`<compartment start=N end=M>\` attributes. Returns the compacted U:/A: transcript for that message range, capped at ~15K tokens.
22211
+ **Search before asking the user**: If you can't remember or don't know something that might have been discussed before or stored in project memory, use \`ctx_search\` before asking the user. Examples:
22212
+ - Can't remember where a related codebase or dependency lives \u2192 \`ctx_search(query="opencode source code path")\`
22213
+ - Forgot a prior architectural decision or constraint \u2192 \`ctx_search(query="why did we choose SQLite over postgres")\`
22214
+ - Need a config value, API key location, or environment detail \u2192 \`ctx_search(query="embedding provider configuration")\`
22215
+ - Looking for how something was implemented previously \u2192 \`ctx_search(query="how does the dreamer lease work")\`
22216
+ - Want to recall what was decided in an earlier conversation \u2192 \`ctx_search(query="dashboard release signing setup")\`
22217
+ \`ctx_search\` returns ranked results from memories, session facts, and raw message history. Use message ordinals from results with \`ctx_expand\` to retrieve surrounding conversation context.`;
22010
22218
  var SISYPHUS_SECTION = `
22011
22219
  ### Reduction Triggers
22012
22220
  - After collecting background agent results (explore/librarian) \u2014 drop raw outputs once you extracted what you need.
@@ -23210,7 +23418,8 @@ async function getSemanticScores(args) {
23210
23418
  function getFtsMatches(args) {
23211
23419
  try {
23212
23420
  return searchMemoriesFTS(args.db, args.projectPath, args.query, args.limit);
23213
- } catch {
23421
+ } catch (error48) {
23422
+ log(`[search] FTS query failed for "${args.query}": ${error48 instanceof Error ? error48.message : String(error48)}`);
23214
23423
  return [];
23215
23424
  }
23216
23425
  }