@cortexkit/opencode-magic-context 0.3.0 → 0.3.2

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.
Files changed (33) hide show
  1. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  2. package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
  3. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  4. package/dist/features/magic-context/message-index.d.ts +1 -0
  5. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  6. package/dist/features/magic-context/search.d.ts.map +1 -1
  7. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  8. package/dist/features/magic-context/storage-meta-persisted.d.ts +3 -0
  9. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  10. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  11. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  12. package/dist/features/magic-context/storage-tags.d.ts +3 -1
  13. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  14. package/dist/features/magic-context/storage.d.ts +3 -2
  15. package/dist/features/magic-context/storage.d.ts.map +1 -1
  16. package/dist/features/magic-context/tagger.d.ts +1 -1
  17. package/dist/features/magic-context/tagger.d.ts.map +1 -1
  18. package/dist/features/magic-context/types.d.ts +1 -0
  19. package/dist/features/magic-context/types.d.ts.map +1 -1
  20. package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -1
  21. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  22. package/dist/hooks/magic-context/event-handler.d.ts +2 -0
  23. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  24. package/dist/hooks/magic-context/event-payloads.d.ts +6 -1
  25. package/dist/hooks/magic-context/event-payloads.d.ts.map +1 -1
  26. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/note-nudger.d.ts +3 -1
  28. package/dist/hooks/magic-context/note-nudger.d.ts.map +1 -1
  29. package/dist/hooks/magic-context/nudge-placement-store.d.ts +3 -1
  30. package/dist/hooks/magic-context/nudge-placement-store.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  32. package/dist/index.js +1264 -1008
  33. package/package.json +1 -1
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,1050 @@ 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
- };
16047
- }
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
- })();
16053
- }
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);
16028
+ return stmt;
16056
16029
  }
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();
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);
16061
16035
  }
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
- };
16036
+ return stmt;
16068
16037
  }
16069
- function setPersistedNoteNudgeTrigger(db, sessionId, triggerMessageId = "") {
16070
- 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);
16073
- })();
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;
16074
16041
  }
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
- })();
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);
16047
+ }
16048
+ getDeleteIndexStatement(db).run(sessionId);
16049
+ return count;
16080
16050
  }
16081
- function setPersistedDeliveredNoteNudge(db, sessionId, text, messageId = "") {
16051
+ function clearIndexedMessages(db, sessionId) {
16082
16052
  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);
16053
+ getDeleteFtsStatement(db).run(sessionId);
16054
+ getDeleteIndexStatement(db).run(sessionId);
16085
16055
  })();
16086
16056
  }
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
16120
- import { Database as Database2 } from "bun:sqlite";
16121
- import { join as join6 } from "path";
16122
- function getOpenCodeDbPath() {
16123
- return join6(getDataDir(), "opencode", "opencode.db");
16124
- }
16125
- var cachedReadOnlyDb = null;
16126
- function closeCachedReadOnlyDb() {
16127
- if (!cachedReadOnlyDb) {
16128
- return;
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(" / ");
16129
16063
  }
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;
16064
+ if (role === "assistant") {
16065
+ return extractTexts(parts).map(removeSystemReminders).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
16136
16066
  }
16067
+ return "";
16137
16068
  }
16138
- function getReadOnlySessionDb() {
16139
- const dbPath = getOpenCodeDbPath();
16140
- if (cachedReadOnlyDb?.path === dbPath) {
16141
- return cachedReadOnlyDb.db;
16069
+ function ensureMessagesIndexed(db, sessionId, readMessages) {
16070
+ const messages = readMessages(sessionId);
16071
+ if (messages.length === 0) {
16072
+ db.transaction(() => clearIndexedMessages(db, sessionId))();
16073
+ return;
16142
16074
  }
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;
16075
+ let lastIndexedOrdinal = getLastIndexedOrdinal(db, sessionId);
16076
+ if (lastIndexedOrdinal > messages.length) {
16077
+ db.transaction(() => clearIndexedMessages(db, sessionId))();
16078
+ lastIndexedOrdinal = 0;
16175
16079
  }
16176
- return false;
16177
- }
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());
16186
- }
16080
+ if (lastIndexedOrdinal >= messages.length) {
16081
+ return;
16187
16082
  }
16188
- return texts;
16189
- }
16190
- function estimateTokens(text) {
16191
- return Math.ceil(text.length / 3.5);
16192
- }
16193
- function normalizeText(text) {
16194
- return text.replace(/\s+/g, " ").trim();
16195
- }
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";
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();
16090
+ db.transaction(() => {
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);
16096
+ })();
16202
16097
  }
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(" / ")}`;
16098
+ // src/features/magic-context/storage-db.ts
16099
+ import { Database as Database2 } from "bun:sqlite";
16100
+ import { mkdirSync } from "fs";
16101
+ import { join as join6 } from "path";
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") };
16207
16109
  }
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;
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");
16356
+ ensureColumn(db, "tags", "reasoning_byte_size", "INTEGER DEFAULT 0");
16221
16357
  }
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 };
16358
+ function ensureColumn(db, table, column, definition) {
16359
+ if (!/^[a-z_]+$/.test(table) || !/^[a-z_]+$/.test(column) || !/^[A-Z0-9_'(),\s]+$/i.test(definition)) {
16360
+ throw new Error(`Unsafe schema identifier: ${table}.${column} ${definition}`);
16226
16361
  }
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();
16228
- return {
16229
- text: withoutHashes.length > 0 ? withoutHashes : text,
16230
- commitHashes
16231
- };
16232
- }
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;
16362
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all();
16363
+ if (rows.some((row) => row.name === column)) {
16364
+ return;
16243
16365
  }
16244
- return merged;
16245
- }
16246
-
16247
- // src/hooks/magic-context/read-session-raw.ts
16248
- function isRawMessageRow(row) {
16249
- if (row === null || typeof row !== "object")
16250
- return false;
16251
- const candidate = row;
16252
- return typeof candidate.id === "string" && typeof candidate.data === "string";
16253
- }
16254
- function isRawPartRow(row) {
16255
- if (row === null || typeof row !== "object")
16256
- return false;
16257
- const candidate = row;
16258
- return typeof candidate.message_id === "string" && typeof candidate.data === "string";
16366
+ db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
16259
16367
  }
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");
16368
+ function createFallbackDatabase() {
16369
+ try {
16370
+ const fallback = new Database2(":memory:");
16371
+ initializeDatabase(fallback);
16372
+ return fallback;
16373
+ } catch (error48) {
16374
+ throw new Error(`[magic-context] storage fatal: failed to initialize fallback database: ${getErrorMessage(error48)}`);
16264
16375
  }
16265
- return parsed;
16266
- }
16267
- function parseJsonUnknown(value) {
16268
- return JSON.parse(value);
16269
16376
  }
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);
16377
+ function openDatabase() {
16378
+ try {
16379
+ const { dbDir, dbPath } = resolveDatabasePath();
16380
+ const existing = databases.get(dbPath);
16381
+ if (existing) {
16382
+ if (!persistenceByDatabase.has(existing)) {
16383
+ persistenceByDatabase.set(existing, true);
16384
+ }
16385
+ return existing;
16386
+ }
16387
+ mkdirSync(dbDir, { recursive: true });
16388
+ const db = new Database2(dbPath);
16389
+ initializeDatabase(db);
16390
+ databases.set(dbPath, db);
16391
+ persistenceByDatabase.set(db, true);
16392
+ persistenceErrorByDatabase.delete(db);
16393
+ return db;
16394
+ } catch (error48) {
16395
+ log("[magic-context] storage error:", error48);
16396
+ const errorMessage = getErrorMessage(error48);
16397
+ const existingFallback = databases.get(FALLBACK_DATABASE_KEY);
16398
+ if (existingFallback) {
16399
+ if (!persistenceByDatabase.has(existingFallback)) {
16400
+ persistenceByDatabase.set(existingFallback, false);
16401
+ persistenceErrorByDatabase.set(existingFallback, errorMessage);
16402
+ }
16403
+ return existingFallback;
16404
+ }
16405
+ const fallback = createFallbackDatabase();
16406
+ databases.set(FALLBACK_DATABASE_KEY, fallback);
16407
+ persistenceByDatabase.set(fallback, false);
16408
+ persistenceErrorByDatabase.set(fallback, errorMessage);
16409
+ return fallback;
16278
16410
  }
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
16411
  }
16300
- function prependTag(tagId, value) {
16301
- const stripped = stripTagPrefix(value);
16302
- return `\xA7${tagId}\xA7 ${stripped}`;
16412
+ function isDatabasePersisted(db) {
16413
+ return persistenceByDatabase.get(db) ?? false;
16303
16414
  }
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";
16415
+ function getDatabasePersistenceError(db) {
16416
+ return persistenceErrorByDatabase.get(db) ?? null;
16309
16417
  }
16310
-
16311
- // src/hooks/magic-context/tag-part-guards.ts
16312
- function isTextPart(part) {
16313
- if (part === null || typeof part !== "object")
16418
+ // src/features/magic-context/storage-meta-shared.ts
16419
+ var META_COLUMNS = {
16420
+ lastResponseTime: "last_response_time",
16421
+ cacheTtl: "cache_ttl",
16422
+ counter: "counter",
16423
+ lastNudgeTokens: "last_nudge_tokens",
16424
+ lastNudgeBand: "last_nudge_band",
16425
+ lastTransformError: "last_transform_error",
16426
+ isSubagent: "is_subagent",
16427
+ lastContextPercentage: "last_context_percentage",
16428
+ lastInputTokens: "last_input_tokens",
16429
+ timesExecuteThresholdReached: "times_execute_threshold_reached",
16430
+ compartmentInProgress: "compartment_in_progress",
16431
+ systemPromptHash: "system_prompt_hash",
16432
+ clearedReasoningThroughTag: "cleared_reasoning_through_tag"
16433
+ };
16434
+ var BOOLEAN_META_KEYS = new Set(["isSubagent", "compartmentInProgress"]);
16435
+ function isSessionMetaRow(row) {
16436
+ if (row === null || typeof row !== "object")
16314
16437
  return false;
16315
- const p = part;
16316
- return p.type === "text" && typeof p.text === "string";
16438
+ const r = row;
16439
+ 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";
16317
16440
  }
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";
16441
+ function getDefaultSessionMeta(sessionId) {
16442
+ return {
16443
+ sessionId,
16444
+ lastResponseTime: 0,
16445
+ cacheTtl: "5m",
16446
+ counter: 0,
16447
+ lastNudgeTokens: 0,
16448
+ lastNudgeBand: null,
16449
+ lastTransformError: null,
16450
+ isSubagent: false,
16451
+ lastContextPercentage: 0,
16452
+ lastInputTokens: 0,
16453
+ timesExecuteThresholdReached: 0,
16454
+ compartmentInProgress: false,
16455
+ systemPromptHash: "",
16456
+ clearedReasoningThroughTag: 0
16457
+ };
16327
16458
  }
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";
16459
+ function ensureSessionMetaRow(db, sessionId) {
16460
+ const defaults = getDefaultSessionMeta(sessionId);
16461
+ 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);
16333
16462
  }
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;
16463
+ function toSessionMeta(row) {
16464
+ return {
16465
+ sessionId: row.session_id,
16466
+ lastResponseTime: row.last_response_time,
16467
+ cacheTtl: row.cache_ttl,
16468
+ counter: row.counter,
16469
+ lastNudgeTokens: row.last_nudge_tokens,
16470
+ lastNudgeBand: row.last_nudge_band.length > 0 ? row.last_nudge_band : null,
16471
+ lastTransformError: row.last_transform_error.length > 0 ? row.last_transform_error : null,
16472
+ isSubagent: row.is_subagent === 1,
16473
+ lastContextPercentage: row.last_context_percentage,
16474
+ lastInputTokens: row.last_input_tokens,
16475
+ timesExecuteThresholdReached: row.times_execute_threshold_reached,
16476
+ compartmentInProgress: row.compartment_in_progress === 1,
16477
+ systemPromptHash: String(row.system_prompt_hash),
16478
+ clearedReasoningThroughTag: row.cleared_reasoning_through_tag
16479
+ };
16338
16480
  }
16339
16481
 
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();
16482
+ // src/features/magic-context/storage-meta-persisted.ts
16483
+ function isPersistedUsageRow(row) {
16484
+ if (row === null || typeof row !== "object")
16485
+ return false;
16486
+ const r = row;
16487
+ return typeof r.last_context_percentage === "number" && typeof r.last_input_tokens === "number" && typeof r.last_response_time === "number";
16344
16488
  }
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
- }
16489
+ function isPersistedReasoningWatermarkRow(row) {
16490
+ if (row === null || typeof row !== "object")
16491
+ return false;
16492
+ const r = row;
16493
+ return typeof r.cleared_reasoning_through_tag === "number";
16357
16494
  }
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));
16495
+ function isPersistedNudgePlacementRow(row) {
16496
+ if (row === null || typeof row !== "object")
16497
+ return false;
16498
+ const r = row;
16499
+ return typeof r.nudge_anchor_message_id === "string" && typeof r.nudge_anchor_text === "string";
16369
16500
  }
16370
- function getRawSessionMessageCount(sessionId) {
16371
- return withReadOnlySessionDb((db) => getRawSessionMessageCountFromDb(db, sessionId));
16501
+ function isPersistedStickyTurnReminderRow(row) {
16502
+ if (row === null || typeof row !== "object")
16503
+ return false;
16504
+ const r = row;
16505
+ return typeof r.sticky_turn_reminder_text === "string" && typeof r.sticky_turn_reminder_message_id === "string";
16372
16506
  }
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
- }
16390
- }
16391
- return keys;
16507
+ function isPersistedNoteNudgeRow(row) {
16508
+ if (row === null || typeof row !== "object")
16509
+ return false;
16510
+ const r = row;
16511
+ 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";
16392
16512
  }
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;
16513
+ function getDefaultPersistedNoteNudge() {
16514
+ return {
16515
+ triggerPending: false,
16516
+ triggerMessageId: null,
16517
+ stickyText: null,
16518
+ stickyMessageId: null
16519
+ };
16520
+ }
16521
+ function loadPersistedUsage(db, sessionId) {
16522
+ const result = db.prepare("SELECT last_context_percentage, last_input_tokens, last_response_time FROM session_meta WHERE session_id = ?").get(sessionId);
16523
+ if (!isPersistedUsageRow(result) || result.last_context_percentage === 0 && result.last_input_tokens === 0) {
16524
+ return null;
16399
16525
  }
16400
- return userOrdinals[userOrdinals.length - PROTECTED_TAIL_USER_TURNS];
16526
+ return {
16527
+ usage: {
16528
+ percentage: result.last_context_percentage,
16529
+ inputTokens: result.last_input_tokens
16530
+ },
16531
+ updatedAt: result.last_response_time || Date.now()
16532
+ };
16401
16533
  }
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;
16534
+ function getPersistedReasoningWatermark(db, sessionId) {
16535
+ const result = db.prepare("SELECT cleared_reasoning_through_tag FROM session_meta WHERE session_id = ?").get(sessionId);
16536
+ return isPersistedReasoningWatermarkRow(result) ? result.cleared_reasoning_through_tag : 0;
16537
+ }
16538
+ function setPersistedReasoningWatermark(db, sessionId, tagNumber) {
16539
+ ensureSessionMetaRow(db, sessionId);
16540
+ db.prepare("UPDATE session_meta SET cleared_reasoning_through_tag = ? WHERE session_id = ?").run(tagNumber, sessionId);
16541
+ }
16542
+ function getPersistedNudgePlacement(db, sessionId) {
16543
+ const result = db.prepare("SELECT nudge_anchor_message_id, nudge_anchor_text FROM session_meta WHERE session_id = ?").get(sessionId);
16544
+ if (!isPersistedNudgePlacementRow(result)) {
16545
+ return null;
16438
16546
  }
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 = [];
16547
+ if (result.nudge_anchor_message_id.length === 0 || result.nudge_anchor_text.length === 0) {
16548
+ return null;
16475
16549
  }
16476
- flushCurrentBlock();
16477
16550
  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
16551
+ messageId: result.nudge_anchor_message_id,
16552
+ nudgeText: result.nudge_anchor_text
16489
16553
  };
16490
16554
  }
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();
16555
+ function setPersistedNudgePlacement(db, sessionId, messageId, nudgeText) {
16556
+ db.transaction(() => {
16557
+ ensureSessionMetaRow(db, sessionId);
16558
+ db.prepare("UPDATE session_meta SET nudge_anchor_message_id = ?, nudge_anchor_text = ? WHERE session_id = ?").run(messageId, nudgeText, sessionId);
16559
+ })();
16500
16560
  }
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;
16561
+ function clearPersistedNudgePlacement(db, sessionId) {
16562
+ db.prepare("UPDATE session_meta SET nudge_anchor_message_id = '', nudge_anchor_text = '' WHERE session_id = ?").run(sessionId);
16508
16563
  }
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);
16564
+ function getPersistedStickyTurnReminder(db, sessionId) {
16565
+ const result = db.prepare("SELECT sticky_turn_reminder_text, sticky_turn_reminder_message_id FROM session_meta WHERE session_id = ?").get(sessionId);
16566
+ if (!isPersistedStickyTurnReminderRow(result)) {
16567
+ return null;
16514
16568
  }
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);
16569
+ if (result.sticky_turn_reminder_text.length === 0) {
16570
+ return null;
16522
16571
  }
16523
- return stmt;
16572
+ return {
16573
+ text: result.sticky_turn_reminder_text,
16574
+ messageId: result.sticky_turn_reminder_message_id.length > 0 ? result.sticky_turn_reminder_message_id : null
16575
+ };
16524
16576
  }
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;
16577
+ function setPersistedStickyTurnReminder(db, sessionId, text, messageId = "") {
16578
+ db.transaction(() => {
16579
+ ensureSessionMetaRow(db, sessionId);
16580
+ db.prepare("UPDATE session_meta SET sticky_turn_reminder_text = ?, sticky_turn_reminder_message_id = ? WHERE session_id = ?").run(text, messageId, sessionId);
16581
+ })();
16532
16582
  }
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;
16583
+ function clearPersistedStickyTurnReminder(db, sessionId) {
16584
+ db.prepare("UPDATE session_meta SET sticky_turn_reminder_text = '', sticky_turn_reminder_message_id = '' WHERE session_id = ?").run(sessionId);
16540
16585
  }
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;
16586
+ function getPersistedNoteNudge(db, sessionId) {
16587
+ 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);
16588
+ if (!isPersistedNoteNudgeRow(result)) {
16589
+ return getDefaultPersistedNoteNudge();
16590
+ }
16591
+ return {
16592
+ triggerPending: result.note_nudge_trigger_pending === 1,
16593
+ triggerMessageId: result.note_nudge_trigger_message_id.length > 0 ? result.note_nudge_trigger_message_id : null,
16594
+ stickyText: result.note_nudge_sticky_text.length > 0 ? result.note_nudge_sticky_text : null,
16595
+ stickyMessageId: result.note_nudge_sticky_message_id.length > 0 ? result.note_nudge_sticky_message_id : null
16596
+ };
16544
16597
  }
16545
- function clearIndexedMessages(db, sessionId) {
16598
+ function setPersistedNoteNudgeTrigger(db, sessionId, triggerMessageId = "") {
16546
16599
  db.transaction(() => {
16547
- getDeleteFtsStatement(db).run(sessionId);
16548
- getDeleteIndexStatement(db).run(sessionId);
16600
+ ensureSessionMetaRow(db, sessionId);
16601
+ 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
16602
  })();
16550
16603
  }
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 "";
16604
+ function setPersistedNoteNudgeTriggerMessageId(db, sessionId, triggerMessageId) {
16605
+ db.transaction(() => {
16606
+ ensureSessionMetaRow(db, sessionId);
16607
+ db.prepare("UPDATE session_meta SET note_nudge_trigger_message_id = ? WHERE session_id = ?").run(triggerMessageId, sessionId);
16608
+ })();
16562
16609
  }
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();
16610
+ function setPersistedDeliveredNoteNudge(db, sessionId, text, messageId = "") {
16584
16611
  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);
16612
+ ensureSessionMetaRow(db, sessionId);
16613
+ 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
16614
  })();
16591
16615
  }
16592
-
16616
+ function clearPersistedNoteNudge(db, sessionId) {
16617
+ 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);
16618
+ }
16619
+ function getStrippedPlaceholderIds(db, sessionId) {
16620
+ const row = db.prepare("SELECT stripped_placeholder_ids FROM session_meta WHERE session_id = ?").get(sessionId);
16621
+ const raw = row?.stripped_placeholder_ids;
16622
+ if (!raw || raw.length === 0)
16623
+ return new Set;
16624
+ try {
16625
+ const parsed = JSON.parse(raw);
16626
+ if (Array.isArray(parsed))
16627
+ return new Set(parsed.filter((v) => typeof v === "string"));
16628
+ } catch {}
16629
+ return new Set;
16630
+ }
16631
+ function setStrippedPlaceholderIds(db, sessionId, ids) {
16632
+ ensureSessionMetaRow(db, sessionId);
16633
+ const json2 = ids.size > 0 ? JSON.stringify([...ids]) : "";
16634
+ db.prepare("UPDATE session_meta SET stripped_placeholder_ids = ? WHERE session_id = ?").run(json2, sessionId);
16635
+ }
16636
+ function removeStrippedPlaceholderId(db, sessionId, messageId) {
16637
+ const ids = getStrippedPlaceholderIds(db, sessionId);
16638
+ if (!ids.delete(messageId)) {
16639
+ return false;
16640
+ }
16641
+ setStrippedPlaceholderIds(db, sessionId, ids);
16642
+ return true;
16643
+ }
16593
16644
  // src/features/magic-context/storage-meta-session.ts
16594
16645
  function getOrCreateSessionMeta(db, sessionId) {
16595
16646
  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,10 +16806,13 @@ function getSourceContents(db, sessionId, tagIds) {
16755
16806
  var insertTagStatements = new WeakMap;
16756
16807
  var updateTagStatusStatements = new WeakMap;
16757
16808
  var updateTagMessageIdStatements = new WeakMap;
16809
+ var getTagNumbersByMessageIdStatements = new WeakMap;
16810
+ var deleteTagsByMessageIdStatements = new WeakMap;
16811
+ var getMaxTagNumberBySessionStatements = new WeakMap;
16758
16812
  function getInsertTagStatement(db) {
16759
16813
  let stmt = insertTagStatements.get(db);
16760
16814
  if (!stmt) {
16761
- stmt = db.prepare("INSERT INTO tags (session_id, message_id, type, byte_size, tag_number) VALUES (?, ?, ?, ?, ?)");
16815
+ stmt = db.prepare("INSERT INTO tags (session_id, message_id, type, byte_size, reasoning_byte_size, tag_number) VALUES (?, ?, ?, ?, ?, ?)");
16762
16816
  insertTagStatements.set(db, stmt);
16763
16817
  }
16764
16818
  return stmt;
@@ -16779,6 +16833,30 @@ function getUpdateTagMessageIdStatement(db) {
16779
16833
  }
16780
16834
  return stmt;
16781
16835
  }
16836
+ function getTagNumbersByMessageIdStatement(db) {
16837
+ let stmt = getTagNumbersByMessageIdStatements.get(db);
16838
+ if (!stmt) {
16839
+ 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");
16840
+ getTagNumbersByMessageIdStatements.set(db, stmt);
16841
+ }
16842
+ return stmt;
16843
+ }
16844
+ function getDeleteTagsByMessageIdStatement(db) {
16845
+ let stmt = deleteTagsByMessageIdStatements.get(db);
16846
+ if (!stmt) {
16847
+ stmt = db.prepare("DELETE FROM tags WHERE session_id = ? AND (message_id = ? OR message_id LIKE ? ESCAPE '\\' OR message_id LIKE ? ESCAPE '\\')");
16848
+ deleteTagsByMessageIdStatements.set(db, stmt);
16849
+ }
16850
+ return stmt;
16851
+ }
16852
+ function getMaxTagNumberBySessionStatement(db) {
16853
+ let stmt = getMaxTagNumberBySessionStatements.get(db);
16854
+ if (!stmt) {
16855
+ stmt = db.prepare("SELECT COALESCE(MAX(tag_number), 0) AS max_tag_number FROM tags WHERE session_id = ?");
16856
+ getMaxTagNumberBySessionStatements.set(db, stmt);
16857
+ }
16858
+ return stmt;
16859
+ }
16782
16860
  function isTagRow(row) {
16783
16861
  if (row === null || typeof row !== "object")
16784
16862
  return false;
@@ -16794,11 +16872,27 @@ function toTagEntry(row) {
16794
16872
  type,
16795
16873
  status,
16796
16874
  byteSize: row.byte_size,
16875
+ reasoningByteSize: row.reasoning_byte_size ?? 0,
16797
16876
  sessionId: row.session_id
16798
16877
  };
16799
16878
  }
16800
- function insertTag(db, sessionId, messageId, type, byteSize2, tagNumber) {
16801
- getInsertTagStatement(db).run(sessionId, messageId, type, byteSize2, tagNumber);
16879
+ function isTagNumberRow(row) {
16880
+ if (row === null || typeof row !== "object")
16881
+ return false;
16882
+ const r = row;
16883
+ return typeof r.tag_number === "number";
16884
+ }
16885
+ function isMaxTagNumberRow(row) {
16886
+ if (row === null || typeof row !== "object")
16887
+ return false;
16888
+ const r = row;
16889
+ return typeof r.max_tag_number === "number";
16890
+ }
16891
+ function escapeLikePattern(value) {
16892
+ return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
16893
+ }
16894
+ function insertTag(db, sessionId, messageId, type, byteSize2, tagNumber, reasoningByteSize = 0) {
16895
+ getInsertTagStatement(db).run(sessionId, messageId, type, byteSize2, reasoningByteSize, tagNumber);
16802
16896
  return tagNumber;
16803
16897
  }
16804
16898
  function updateTagStatus(db, sessionId, tagId, status) {
@@ -16807,15 +16901,30 @@ function updateTagStatus(db, sessionId, tagId, status) {
16807
16901
  function updateTagMessageId(db, sessionId, tagId, messageId) {
16808
16902
  getUpdateTagMessageIdStatement(db).run(messageId, sessionId, tagId);
16809
16903
  }
16904
+ function deleteTagsByMessageId(db, sessionId, messageId) {
16905
+ const escapedMessageId = escapeLikePattern(messageId);
16906
+ const textPartPattern = `${escapedMessageId}:p%`;
16907
+ const filePartPattern = `${escapedMessageId}:file%`;
16908
+ const tagNumbers = getTagNumbersByMessageIdStatement(db).all(sessionId, messageId, textPartPattern, filePartPattern).filter(isTagNumberRow).map((row) => row.tag_number);
16909
+ if (tagNumbers.length === 0) {
16910
+ return [];
16911
+ }
16912
+ getDeleteTagsByMessageIdStatement(db).run(sessionId, messageId, textPartPattern, filePartPattern);
16913
+ return tagNumbers;
16914
+ }
16915
+ function getMaxTagNumberBySession(db, sessionId) {
16916
+ const row = getMaxTagNumberBySessionStatement(db).get(sessionId);
16917
+ return isMaxTagNumberRow(row) ? row.max_tag_number : 0;
16918
+ }
16810
16919
  function getTagsBySession(db, sessionId) {
16811
- 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);
16920
+ const rows = db.prepare("SELECT id, message_id, type, status, byte_size, reasoning_byte_size, session_id, tag_number FROM tags WHERE session_id = ? ORDER BY tag_number ASC, id ASC").all(sessionId).filter(isTagRow);
16812
16921
  return rows.map(toTagEntry);
16813
16922
  }
16814
16923
  function getTopNBySize(db, sessionId, n) {
16815
16924
  if (n <= 0) {
16816
16925
  return [];
16817
16926
  }
16818
- const rows = db.prepare("SELECT id, message_id, type, status, byte_size, session_id, tag_number FROM tags WHERE session_id = ? AND status = 'active' ORDER BY byte_size DESC, tag_number ASC LIMIT ?").all(sessionId, n).filter(isTagRow);
16927
+ const rows = db.prepare("SELECT id, message_id, type, status, byte_size, reasoning_byte_size, session_id, tag_number FROM tags WHERE session_id = ? AND status = 'active' ORDER BY byte_size DESC, tag_number ASC LIMIT ?").all(sessionId, n).filter(isTagRow);
16819
16928
  return rows.map(toTagEntry);
16820
16929
  }
16821
16930
  // src/plugin/dream-timer.ts
@@ -17035,7 +17144,7 @@ function createTagger() {
17035
17144
  }
17036
17145
  return map2;
17037
17146
  }
17038
- function assignTag(sessionId, messageId, type, byteSize2, db) {
17147
+ function assignTag(sessionId, messageId, type, byteSize2, db, reasoningByteSize = 0) {
17039
17148
  const sessionAssignments = getSessionAssignments(sessionId);
17040
17149
  const existing = sessionAssignments.get(messageId);
17041
17150
  if (existing !== undefined) {
@@ -17044,7 +17153,7 @@ function createTagger() {
17044
17153
  const current = counters.get(sessionId) ?? 0;
17045
17154
  const next = current + 1;
17046
17155
  db.transaction(() => {
17047
- insertTag(db, sessionId, messageId, type, byteSize2, next);
17156
+ insertTag(db, sessionId, messageId, type, byteSize2, next, reasoningByteSize);
17048
17157
  getUpsertCounterStatement(db).run(sessionId, next);
17049
17158
  })();
17050
17159
  counters.set(sessionId, next);
@@ -17180,17 +17289,50 @@ var FORCE_MATERIALIZE_PERCENTAGE = 85;
17180
17289
  function getProactiveCompartmentTriggerPercentage(executeThresholdPercentage) {
17181
17290
  return Math.max(0, executeThresholdPercentage - PROACTIVE_TRIGGER_OFFSET_PERCENTAGE);
17182
17291
  }
17183
- function estimateProjectedPostDropPercentage(db, sessionId, usage) {
17184
- const pendingDrops = getPendingOps(db, sessionId).filter((op) => op.operation === "drop");
17185
- if (pendingDrops.length === 0)
17186
- return null;
17292
+ function estimateProjectedPostDropPercentage(db, sessionId, usage, autoDropToolAge, protectedTags, clearReasoningAge, clearedReasoningThroughTag) {
17187
17293
  const activeTags = getTagsBySession(db, sessionId).filter((tag) => tag.status === "active");
17188
- const totalActiveBytes = activeTags.reduce((sum, tag) => sum + tag.byteSize, 0);
17294
+ const totalActiveBytes = activeTags.reduce((sum, tag) => sum + tag.byteSize + tag.reasoningByteSize, 0);
17189
17295
  if (totalActiveBytes === 0)
17190
17296
  return null;
17297
+ let droppableBytes = 0;
17298
+ const pendingDrops = getPendingOps(db, sessionId).filter((op) => op.operation === "drop");
17191
17299
  const pendingDropTagIds = new Set(pendingDrops.map((op) => op.tagId));
17192
- const pendingDropBytes = activeTags.filter((tag) => pendingDropTagIds.has(tag.tagNumber)).reduce((sum, tag) => sum + tag.byteSize, 0);
17193
- const dropRatio = pendingDropBytes / totalActiveBytes;
17300
+ if (pendingDrops.length > 0) {
17301
+ droppableBytes += activeTags.filter((tag) => pendingDropTagIds.has(tag.tagNumber)).reduce((sum, tag) => sum + tag.byteSize + tag.reasoningByteSize, 0);
17302
+ }
17303
+ const maxTag = activeTags.reduce((max, t) => Math.max(max, t.tagNumber), 0);
17304
+ if (autoDropToolAge !== undefined && protectedTags !== undefined) {
17305
+ const toolAgeCutoff = maxTag - autoDropToolAge;
17306
+ const protectedCutoff = maxTag - protectedTags;
17307
+ for (const tag of activeTags) {
17308
+ if (pendingDropTagIds.has(tag.tagNumber))
17309
+ continue;
17310
+ if (tag.tagNumber > protectedCutoff)
17311
+ continue;
17312
+ if (tag.type === "tool" && tag.tagNumber <= toolAgeCutoff) {
17313
+ droppableBytes += tag.byteSize + tag.reasoningByteSize;
17314
+ }
17315
+ }
17316
+ }
17317
+ if (clearReasoningAge !== undefined && clearedReasoningThroughTag !== undefined) {
17318
+ const reasoningAgeCutoff = maxTag - clearReasoningAge;
17319
+ for (const tag of activeTags) {
17320
+ if (tag.type !== "message")
17321
+ continue;
17322
+ if (pendingDropTagIds.has(tag.tagNumber))
17323
+ continue;
17324
+ if (tag.tagNumber <= clearedReasoningThroughTag)
17325
+ continue;
17326
+ if (tag.tagNumber > reasoningAgeCutoff)
17327
+ continue;
17328
+ if (tag.reasoningByteSize > 0) {
17329
+ droppableBytes += tag.reasoningByteSize;
17330
+ }
17331
+ }
17332
+ }
17333
+ if (droppableBytes === 0)
17334
+ return null;
17335
+ const dropRatio = Math.min(droppableBytes / totalActiveBytes, 1);
17194
17336
  return usage.percentage * (1 - dropRatio);
17195
17337
  }
17196
17338
  var TAIL_INFO_DEFAULTS = {
@@ -17227,7 +17369,7 @@ function getUnsummarizedTailInfo(db, sessionId, compartmentTokenBudget) {
17227
17369
  }
17228
17370
  });
17229
17371
  }
17230
- function checkCompartmentTrigger(db, sessionId, sessionMeta, usage, _previousPercentage, executeThresholdPercentage, compartmentTokenBudget = DEFAULT_COMPARTMENT_TOKEN_BUDGET) {
17372
+ function checkCompartmentTrigger(db, sessionId, sessionMeta, usage, _previousPercentage, executeThresholdPercentage, compartmentTokenBudget = DEFAULT_COMPARTMENT_TOKEN_BUDGET, autoDropToolAge, protectedTagCount, clearReasoningAge) {
17231
17373
  if (sessionMeta.compartmentInProgress) {
17232
17374
  return { shouldFire: false };
17233
17375
  }
@@ -17235,7 +17377,7 @@ function checkCompartmentTrigger(db, sessionId, sessionMeta, usage, _previousPer
17235
17377
  if (!tailInfo.hasNewRawHistory) {
17236
17378
  return { shouldFire: false };
17237
17379
  }
17238
- const projectedPostDropPercentage = estimateProjectedPostDropPercentage(db, sessionId, usage);
17380
+ const projectedPostDropPercentage = estimateProjectedPostDropPercentage(db, sessionId, usage, autoDropToolAge, protectedTagCount, clearReasoningAge, sessionMeta.clearedReasoningThroughTag);
17239
17381
  const relativePostDropTarget = executeThresholdPercentage * POST_DROP_TARGET_RATIO;
17240
17382
  if (usage.percentage >= FORCE_COMPARTMENT_PERCENTAGE) {
17241
17383
  if (projectedPostDropPercentage !== null && projectedPostDropPercentage <= relativePostDropTarget) {
@@ -17679,6 +17821,85 @@ function getMessageUpdatedAssistantInfo(properties) {
17679
17821
  }
17680
17822
  };
17681
17823
  }
17824
+ function getMessageRemovedInfo(properties) {
17825
+ if (!isRecord(properties)) {
17826
+ return null;
17827
+ }
17828
+ if (typeof properties.sessionID !== "string" || typeof properties.messageID !== "string") {
17829
+ return null;
17830
+ }
17831
+ return {
17832
+ sessionID: properties.sessionID,
17833
+ messageID: properties.messageID
17834
+ };
17835
+ }
17836
+
17837
+ // src/hooks/magic-context/note-nudger.ts
17838
+ var NOTE_NUDGE_COOLDOWN_MS = 15 * 60 * 1000;
17839
+ var lastDeliveredAt = new Map;
17840
+ function getPersistedNoteNudgeDeliveredAt(_db, sessionId) {
17841
+ return lastDeliveredAt.get(sessionId) ?? 0;
17842
+ }
17843
+ function recordNoteNudgeDeliveryTime(sessionId) {
17844
+ lastDeliveredAt.set(sessionId, Date.now());
17845
+ }
17846
+ function onNoteTrigger(db, sessionId, trigger) {
17847
+ setPersistedNoteNudgeTrigger(db, sessionId);
17848
+ sessionLog(sessionId, `note-nudge: trigger fired (${trigger}), triggerPending=true`);
17849
+ }
17850
+ function peekNoteNudgeText(db, sessionId, currentUserMessageId, projectIdentity) {
17851
+ const state = getPersistedNoteNudge(db, sessionId);
17852
+ if (!state.triggerPending)
17853
+ return null;
17854
+ if (!state.triggerMessageId && currentUserMessageId) {
17855
+ setPersistedNoteNudgeTriggerMessageId(db, sessionId, currentUserMessageId);
17856
+ state.triggerMessageId = currentUserMessageId;
17857
+ }
17858
+ if (state.triggerMessageId && currentUserMessageId && state.triggerMessageId === currentUserMessageId) {
17859
+ sessionLog(sessionId, `note-nudge: deferring \u2014 current user message ${currentUserMessageId} is same as trigger-time message`);
17860
+ return null;
17861
+ }
17862
+ const deliveredAt = getPersistedNoteNudgeDeliveredAt(db, sessionId);
17863
+ if (deliveredAt > 0 && Date.now() - deliveredAt < NOTE_NUDGE_COOLDOWN_MS) {
17864
+ sessionLog(sessionId, `note-nudge: suppressing \u2014 last delivered ${Math.round((Date.now() - deliveredAt) / 1000)}s ago (cooldown ${NOTE_NUDGE_COOLDOWN_MS / 60000}m)`);
17865
+ clearPersistedNoteNudge(db, sessionId);
17866
+ return null;
17867
+ }
17868
+ const notes = getSessionNotes(db, sessionId);
17869
+ const readySmartCount = projectIdentity ? getReadySmartNotes(db, projectIdentity).length : 0;
17870
+ const totalCount = notes.length + readySmartCount;
17871
+ if (totalCount === 0) {
17872
+ sessionLog(sessionId, "note-nudge: triggerPending but no notes found, skipping");
17873
+ clearPersistedNoteNudge(db, sessionId);
17874
+ return null;
17875
+ }
17876
+ const parts = [];
17877
+ if (notes.length > 0) {
17878
+ parts.push(`${notes.length} deferred note${notes.length === 1 ? "" : "s"}`);
17879
+ }
17880
+ if (readySmartCount > 0) {
17881
+ parts.push(`${readySmartCount} ready smart note${readySmartCount === 1 ? "" : "s"}`);
17882
+ }
17883
+ sessionLog(sessionId, `note-nudge: delivering nudge for ${parts.join(" and ")}`);
17884
+ return `You have ${parts.join(" and ")}. Review with ctx_note read \u2014 some may be actionable now.`;
17885
+ }
17886
+ function markNoteNudgeDelivered(db, sessionId, text, messageId) {
17887
+ setPersistedDeliveredNoteNudge(db, sessionId, messageId ? text : "", messageId ?? "");
17888
+ recordNoteNudgeDeliveryTime(sessionId);
17889
+ sessionLog(sessionId, messageId ? `note-nudge: marked delivered, sticky anchor=${messageId}` : "note-nudge: marked delivered without anchor");
17890
+ }
17891
+ function getStickyNoteNudge(db, sessionId) {
17892
+ const state = getPersistedNoteNudge(db, sessionId);
17893
+ if (!state.stickyText || !state.stickyMessageId)
17894
+ return null;
17895
+ return { text: state.stickyText, messageId: state.stickyMessageId };
17896
+ }
17897
+ function clearNoteNudgeState(db, sessionId, options) {
17898
+ if (options?.persist !== false) {
17899
+ clearPersistedNoteNudge(db, sessionId);
17900
+ }
17901
+ lastDeliveredAt.delete(sessionId);
17902
+ }
17682
17903
 
17683
17904
  // src/hooks/magic-context/event-handler.ts
17684
17905
  var CONTEXT_USAGE_TTL_MS = 60 * 60 * 1000;
@@ -17690,6 +17911,46 @@ function evictExpiredUsageEntries(contextUsageMap) {
17690
17911
  }
17691
17912
  }
17692
17913
  }
17914
+ function cleanupRemovedMessageState(deps, sessionId, messageId) {
17915
+ return deps.db.transaction(() => {
17916
+ const removedTagNumbers = deleteTagsByMessageId(deps.db, sessionId, messageId);
17917
+ sessionLog(sessionId, `event message.removed: deleted ${removedTagNumbers.length} tag(s) for message ${messageId}`);
17918
+ const strippedPlaceholderRemoved = removeStrippedPlaceholderId(deps.db, sessionId, messageId);
17919
+ sessionLog(sessionId, strippedPlaceholderRemoved ? `event message.removed: removed ${messageId} from stripped placeholder ids` : `event message.removed: stripped placeholder ids unchanged for ${messageId}`);
17920
+ const persistedNudgePlacement = getPersistedNudgePlacement(deps.db, sessionId);
17921
+ const clearedNudgePlacement = persistedNudgePlacement?.messageId === messageId;
17922
+ if (clearedNudgePlacement) {
17923
+ clearPersistedNudgePlacement(deps.db, sessionId);
17924
+ }
17925
+ sessionLog(sessionId, clearedNudgePlacement ? `event message.removed: cleared nudge anchor for ${messageId}` : `event message.removed: nudge anchor unchanged for ${messageId}`);
17926
+ const persistedNoteNudge = getPersistedNoteNudge(deps.db, sessionId);
17927
+ const clearedNoteNudge = persistedNoteNudge.triggerMessageId === messageId || persistedNoteNudge.stickyMessageId === messageId;
17928
+ if (clearedNoteNudge) {
17929
+ clearPersistedNoteNudge(deps.db, sessionId);
17930
+ }
17931
+ sessionLog(sessionId, clearedNoteNudge ? `event message.removed: cleared note nudge state for ${messageId}` : `event message.removed: note nudge state unchanged for ${messageId}`);
17932
+ const persistedStickyTurnReminder = getPersistedStickyTurnReminder(deps.db, sessionId);
17933
+ const clearedStickyTurnReminder = persistedStickyTurnReminder?.messageId === messageId;
17934
+ if (clearedStickyTurnReminder) {
17935
+ clearPersistedStickyTurnReminder(deps.db, sessionId);
17936
+ }
17937
+ sessionLog(sessionId, clearedStickyTurnReminder ? `event message.removed: cleared sticky turn reminder for ${messageId}` : `event message.removed: sticky turn reminder unchanged for ${messageId}`);
17938
+ const currentWatermark = getPersistedReasoningWatermark(deps.db, sessionId);
17939
+ const maxRemainingTag = getMaxTagNumberBySession(deps.db, sessionId);
17940
+ if (currentWatermark > maxRemainingTag) {
17941
+ setPersistedReasoningWatermark(deps.db, sessionId, maxRemainingTag);
17942
+ sessionLog(sessionId, `event message.removed: reset reasoning watermark ${currentWatermark}\u2192${maxRemainingTag}`);
17943
+ } else {
17944
+ sessionLog(sessionId, `event message.removed: reasoning watermark unchanged at ${currentWatermark} (max tag ${maxRemainingTag})`);
17945
+ }
17946
+ const removedIndexedMessages = deleteIndexedMessage(deps.db, sessionId, messageId);
17947
+ sessionLog(sessionId, `event message.removed: deleted ${removedIndexedMessages} indexed message row(s) for ${messageId}`);
17948
+ return {
17949
+ clearedNudgePlacement,
17950
+ clearedNoteNudge
17951
+ };
17952
+ })();
17953
+ }
17693
17954
  function createEventHandler2(deps) {
17694
17955
  return async (input) => {
17695
17956
  evictExpiredUsageEntries(deps.contextUsageMap);
@@ -17763,7 +18024,7 @@ function createEventHandler2(deps) {
17763
18024
  const sessionMeta = getOrCreateSessionMeta(deps.db, info.sessionID);
17764
18025
  const previousPercentage = sessionMeta.lastContextPercentage;
17765
18026
  if (!sessionMeta.isSubagent) {
17766
- const triggerResult = checkCompartmentTrigger(deps.db, info.sessionID, sessionMeta, { percentage, inputTokens: totalInputTokens }, previousPercentage, resolveExecuteThreshold(deps.config.execute_threshold_percentage ?? 65, modelKey, 65));
18027
+ const triggerResult = checkCompartmentTrigger(deps.db, info.sessionID, sessionMeta, { percentage, inputTokens: totalInputTokens }, previousPercentage, resolveExecuteThreshold(deps.config.execute_threshold_percentage ?? 65, modelKey, 65), undefined, deps.config.auto_drop_tool_age ?? 100, deps.config.protected_tags, deps.config.clear_reasoning_age ?? 50);
17767
18028
  if (triggerResult.shouldFire) {
17768
18029
  sessionLog(info.sessionID, `compartment trigger: firing (reason=${triggerResult.reason})`);
17769
18030
  updateSessionMeta(deps.db, info.sessionID, {
@@ -17778,6 +18039,37 @@ function createEventHandler2(deps) {
17778
18039
  }
17779
18040
  return;
17780
18041
  }
18042
+ if (input.event.type === "message.removed") {
18043
+ const info = getMessageRemovedInfo(input.event.properties);
18044
+ if (!info) {
18045
+ const sessionId = properties ? resolveSessionId(properties) : null;
18046
+ if (sessionId) {
18047
+ sessionLog(sessionId, "event message.removed: no message removal info extracted from event");
18048
+ } else {
18049
+ log("[magic-context] event message.removed: no message removal info extracted from event");
18050
+ }
18051
+ return;
18052
+ }
18053
+ sessionLog(info.sessionID, `event message.removed: invalidating state for message ${info.messageID}`);
18054
+ try {
18055
+ const cleanup = cleanupRemovedMessageState(deps, info.sessionID, info.messageID);
18056
+ deps.tagger.cleanup(info.sessionID);
18057
+ sessionLog(info.sessionID, "event message.removed: invalidated tagger session cache");
18058
+ if (cleanup.clearedNudgePlacement) {
18059
+ deps.nudgePlacements.clear(info.sessionID, { persist: false });
18060
+ sessionLog(info.sessionID, "event message.removed: cleared in-memory nudge placement cache");
18061
+ }
18062
+ if (cleanup.clearedNoteNudge) {
18063
+ clearNoteNudgeState(deps.db, info.sessionID, { persist: false });
18064
+ sessionLog(info.sessionID, "event message.removed: cleared in-memory note nudge state");
18065
+ }
18066
+ deps.onSessionCacheInvalidated?.(info.sessionID);
18067
+ sessionLog(info.sessionID, "event message.removed: cleared session injection cache");
18068
+ } catch (error48) {
18069
+ sessionLog(info.sessionID, "event message.removed cleanup failed:", error48);
18070
+ }
18071
+ return;
18072
+ }
17781
18073
  if (input.event.type === "session.compacted") {
17782
18074
  const sessionId = resolveSessionId(properties);
17783
18075
  if (!sessionId) {
@@ -18622,72 +18914,6 @@ function createTextCompleteHandler() {
18622
18914
  };
18623
18915
  }
18624
18916
 
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
18917
  // src/hooks/magic-context/strip-content.ts
18692
18918
  var DROPPED_PLACEHOLDER_PATTERN = /^\[dropped \u00A7\d+\u00A7\]$/;
18693
18919
  var TAG_PREFIX_PATTERN = /^\u00A7\d+\u00A7\s*/;
@@ -20928,6 +21154,16 @@ function collectRelevantSourceTagIds(messages, assignments) {
20928
21154
  }
20929
21155
  return Array.from(relevantTagIds);
20930
21156
  }
21157
+ function getReasoningByteSize(parts) {
21158
+ let reasoningBytes = 0;
21159
+ for (const part of parts) {
21160
+ const content = part.thinking ?? part.text ?? "";
21161
+ if (content && content !== "[cleared]") {
21162
+ reasoningBytes += byteSize(content);
21163
+ }
21164
+ }
21165
+ return reasoningBytes;
21166
+ }
20931
21167
  function tagMessages(sessionId, messages, tagger, db) {
20932
21168
  const targets = new Map;
20933
21169
  const reasoningByMessage = new Map;
@@ -20988,7 +21224,8 @@ function tagMessages(sessionId, messages, tagger, db) {
20988
21224
  const thinkingParts = messageThinkingParts;
20989
21225
  const contentId = `${messageId}:p${partIndex}`;
20990
21226
  const existingTagId = resolver.resolve(messageId, "message", contentId, textOrdinal);
20991
- const tagId = tagger.assignTag(sessionId, contentId, "message", byteSize(textPart.text), db);
21227
+ const reasoningBytes = textOrdinal === 0 ? getReasoningByteSize(thinkingParts) : 0;
21228
+ const tagId = tagger.assignTag(sessionId, contentId, "message", byteSize(textPart.text), db, reasoningBytes);
20992
21229
  if (existingTagId === undefined) {
20993
21230
  const sourceContent = stripTagPrefix(textPart.text);
20994
21231
  if (sourceContent.trim().length > 0) {
@@ -21024,7 +21261,8 @@ function tagMessages(sessionId, messages, tagger, db) {
21024
21261
  if (isToolPartWithOutput(part)) {
21025
21262
  const toolPart = part;
21026
21263
  const thinkingParts = precedingThinkingParts;
21027
- const tagId = tagger.assignTag(sessionId, toolPart.callID, "tool", byteSize(toolPart.state.output), db);
21264
+ const reasoningBytes = getReasoningByteSize(thinkingParts);
21265
+ const tagId = tagger.assignTag(sessionId, toolPart.callID, "tool", byteSize(toolPart.state.output), db, reasoningBytes);
21028
21266
  messageTagNumbers.set(message, Math.max(messageTagNumbers.get(message) ?? 0, tagId));
21029
21267
  toolPart.state.output = prependTag(tagId, toolPart.state.output);
21030
21268
  toolTagByCallId.set(toolPart.callID, tagId);
@@ -21676,10 +21914,10 @@ function createNudgePlacementStore(db) {
21676
21914
  store.set(sessionId, persisted);
21677
21915
  return persisted;
21678
21916
  },
21679
- clear(sessionId) {
21917
+ clear(sessionId, options) {
21680
21918
  store.delete(sessionId);
21681
21919
  missingSessions.add(sessionId);
21682
- if (db) {
21920
+ if (db && options?.persist !== false) {
21683
21921
  clearPersistedNudgePlacement(db, sessionId);
21684
21922
  }
21685
21923
  }
@@ -21906,6 +22144,9 @@ function createEventHook(args) {
21906
22144
  args.commitSeenLastPass?.delete(sessionId);
21907
22145
  clearNoteNudgeState(args.db, sessionId);
21908
22146
  }
22147
+ if (input.event.type === "message.removed") {
22148
+ return;
22149
+ }
21909
22150
  const entry = args.contextUsageMap.get(sessionId);
21910
22151
  if (!entry)
21911
22152
  return;
@@ -21998,6 +22239,13 @@ Use \`ctx_note\` for deferred intentions \u2014 things to tackle later, not righ
21998
22239
  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
22240
  Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
22000
22241
  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.
22242
+ **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:
22243
+ - Can't remember where a related codebase or dependency lives \u2192 \`ctx_search(query="opencode source code path")\`
22244
+ - Forgot a prior architectural decision or constraint \u2192 \`ctx_search(query="why did we choose SQLite over postgres")\`
22245
+ - Need a config value, API key location, or environment detail \u2192 \`ctx_search(query="embedding provider configuration")\`
22246
+ - Looking for how something was implemented previously \u2192 \`ctx_search(query="how does the dreamer lease work")\`
22247
+ - Want to recall what was decided in an earlier conversation \u2192 \`ctx_search(query="dashboard release signing setup")\`
22248
+ \`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
22249
  NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
22002
22250
  NEVER drop user messages \u2014 they are short and will be summarized by compartmentalization automatically. Dropping them loses context the historian needs.
22003
22251
  NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
@@ -22006,7 +22254,14 @@ var BASE_INTRO_NO_REDUCE = `Messages and tool outputs are tagged with \xA7N\xA7
22006
22254
  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
22255
  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
22256
  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.`;
22257
+ 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.
22258
+ **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:
22259
+ - Can't remember where a related codebase or dependency lives \u2192 \`ctx_search(query="opencode source code path")\`
22260
+ - Forgot a prior architectural decision or constraint \u2192 \`ctx_search(query="why did we choose SQLite over postgres")\`
22261
+ - Need a config value, API key location, or environment detail \u2192 \`ctx_search(query="embedding provider configuration")\`
22262
+ - Looking for how something was implemented previously \u2192 \`ctx_search(query="how does the dreamer lease work")\`
22263
+ - Want to recall what was decided in an earlier conversation \u2192 \`ctx_search(query="dashboard release signing setup")\`
22264
+ \`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
22265
  var SISYPHUS_SECTION = `
22011
22266
  ### Reduction Triggers
22012
22267
  - After collecting background agent results (explore/librarian) \u2014 drop raw outputs once you extracted what you need.
@@ -23210,7 +23465,8 @@ async function getSemanticScores(args) {
23210
23465
  function getFtsMatches(args) {
23211
23466
  try {
23212
23467
  return searchMemoriesFTS(args.db, args.projectPath, args.query, args.limit);
23213
- } catch {
23468
+ } catch (error48) {
23469
+ log(`[search] FTS query failed for "${args.query}": ${error48 instanceof Error ? error48.message : String(error48)}`);
23214
23470
  return [];
23215
23471
  }
23216
23472
  }