@cortexkit/opencode-magic-context 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +14 -3
  2. package/dist/agents/magic-context-prompt.d.ts +1 -1
  3. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  4. package/dist/config/schema/magic-context.d.ts +6 -14
  5. package/dist/config/schema/magic-context.d.ts.map +1 -1
  6. package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
  7. package/dist/features/magic-context/dreamer/queue.d.ts +0 -2
  8. package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
  9. package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
  10. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  11. package/dist/features/magic-context/index.d.ts +1 -0
  12. package/dist/features/magic-context/index.d.ts.map +1 -1
  13. package/dist/features/magic-context/memory/embedding-cache.d.ts +8 -0
  14. package/dist/features/magic-context/memory/embedding-cache.d.ts.map +1 -0
  15. package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
  16. package/dist/features/magic-context/memory/index.d.ts +1 -0
  17. package/dist/features/magic-context/memory/index.d.ts.map +1 -1
  18. package/dist/features/magic-context/memory/storage-memory-fts.d.ts +8 -0
  19. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  20. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  21. package/dist/features/magic-context/message-index.d.ts +5 -0
  22. package/dist/features/magic-context/message-index.d.ts.map +1 -0
  23. package/dist/features/magic-context/scheduler.d.ts +1 -1
  24. package/dist/features/magic-context/scheduler.d.ts.map +1 -1
  25. package/dist/features/magic-context/search.d.ts +36 -0
  26. package/dist/features/magic-context/search.d.ts.map +1 -0
  27. package/dist/features/magic-context/sidekick/agent.d.ts +1 -1
  28. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  29. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  30. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  31. package/dist/features/magic-context/storage-ops.d.ts +0 -1
  32. package/dist/features/magic-context/storage-ops.d.ts.map +1 -1
  33. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  34. package/dist/features/magic-context/storage.d.ts +2 -2
  35. package/dist/features/magic-context/storage.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/hook-handlers.d.ts +2 -0
  39. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  40. package/dist/hooks/magic-context/hook.d.ts +1 -0
  41. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  42. package/dist/hooks/magic-context/read-session-chunk.d.ts +6 -0
  43. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/system-prompt-hash.d.ts +1 -0
  45. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/tag-id-fallback.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/transform-context-state.d.ts +1 -1
  48. package/dist/hooks/magic-context/transform-context-state.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/transform.d.ts +1 -0
  50. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2403 -1839
  53. package/dist/plugin/dream-timer.d.ts +14 -0
  54. package/dist/plugin/dream-timer.d.ts.map +1 -0
  55. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  56. package/dist/plugin/tool-registry.d.ts.map +1 -1
  57. package/dist/shared/jsonc-parser.d.ts +0 -9
  58. package/dist/shared/jsonc-parser.d.ts.map +1 -1
  59. package/dist/tools/ctx-memory/constants.d.ts +1 -1
  60. package/dist/tools/ctx-memory/constants.d.ts.map +1 -1
  61. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  62. package/dist/tools/ctx-memory/types.d.ts +3 -10
  63. package/dist/tools/ctx-memory/types.d.ts.map +1 -1
  64. package/dist/tools/ctx-search/constants.d.ts +4 -0
  65. package/dist/tools/ctx-search/constants.d.ts.map +1 -0
  66. package/dist/tools/ctx-search/index.d.ts +4 -0
  67. package/dist/tools/ctx-search/index.d.ts.map +1 -0
  68. package/dist/tools/ctx-search/tools.d.ts +4 -0
  69. package/dist/tools/ctx-search/tools.d.ts.map +1 -0
  70. package/dist/tools/ctx-search/types.d.ts +19 -0
  71. package/dist/tools/ctx-search/types.d.ts.map +1 -0
  72. package/dist/tools/index.d.ts +1 -0
  73. package/dist/tools/index.d.ts.map +1 -1
  74. package/package.json +1 -1
  75. package/dist/plugin/hooks/create-tag-content-resolver.d.ts +0 -7
  76. package/dist/plugin/hooks/create-tag-content-resolver.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -31,13 +31,6 @@ import { join } from "path";
31
31
 
32
32
  // src/shared/jsonc-parser.ts
33
33
  import { existsSync, readFileSync } from "fs";
34
-
35
- // src/shared/error-message.ts
36
- function getErrorMessage(error) {
37
- return error instanceof Error ? error.message : String(error);
38
- }
39
-
40
- // src/shared/jsonc-parser.ts
41
34
  function stripJsonComments(content) {
42
35
  let result = "";
43
36
  let inString = false;
@@ -13733,19 +13726,6 @@ var DEFAULT_DREAMER_TASKS = [
13733
13726
  "archive-stale",
13734
13727
  "improve"
13735
13728
  ];
13736
- var DreamingConfigSchema = exports_external.object({
13737
- enabled: exports_external.boolean().default(false),
13738
- schedule: exports_external.string().default("02:00-06:00"),
13739
- max_runtime_minutes: exports_external.number().min(10).default(120),
13740
- tasks: exports_external.array(DreamingTaskSchema).default(DEFAULT_DREAMER_TASKS),
13741
- task_timeout_minutes: exports_external.number().min(5).default(20)
13742
- }).default({
13743
- enabled: false,
13744
- schedule: "02:00-06:00",
13745
- max_runtime_minutes: 120,
13746
- tasks: DEFAULT_DREAMER_TASKS,
13747
- task_timeout_minutes: 20
13748
- });
13749
13729
  var DreamerConfigSchema = AgentOverrideConfigSchema.merge(exports_external.object({
13750
13730
  enabled: exports_external.boolean().default(false),
13751
13731
  schedule: exports_external.string().default("02:00-06:00"),
@@ -13799,6 +13779,7 @@ var EmbeddingConfigSchema = BaseEmbeddingConfigSchema.transform((data) => {
13799
13779
  });
13800
13780
  var MagicContextConfigSchema = exports_external.object({
13801
13781
  enabled: exports_external.boolean().default(false),
13782
+ ctx_reduce_enabled: exports_external.boolean().default(true),
13802
13783
  historian: AgentOverrideConfigSchema.optional(),
13803
13784
  dreamer: DreamerConfigSchema.optional(),
13804
13785
  cache_ttl: exports_external.union([exports_external.string(), exports_external.object({ default: exports_external.string() }).catchall(exports_external.string())]).default("5m"),
@@ -13951,7 +13932,6 @@ You run during scheduled dream windows to maintain a project's cross-session mem
13951
13932
 
13952
13933
  **Memory operations** (ctx_memory with extended dreamer actions):
13953
13934
  - \`action="list"\` \u2014 browse all active memories, optionally filter by category
13954
- - \`action="search", query="..."\` \u2014 semantic search across memories
13955
13935
  - \`action="update", id=N, content="..."\` \u2014 rewrite a memory's content
13956
13936
  - \`action="merge", ids=[N,M,...], content="...", category="..."\` \u2014 consolidate duplicates into one canonical memory
13957
13937
  - \`action="archive", id=N, reason="..."\` \u2014 archive a stale memory with provenance
@@ -14033,7 +14013,7 @@ Check verifiable memories against actual repository state. Update stale wording,
14033
14013
  ### Verification examples
14034
14014
  - Memory: "compartment_token_budget defaults to 20000" \u2192 grep schema for \`compartment_token_budget\`, check \`.default(...)\`
14035
14015
  - Memory: "Durable state lives in ~/.local/share/opencode/storage/plugin/magic-context/context.db" \u2192 check storage-db.ts for the path construction
14036
- - Memory: "ctx_memory search combines semantic and FTS" \u2192 grep for ctx_memory tool definition, verify search action exists
14016
+ - Memory: "ctx_search searches memories, facts, and history" \u2192 grep for ctx_search tool definition and unified search implementation
14037
14017
 
14038
14018
  ### Success criteria
14039
14019
  - All CONFIG_DEFAULTS memories match actual schema defaults.
@@ -14549,12 +14529,12 @@ function extractLatestAssistantText(messages) {
14549
14529
  // src/features/magic-context/sidekick/agent.ts
14550
14530
  var SIDEKICK_SYSTEM_PROMPT = `You are Sidekick, a focused memory-retrieval subagent for an AI coding assistant.
14551
14531
 
14552
- Your job is to search project memories and return a concise augmentation for the user's prompt.
14532
+ Your job is to search project memories, session facts, and conversation history and return a concise augmentation for the user's prompt.
14553
14533
 
14554
14534
  Rules:
14555
- - Use ctx_memory(action="search", query="...") to look up relevant memories before answering.
14535
+ - Use ctx_search(query="...") to look up relevant memories, facts, and history before answering.
14556
14536
  - Run targeted searches only; prefer 1-3 precise queries.
14557
- - Return only memories that materially help with the user's prompt.
14537
+ - Return only findings that materially help with the user's prompt.
14558
14538
  - If nothing useful is found, respond with exactly: No relevant memories found.
14559
14539
  - Keep the response focused and concise.
14560
14540
  - Do not invent facts or speculate beyond what memories support.`;
@@ -14618,6 +14598,24 @@ async function runSidekick(deps) {
14618
14598
  }
14619
14599
 
14620
14600
  // src/features/magic-context/compartment-storage.ts
14601
+ var insertCompartmentStatements = new WeakMap;
14602
+ var insertFactStatements = new WeakMap;
14603
+ function getInsertCompartmentStatement(db) {
14604
+ let stmt = insertCompartmentStatements.get(db);
14605
+ if (!stmt) {
14606
+ stmt = db.prepare("INSERT INTO compartments (session_id, sequence, start_message, end_message, start_message_id, end_message_id, title, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
14607
+ insertCompartmentStatements.set(db, stmt);
14608
+ }
14609
+ return stmt;
14610
+ }
14611
+ function getInsertFactStatement(db) {
14612
+ let stmt = insertFactStatements.get(db);
14613
+ if (!stmt) {
14614
+ stmt = db.prepare("INSERT INTO session_facts (session_id, category, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?)");
14615
+ insertFactStatements.set(db, stmt);
14616
+ }
14617
+ return stmt;
14618
+ }
14621
14619
  function isCompartmentRow(row) {
14622
14620
  if (row === null || typeof row !== "object")
14623
14621
  return false;
@@ -14630,6 +14628,18 @@ function isSessionFactRow(row) {
14630
14628
  const candidate = row;
14631
14629
  return typeof candidate.id === "number" && typeof candidate.session_id === "string" && typeof candidate.category === "string" && typeof candidate.content === "string" && typeof candidate.created_at === "number" && typeof candidate.updated_at === "number";
14632
14630
  }
14631
+ function insertCompartmentRows(db, sessionId, compartments, now) {
14632
+ const stmt = getInsertCompartmentStatement(db);
14633
+ for (const compartment of compartments) {
14634
+ stmt.run(sessionId, compartment.sequence, compartment.startMessage, compartment.endMessage, compartment.startMessageId, compartment.endMessageId, compartment.title, compartment.content, now);
14635
+ }
14636
+ }
14637
+ function insertFactRows(db, sessionId, facts, now) {
14638
+ const stmt = getInsertFactStatement(db);
14639
+ for (const fact of facts) {
14640
+ stmt.run(sessionId, fact.category, fact.content, now, now);
14641
+ }
14642
+ }
14633
14643
  function toCompartment(row) {
14634
14644
  return {
14635
14645
  id: row.id,
@@ -14667,20 +14677,14 @@ function appendCompartments(db, sessionId, compartments) {
14667
14677
  return;
14668
14678
  const now = Date.now();
14669
14679
  db.transaction(() => {
14670
- const stmt = db.prepare("INSERT INTO compartments (session_id, sequence, start_message, end_message, start_message_id, end_message_id, title, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
14671
- for (const c of compartments) {
14672
- stmt.run(sessionId, c.sequence, c.startMessage, c.endMessage, c.startMessageId, c.endMessageId, c.title, c.content, now);
14673
- }
14680
+ insertCompartmentRows(db, sessionId, compartments, now);
14674
14681
  })();
14675
14682
  }
14676
14683
  function replaceSessionFacts(db, sessionId, facts) {
14677
14684
  const now = Date.now();
14678
14685
  db.transaction(() => {
14679
14686
  db.prepare("DELETE FROM session_facts WHERE session_id = ?").run(sessionId);
14680
- const stmt = db.prepare("INSERT INTO session_facts (session_id, category, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?)");
14681
- for (const f of facts) {
14682
- stmt.run(sessionId, f.category, f.content, now, now);
14683
- }
14687
+ insertFactRows(db, sessionId, facts, now);
14684
14688
  db.prepare("UPDATE session_meta SET memory_block_cache = '', memory_block_count = 0 WHERE session_id = ?").run(sessionId);
14685
14689
  })();
14686
14690
  }
@@ -14693,14 +14697,8 @@ function replaceAllCompartmentState(db, sessionId, compartments, facts) {
14693
14697
  db.transaction(() => {
14694
14698
  db.prepare("DELETE FROM compartments WHERE session_id = ?").run(sessionId);
14695
14699
  db.prepare("DELETE FROM session_facts WHERE session_id = ?").run(sessionId);
14696
- const compartmentStmt = db.prepare("INSERT INTO compartments (session_id, sequence, start_message, end_message, start_message_id, end_message_id, title, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
14697
- for (const c of compartments) {
14698
- compartmentStmt.run(sessionId, c.sequence, c.startMessage, c.endMessage, c.startMessageId, c.endMessageId, c.title, c.content, now);
14699
- }
14700
- const factStmt = db.prepare("INSERT INTO session_facts (session_id, category, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?)");
14701
- for (const f of facts) {
14702
- factStmt.run(sessionId, f.category, f.content, now, now);
14703
- }
14700
+ insertCompartmentRows(db, sessionId, compartments, now);
14701
+ insertFactRows(db, sessionId, facts, now);
14704
14702
  db.prepare("UPDATE session_meta SET memory_block_cache = '', memory_block_count = 0 WHERE session_id = ?").run(sessionId);
14705
14703
  })();
14706
14704
  }
@@ -14777,14 +14775,8 @@ function promoteRecompStaging(db, sessionId) {
14777
14775
  return null;
14778
14776
  db.prepare("DELETE FROM compartments WHERE session_id = ?").run(sessionId);
14779
14777
  db.prepare("DELETE FROM session_facts WHERE session_id = ?").run(sessionId);
14780
- const compartmentStmt = db.prepare("INSERT INTO compartments (session_id, sequence, start_message, end_message, start_message_id, end_message_id, title, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
14781
- for (const c of staging.compartments) {
14782
- compartmentStmt.run(sessionId, c.sequence, c.startMessage, c.endMessage, c.startMessageId, c.endMessageId, c.title, c.content, now);
14783
- }
14784
- const factStmt = db.prepare("INSERT INTO session_facts (session_id, category, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?)");
14785
- for (const f of staging.facts) {
14786
- factStmt.run(sessionId, f.category, f.content, now, now);
14787
- }
14778
+ insertCompartmentRows(db, sessionId, staging.compartments, now);
14779
+ insertFactRows(db, sessionId, staging.facts, now);
14788
14780
  db.prepare("DELETE FROM recomp_compartments WHERE session_id = ?").run(sessionId);
14789
14781
  db.prepare("DELETE FROM recomp_facts WHERE session_id = ?").run(sessionId);
14790
14782
  db.prepare("UPDATE session_meta SET memory_block_cache = '', memory_block_count = 0 WHERE session_id = ?").run(sessionId);
@@ -14945,1705 +14937,1929 @@ function buildCompartmentAgentPrompt(existingState, inputSource) {
14945
14937
  `);
14946
14938
  }
14947
14939
 
14948
- // src/plugin/event.ts
14949
- function createEventHandler(args) {
14950
- return async (input) => {
14951
- await args.magicContext?.event?.(input);
14952
- };
14940
+ // src/features/magic-context/dreamer/storage-dream-state.ts
14941
+ var getDreamStateStatements = new WeakMap;
14942
+ var setDreamStateStatements = new WeakMap;
14943
+ var deleteDreamStateStatements = new WeakMap;
14944
+ function getGetDreamStateStatement(db) {
14945
+ let stmt = getDreamStateStatements.get(db);
14946
+ if (!stmt) {
14947
+ stmt = db.prepare("SELECT value FROM dream_state WHERE key = ?");
14948
+ getDreamStateStatements.set(db, stmt);
14949
+ }
14950
+ return stmt;
14953
14951
  }
14954
- // src/features/magic-context/storage-db.ts
14955
- import { Database } from "bun:sqlite";
14956
- import { mkdirSync } from "fs";
14957
- import { join as join4 } from "path";
14958
-
14959
- // src/shared/data-path.ts
14960
- import * as os2 from "os";
14961
- import * as path2 from "path";
14962
- function getDataDir() {
14963
- return process.env.XDG_DATA_HOME ?? path2.join(os2.homedir(), ".local", "share");
14952
+ function getSetDreamStateStatement(db) {
14953
+ let stmt = setDreamStateStatements.get(db);
14954
+ if (!stmt) {
14955
+ stmt = db.prepare("INSERT INTO dream_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value");
14956
+ setDreamStateStatements.set(db, stmt);
14957
+ }
14958
+ return stmt;
14964
14959
  }
14965
- function getOpenCodeStorageDir() {
14966
- return path2.join(getDataDir(), "opencode", "storage");
14960
+ function getDeleteDreamStateStatement(db) {
14961
+ let stmt = deleteDreamStateStatements.get(db);
14962
+ if (!stmt) {
14963
+ stmt = db.prepare("DELETE FROM dream_state WHERE key = ?");
14964
+ deleteDreamStateStatements.set(db, stmt);
14965
+ }
14966
+ return stmt;
14967
14967
  }
14968
-
14969
- // src/features/magic-context/storage-db.ts
14970
- var databases = new Map;
14971
- var FALLBACK_DATABASE_KEY = "__fallback__:memory:";
14972
- var persistenceByDatabase = new WeakMap;
14973
- var persistenceErrorByDatabase = new WeakMap;
14974
- function resolveDatabasePath() {
14975
- const dbDir = join4(getOpenCodeStorageDir(), "plugin", "magic-context");
14976
- return { dbDir, dbPath: join4(dbDir, "context.db") };
14968
+ function getDreamState(db, key) {
14969
+ const row = getGetDreamStateStatement(db).get(key);
14970
+ return typeof row?.value === "string" ? row.value : null;
14971
+ }
14972
+ function setDreamState(db, key, value) {
14973
+ getSetDreamStateStatement(db).run(key, value);
14974
+ }
14975
+ function deleteDreamState(db, key) {
14976
+ getDeleteDreamStateStatement(db).run(key);
14977
14977
  }
14978
- function initializeDatabase(db) {
14979
- db.run("PRAGMA journal_mode=WAL");
14980
- db.run("PRAGMA busy_timeout=5000");
14981
- db.run("PRAGMA foreign_keys=ON");
14982
- db.run(`
14983
- CREATE TABLE IF NOT EXISTS tags (
14984
- id INTEGER PRIMARY KEY AUTOINCREMENT,
14985
- session_id TEXT,
14986
- message_id TEXT,
14987
- type TEXT,
14988
- status TEXT DEFAULT 'active',
14989
- byte_size INTEGER,
14990
- tag_number INTEGER,
14991
- UNIQUE(session_id, tag_number)
14992
- );
14993
-
14994
- CREATE TABLE IF NOT EXISTS pending_ops (
14995
- id INTEGER PRIMARY KEY AUTOINCREMENT,
14996
- session_id TEXT,
14997
- tag_id INTEGER,
14998
- operation TEXT,
14999
- queued_at INTEGER
15000
- );
15001
14978
 
15002
- CREATE TABLE IF NOT EXISTS source_contents (
15003
- tag_id INTEGER,
15004
- session_id TEXT,
15005
- content TEXT,
15006
- created_at INTEGER,
15007
- PRIMARY KEY(session_id, tag_id)
15008
- );
14979
+ // src/features/magic-context/dreamer/lease.ts
14980
+ var LEASE_HOLDER_KEY = "dreaming_lease_holder";
14981
+ var LEASE_HEARTBEAT_KEY = "dreaming_lease_heartbeat";
14982
+ var LEASE_EXPIRY_KEY = "dreaming_lease_expiry";
14983
+ var LEASE_DURATION_MS = 2 * 60 * 1000;
14984
+ function getLeaseExpiry(db) {
14985
+ const value = getDreamState(db, LEASE_EXPIRY_KEY);
14986
+ if (!value) {
14987
+ return null;
14988
+ }
14989
+ const expiry = Number(value);
14990
+ return Number.isFinite(expiry) ? expiry : null;
14991
+ }
14992
+ function isLeaseActive(db) {
14993
+ const expiry = getLeaseExpiry(db);
14994
+ return expiry !== null && expiry > Date.now();
14995
+ }
14996
+ function getLeaseHolder(db) {
14997
+ return getDreamState(db, LEASE_HOLDER_KEY);
14998
+ }
14999
+ function acquireLease(db, holderId) {
15000
+ return db.transaction(() => {
15001
+ if (isLeaseActive(db)) {
15002
+ const existingHolder = getLeaseHolder(db);
15003
+ if (existingHolder && existingHolder !== holderId) {
15004
+ return false;
15005
+ }
15006
+ }
15007
+ const now = Date.now();
15008
+ setDreamState(db, LEASE_HOLDER_KEY, holderId);
15009
+ setDreamState(db, LEASE_HEARTBEAT_KEY, String(now));
15010
+ setDreamState(db, LEASE_EXPIRY_KEY, String(now + LEASE_DURATION_MS));
15011
+ return true;
15012
+ })();
15013
+ }
15014
+ function renewLease(db, holderId) {
15015
+ return db.transaction(() => {
15016
+ if (getLeaseHolder(db) !== holderId || !isLeaseActive(db)) {
15017
+ return false;
15018
+ }
15019
+ const now = Date.now();
15020
+ setDreamState(db, LEASE_HEARTBEAT_KEY, String(now));
15021
+ setDreamState(db, LEASE_EXPIRY_KEY, String(now + LEASE_DURATION_MS));
15022
+ return true;
15023
+ })();
15024
+ }
15025
+ function releaseLease(db, holderId) {
15026
+ db.transaction(() => {
15027
+ if (getLeaseHolder(db) !== holderId) {
15028
+ return;
15029
+ }
15030
+ deleteDreamState(db, LEASE_HOLDER_KEY);
15031
+ deleteDreamState(db, LEASE_HEARTBEAT_KEY);
15032
+ deleteDreamState(db, LEASE_EXPIRY_KEY);
15033
+ })();
15034
+ }
15035
+ // src/features/magic-context/dreamer/queue.ts
15036
+ function enqueueDream(db, projectIdentity, reason) {
15037
+ const now = Date.now();
15038
+ return db.transaction(() => {
15039
+ const existing = db.query("SELECT id FROM dream_queue WHERE project_path = ?").get(projectIdentity);
15040
+ if (existing) {
15041
+ return null;
15042
+ }
15043
+ const result = db.prepare("INSERT INTO dream_queue (project_path, reason, enqueued_at) VALUES (?, ?, ?)").run(projectIdentity, reason, now);
15044
+ return {
15045
+ id: Number(result.lastInsertRowid),
15046
+ projectIdentity,
15047
+ reason,
15048
+ enqueuedAt: now,
15049
+ startedAt: null
15050
+ };
15051
+ })();
15052
+ }
15053
+ function peekQueue(db) {
15054
+ const row = db.query("SELECT id, project_path, reason, enqueued_at FROM dream_queue WHERE started_at IS NULL ORDER BY enqueued_at ASC LIMIT 1").get();
15055
+ if (!row)
15056
+ return null;
15057
+ return {
15058
+ id: row.id,
15059
+ projectIdentity: row.project_path,
15060
+ reason: row.reason,
15061
+ enqueuedAt: row.enqueued_at,
15062
+ startedAt: null
15063
+ };
15064
+ }
15065
+ function dequeueNext(db) {
15066
+ const now = Date.now();
15067
+ return db.transaction(() => {
15068
+ const entry = peekQueue(db);
15069
+ if (!entry)
15070
+ return null;
15071
+ const result = db.prepare("UPDATE dream_queue SET started_at = ? WHERE id = ? AND started_at IS NULL").run(now, entry.id);
15072
+ if (result.changes === 0)
15073
+ return null;
15074
+ return { ...entry, startedAt: now };
15075
+ })();
15076
+ }
15077
+ function removeDreamEntry(db, id) {
15078
+ db.prepare("DELETE FROM dream_queue WHERE id = ?").run(id);
15079
+ }
15080
+ function resetDreamEntry(db, id) {
15081
+ db.prepare("UPDATE dream_queue SET started_at = NULL, retry_count = COALESCE(retry_count, 0) + 1 WHERE id = ?").run(id);
15082
+ }
15083
+ function getEntryRetryCount(db, id) {
15084
+ const row = db.query("SELECT retry_count FROM dream_queue WHERE id = ?").get(id);
15085
+ return row?.retry_count ?? 0;
15086
+ }
15087
+ function clearStaleEntries(db, maxAgeMs) {
15088
+ const cutoff = Date.now() - maxAgeMs;
15089
+ const result = db.prepare("DELETE FROM dream_queue WHERE started_at IS NOT NULL AND started_at < ?").run(cutoff);
15090
+ return result.changes;
15091
+ }
15092
+ // src/features/magic-context/dreamer/runner.ts
15093
+ import { existsSync as existsSync3 } from "fs";
15094
+ import { join as join3 } from "path";
15009
15095
 
15010
- CREATE TABLE IF NOT EXISTS compartments (
15011
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15012
- session_id TEXT NOT NULL,
15013
- sequence INTEGER NOT NULL,
15014
- start_message INTEGER NOT NULL,
15015
- end_message INTEGER NOT NULL,
15016
- start_message_id TEXT DEFAULT '',
15017
- end_message_id TEXT DEFAULT '',
15018
- title TEXT NOT NULL,
15019
- content TEXT NOT NULL,
15020
- created_at INTEGER NOT NULL,
15021
- UNIQUE(session_id, sequence)
15022
- );
15096
+ // src/shared/error-message.ts
15097
+ function getErrorMessage(error48) {
15098
+ return error48 instanceof Error ? error48.message : String(error48);
15099
+ }
15023
15100
 
15024
- CREATE TABLE IF NOT EXISTS session_facts (
15025
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15026
- session_id TEXT NOT NULL,
15027
- category TEXT NOT NULL,
15028
- content TEXT NOT NULL,
15029
- created_at INTEGER NOT NULL,
15030
- updated_at INTEGER NOT NULL
15031
- );
15032
-
15033
- CREATE TABLE IF NOT EXISTS session_notes (
15034
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15035
- session_id TEXT NOT NULL,
15036
- content TEXT NOT NULL,
15037
- created_at INTEGER NOT NULL
15038
- );
15039
-
15040
- CREATE TABLE IF NOT EXISTS memories (
15041
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15042
- project_path TEXT NOT NULL,
15043
- category TEXT NOT NULL,
15044
- content TEXT NOT NULL,
15045
- normalized_hash TEXT NOT NULL,
15046
- source_session_id TEXT,
15047
- source_type TEXT DEFAULT 'historian',
15048
- seen_count INTEGER DEFAULT 1,
15049
- retrieval_count INTEGER DEFAULT 0,
15050
- first_seen_at INTEGER NOT NULL,
15051
- created_at INTEGER NOT NULL,
15052
- updated_at INTEGER NOT NULL,
15053
- last_seen_at INTEGER NOT NULL,
15054
- last_retrieved_at INTEGER,
15055
- status TEXT DEFAULT 'active',
15056
- expires_at INTEGER,
15057
- verification_status TEXT DEFAULT 'unverified',
15058
- verified_at INTEGER,
15059
- superseded_by_memory_id INTEGER,
15060
- merged_from TEXT,
15061
- metadata_json TEXT,
15062
- UNIQUE(project_path, category, normalized_hash)
15063
- );
15064
-
15065
- CREATE TABLE IF NOT EXISTS memory_embeddings (
15066
- memory_id INTEGER PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
15067
- embedding BLOB NOT NULL,
15068
- model_id TEXT
15069
- );
15070
-
15071
- CREATE TABLE IF NOT EXISTS dream_state (
15072
- key TEXT PRIMARY KEY,
15073
- value TEXT NOT NULL
15074
- );
15075
-
15076
- CREATE TABLE IF NOT EXISTS dream_queue (
15077
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15078
- project_path TEXT NOT NULL,
15079
- reason TEXT NOT NULL,
15080
- enqueued_at INTEGER NOT NULL,
15081
- started_at INTEGER,
15082
- retry_count INTEGER DEFAULT 0
15083
- );
15084
- CREATE INDEX IF NOT EXISTS idx_dream_queue_project ON dream_queue(project_path);
15085
-
15086
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
15087
- content,
15088
- category,
15089
- content='memories',
15090
- content_rowid='id',
15091
- tokenize='porter unicode61'
15092
- );
15093
-
15094
- CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
15095
- INSERT INTO memories_fts(rowid, content, category) VALUES (new.id, new.content, new.category);
15096
- END;
15097
-
15098
- CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
15099
- INSERT INTO memories_fts(memories_fts, rowid, content, category) VALUES ('delete', old.id, old.content, old.category);
15100
- END;
15101
-
15102
- CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
15103
- INSERT INTO memories_fts(memories_fts, rowid, content, category) VALUES ('delete', old.id, old.content, old.category);
15104
- INSERT INTO memories_fts(rowid, content, category) VALUES (new.id, new.content, new.category);
15105
- END;
15106
-
15107
- CREATE TABLE IF NOT EXISTS session_meta (
15108
- session_id TEXT PRIMARY KEY,
15109
- last_response_time INTEGER,
15110
- cache_ttl TEXT,
15111
- counter INTEGER DEFAULT 0,
15112
- last_nudge_tokens INTEGER DEFAULT 0,
15113
- last_nudge_band TEXT DEFAULT '',
15114
- last_transform_error TEXT DEFAULT '',
15115
- nudge_anchor_message_id TEXT DEFAULT '',
15116
- nudge_anchor_text TEXT DEFAULT '',
15117
- sticky_turn_reminder_text TEXT DEFAULT '',
15118
- sticky_turn_reminder_message_id TEXT DEFAULT '',
15119
- is_subagent INTEGER DEFAULT 0,
15120
- last_context_percentage REAL DEFAULT 0,
15121
- last_input_tokens INTEGER DEFAULT 0,
15122
- times_execute_threshold_reached INTEGER DEFAULT 0,
15123
- compartment_in_progress INTEGER DEFAULT 0,
15124
- system_prompt_hash TEXT DEFAULT '',
15125
- memory_block_cache TEXT DEFAULT '',
15126
- memory_block_count INTEGER DEFAULT 0
15127
- );
15128
-
15129
- CREATE INDEX IF NOT EXISTS idx_tags_session_tag_number ON tags(session_id, tag_number);
15130
- CREATE INDEX IF NOT EXISTS idx_pending_ops_session ON pending_ops(session_id);
15131
- CREATE INDEX IF NOT EXISTS idx_pending_ops_session_tag_id ON pending_ops(session_id, tag_id);
15132
- CREATE INDEX IF NOT EXISTS idx_source_contents_session ON source_contents(session_id);
15133
-
15134
- CREATE TABLE IF NOT EXISTS recomp_compartments (
15135
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15136
- session_id TEXT NOT NULL,
15137
- sequence INTEGER NOT NULL,
15138
- start_message INTEGER NOT NULL,
15139
- end_message INTEGER NOT NULL,
15140
- start_message_id TEXT DEFAULT '',
15141
- end_message_id TEXT DEFAULT '',
15142
- title TEXT NOT NULL,
15143
- content TEXT NOT NULL,
15144
- pass_number INTEGER NOT NULL,
15145
- created_at INTEGER NOT NULL,
15146
- UNIQUE(session_id, sequence)
15147
- );
15148
-
15149
- CREATE TABLE IF NOT EXISTS recomp_facts (
15150
- id INTEGER PRIMARY KEY AUTOINCREMENT,
15151
- session_id TEXT NOT NULL,
15152
- category TEXT NOT NULL,
15153
- content TEXT NOT NULL,
15154
- pass_number INTEGER NOT NULL,
15155
- created_at INTEGER NOT NULL
15156
- );
15157
-
15158
- CREATE INDEX IF NOT EXISTS idx_compartments_session ON compartments(session_id);
15159
- CREATE INDEX IF NOT EXISTS idx_session_facts_session ON session_facts(session_id);
15160
- CREATE INDEX IF NOT EXISTS idx_recomp_compartments_session ON recomp_compartments(session_id);
15161
- CREATE INDEX IF NOT EXISTS idx_recomp_facts_session ON recomp_facts(session_id);
15162
- CREATE INDEX IF NOT EXISTS idx_session_notes_session ON session_notes(session_id);
15163
- CREATE INDEX IF NOT EXISTS idx_memories_project_status_category ON memories(project_path, status, category);
15164
- CREATE INDEX IF NOT EXISTS idx_memories_project_status_expires ON memories(project_path, status, expires_at);
15165
- CREATE INDEX IF NOT EXISTS idx_memories_project_category_hash ON memories(project_path, category, normalized_hash);
15166
- `);
15167
- ensureColumn(db, "session_meta", "last_nudge_band", "TEXT DEFAULT ''");
15168
- ensureColumn(db, "session_meta", "last_transform_error", "TEXT DEFAULT ''");
15169
- ensureColumn(db, "session_meta", "nudge_anchor_message_id", "TEXT DEFAULT ''");
15170
- ensureColumn(db, "session_meta", "nudge_anchor_text", "TEXT DEFAULT ''");
15171
- ensureColumn(db, "session_meta", "sticky_turn_reminder_text", "TEXT DEFAULT ''");
15172
- ensureColumn(db, "session_meta", "sticky_turn_reminder_message_id", "TEXT DEFAULT ''");
15173
- ensureColumn(db, "session_meta", "times_execute_threshold_reached", "INTEGER DEFAULT 0");
15174
- ensureColumn(db, "session_meta", "compartment_in_progress", "INTEGER DEFAULT 0");
15175
- ensureColumn(db, "session_meta", "system_prompt_hash", "TEXT DEFAULT ''");
15176
- ensureColumn(db, "compartments", "start_message_id", "TEXT DEFAULT ''");
15177
- ensureColumn(db, "compartments", "end_message_id", "TEXT DEFAULT ''");
15178
- ensureColumn(db, "memory_embeddings", "model_id", "TEXT");
15179
- ensureColumn(db, "session_meta", "memory_block_cache", "TEXT DEFAULT ''");
15180
- ensureColumn(db, "session_meta", "memory_block_count", "INTEGER DEFAULT 0");
15181
- ensureColumn(db, "dream_queue", "retry_count", "INTEGER DEFAULT 0");
15101
+ // src/features/magic-context/dreamer/runner.ts
15102
+ var dreamProjectDirectories = new Map;
15103
+ function registerDreamProjectDirectory(projectIdentity, directory) {
15104
+ dreamProjectDirectories.set(projectIdentity, directory);
15182
15105
  }
15183
- function ensureColumn(db, table, column, definition) {
15184
- if (!/^[a-z_]+$/.test(table) || !/^[a-z_]+$/.test(column) || !/^[A-Z0-9_'(),\s]+$/i.test(definition)) {
15185
- throw new Error(`Unsafe schema identifier: ${table}.${column} ${definition}`);
15186
- }
15187
- const rows = db.prepare(`PRAGMA table_info(${table})`).all();
15188
- if (rows.some((row) => row.name === column)) {
15189
- return;
15190
- }
15191
- db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
15106
+ function resolveDreamSessionDirectory(projectIdentity) {
15107
+ return dreamProjectDirectories.get(projectIdentity) ?? projectIdentity;
15192
15108
  }
15193
- function createFallbackDatabase() {
15194
- try {
15195
- const fallback = new Database(":memory:");
15196
- initializeDatabase(fallback);
15197
- return fallback;
15198
- } catch (error48) {
15199
- throw new Error(`[magic-context] storage fatal: failed to initialize fallback database: ${String(error48)}`);
15109
+ async function runDream(args) {
15110
+ const holderId = crypto.randomUUID();
15111
+ const startedAt = Date.now();
15112
+ const result = {
15113
+ startedAt,
15114
+ finishedAt: startedAt,
15115
+ holderId,
15116
+ tasks: []
15117
+ };
15118
+ log(`[dreamer] starting dream run: ${args.tasks.length} tasks, timeout=${args.taskTimeoutMinutes}m, maxRuntime=${args.maxRuntimeMinutes}m, project=${args.projectIdentity}`);
15119
+ if (!acquireLease(args.db, holderId)) {
15120
+ const currentHolder = getLeaseHolder(args.db) ?? "another holder";
15121
+ log(`[dreamer] lease acquisition failed \u2014 already held by ${currentHolder}`);
15122
+ result.tasks.push({
15123
+ name: "lease",
15124
+ durationMs: 0,
15125
+ result: null,
15126
+ error: `Dream lease is already held by ${currentHolder}`
15127
+ });
15128
+ result.finishedAt = Date.now();
15129
+ return result;
15200
15130
  }
15201
- }
15202
- function openDatabase() {
15203
- try {
15204
- const { dbDir, dbPath } = resolveDatabasePath();
15205
- const existing = databases.get(dbPath);
15206
- if (existing) {
15207
- if (!persistenceByDatabase.has(existing)) {
15208
- persistenceByDatabase.set(existing, true);
15131
+ log(`[dreamer] lease acquired: ${holderId}`);
15132
+ let parentSessionId = args.parentSessionId;
15133
+ if (!parentSessionId) {
15134
+ try {
15135
+ const sessionDir = args.sessionDirectory ?? args.projectIdentity;
15136
+ const listResponse = await args.client.session.list({
15137
+ query: { directory: sessionDir }
15138
+ });
15139
+ const sessions = normalizeSDKResponse(listResponse, [], {
15140
+ preferResponseOnMissingData: true
15141
+ });
15142
+ parentSessionId = sessions?.find((s) => typeof s?.id === "string")?.id;
15143
+ if (parentSessionId) {
15144
+ log(`[dreamer] resolved parent session: ${parentSessionId}`);
15209
15145
  }
15210
- return existing;
15146
+ } catch {
15147
+ log("[dreamer] could not resolve parent session \u2014 child sessions will be visible in UI");
15211
15148
  }
15212
- mkdirSync(dbDir, { recursive: true });
15213
- const db = new Database(dbPath);
15214
- initializeDatabase(db);
15215
- databases.set(dbPath, db);
15216
- persistenceByDatabase.set(db, true);
15217
- persistenceErrorByDatabase.delete(db);
15218
- return db;
15219
- } catch (error48) {
15220
- log("[magic-context] storage error:", error48);
15221
- const errorMessage = getErrorMessage(error48);
15222
- const existingFallback = databases.get(FALLBACK_DATABASE_KEY);
15223
- if (existingFallback) {
15224
- if (!persistenceByDatabase.has(existingFallback)) {
15225
- persistenceByDatabase.set(existingFallback, false);
15226
- persistenceErrorByDatabase.set(existingFallback, errorMessage);
15149
+ }
15150
+ const deadline = startedAt + args.maxRuntimeMinutes * 60 * 1000;
15151
+ const lastDreamAt = getDreamState(args.db, `last_dream_at:${args.projectIdentity}`) ?? getDreamState(args.db, "last_dream_at");
15152
+ log(`[dreamer] last dream at: ${lastDreamAt ?? "never"} (project=${args.projectIdentity})`);
15153
+ try {
15154
+ for (const taskName of args.tasks) {
15155
+ if (Date.now() > deadline) {
15156
+ log(`[dreamer] deadline reached, stopping after ${result.tasks.length} tasks`);
15157
+ break;
15158
+ }
15159
+ log(`[dreamer] starting task: ${taskName}`);
15160
+ const taskStartedAt = Date.now();
15161
+ let agentSessionId = null;
15162
+ const taskAbortController = new AbortController;
15163
+ const leaseRenewalInterval = setInterval(() => {
15164
+ try {
15165
+ if (!renewLease(args.db, holderId)) {
15166
+ log(`[dreamer] task ${taskName}: lease renewal failed \u2014 aborting LLM call`);
15167
+ taskAbortController.abort();
15168
+ }
15169
+ } catch (err) {
15170
+ log(`[dreamer] task ${taskName}: lease renewal threw \u2014 aborting LLM call: ${err}`);
15171
+ taskAbortController.abort();
15172
+ }
15173
+ }, 60000);
15174
+ try {
15175
+ const docsDir = args.sessionDirectory ?? args.projectIdentity;
15176
+ const existingDocs = taskName === "maintain-docs" ? {
15177
+ architecture: existsSync3(join3(docsDir, "ARCHITECTURE.md")),
15178
+ structure: existsSync3(join3(docsDir, "STRUCTURE.md"))
15179
+ } : undefined;
15180
+ const taskPrompt = buildDreamTaskPrompt(taskName, {
15181
+ projectPath: args.projectIdentity,
15182
+ lastDreamAt,
15183
+ existingDocs
15184
+ });
15185
+ const createResponse = await args.client.session.create({
15186
+ body: {
15187
+ ...parentSessionId ? { parentID: parentSessionId } : {},
15188
+ title: `magic-context-dream-${taskName}`
15189
+ },
15190
+ query: { directory: args.sessionDirectory ?? args.projectIdentity }
15191
+ });
15192
+ const createdSession = normalizeSDKResponse(createResponse, null, { preferResponseOnMissingData: true });
15193
+ agentSessionId = typeof createdSession?.id === "string" ? createdSession.id : null;
15194
+ if (!agentSessionId) {
15195
+ throw new Error("Dreamer could not create its child session.");
15196
+ }
15197
+ log(`[dreamer] task ${taskName}: child session created ${agentSessionId}`);
15198
+ await promptSyncWithModelSuggestionRetry(args.client, {
15199
+ path: { id: agentSessionId },
15200
+ query: { directory: args.sessionDirectory ?? args.projectIdentity },
15201
+ body: {
15202
+ agent: DREAMER_AGENT,
15203
+ system: DREAMER_SYSTEM_PROMPT,
15204
+ parts: [{ type: "text", text: taskPrompt }]
15205
+ }
15206
+ }, {
15207
+ timeoutMs: args.taskTimeoutMinutes * 60 * 1000,
15208
+ signal: taskAbortController.signal
15209
+ });
15210
+ const messagesResponse = await args.client.session.messages({
15211
+ path: { id: agentSessionId },
15212
+ query: { directory: args.sessionDirectory ?? args.projectIdentity }
15213
+ });
15214
+ const messages = normalizeSDKResponse(messagesResponse, [], {
15215
+ preferResponseOnMissingData: true
15216
+ });
15217
+ const taskResult = extractLatestAssistantText(messages);
15218
+ if (!taskResult) {
15219
+ throw new Error("Dreamer returned no assistant output.");
15220
+ }
15221
+ const durationMs = Date.now() - taskStartedAt;
15222
+ log(`[dreamer] task ${taskName}: completed in ${(durationMs / 1000).toFixed(1)}s (result: ${String(taskResult).length} chars)`);
15223
+ result.tasks.push({
15224
+ name: taskName,
15225
+ durationMs,
15226
+ result: taskResult
15227
+ });
15228
+ } catch (error48) {
15229
+ const durationMs = Date.now() - taskStartedAt;
15230
+ const errorMsg = getErrorMessage(error48);
15231
+ log(`[dreamer] task ${taskName}: failed after ${(durationMs / 1000).toFixed(1)}s \u2014 ${errorMsg}`);
15232
+ result.tasks.push({
15233
+ name: taskName,
15234
+ durationMs,
15235
+ result: null,
15236
+ error: errorMsg
15237
+ });
15238
+ } finally {
15239
+ clearInterval(leaseRenewalInterval);
15240
+ if (agentSessionId) {
15241
+ await args.client.session.delete({
15242
+ path: { id: agentSessionId },
15243
+ query: { directory: args.sessionDirectory ?? args.projectIdentity }
15244
+ }).catch((error48) => {
15245
+ log("[dreamer] failed to delete child session:", error48);
15246
+ });
15247
+ }
15227
15248
  }
15228
- return existingFallback;
15229
15249
  }
15230
- const fallback = createFallbackDatabase();
15231
- databases.set(FALLBACK_DATABASE_KEY, fallback);
15232
- persistenceByDatabase.set(fallback, false);
15233
- persistenceErrorByDatabase.set(fallback, errorMessage);
15234
- return fallback;
15235
- }
15236
- }
15237
- function isDatabasePersisted(db) {
15238
- return persistenceByDatabase.get(db) ?? false;
15239
- }
15240
- function getDatabasePersistenceError(db) {
15241
- return persistenceErrorByDatabase.get(db) ?? null;
15242
- }
15243
- // src/features/magic-context/storage-meta-shared.ts
15244
- var META_COLUMNS = {
15245
- lastResponseTime: "last_response_time",
15246
- cacheTtl: "cache_ttl",
15247
- counter: "counter",
15248
- lastNudgeTokens: "last_nudge_tokens",
15249
- lastNudgeBand: "last_nudge_band",
15250
- lastTransformError: "last_transform_error",
15251
- isSubagent: "is_subagent",
15252
- lastContextPercentage: "last_context_percentage",
15253
- lastInputTokens: "last_input_tokens",
15254
- timesExecuteThresholdReached: "times_execute_threshold_reached",
15255
- compartmentInProgress: "compartment_in_progress",
15256
- systemPromptHash: "system_prompt_hash"
15257
- };
15258
- var BOOLEAN_META_KEYS = new Set(["isSubagent", "compartmentInProgress"]);
15259
- function isSessionMetaRow(row) {
15260
- if (row === null || typeof row !== "object")
15261
- return false;
15262
- const r = row;
15263
- 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");
15264
- }
15265
- function getDefaultSessionMeta(sessionId) {
15266
- return {
15267
- sessionId,
15268
- lastResponseTime: 0,
15269
- cacheTtl: "5m",
15270
- counter: 0,
15271
- lastNudgeTokens: 0,
15272
- lastNudgeBand: null,
15273
- lastTransformError: null,
15274
- isSubagent: false,
15275
- lastContextPercentage: 0,
15276
- lastInputTokens: 0,
15277
- timesExecuteThresholdReached: 0,
15278
- compartmentInProgress: false,
15279
- systemPromptHash: ""
15280
- };
15281
- }
15282
- function ensureSessionMetaRow(db, sessionId) {
15283
- const defaults = getDefaultSessionMeta(sessionId);
15284
- 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) 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 ?? "");
15285
- }
15286
- function toSessionMeta(row) {
15287
- return {
15288
- sessionId: row.session_id,
15289
- lastResponseTime: row.last_response_time,
15290
- cacheTtl: row.cache_ttl,
15291
- counter: row.counter,
15292
- lastNudgeTokens: row.last_nudge_tokens,
15293
- lastNudgeBand: row.last_nudge_band.length > 0 ? row.last_nudge_band : null,
15294
- lastTransformError: row.last_transform_error.length > 0 ? row.last_transform_error : null,
15295
- isSubagent: row.is_subagent === 1,
15296
- lastContextPercentage: row.last_context_percentage,
15297
- lastInputTokens: row.last_input_tokens,
15298
- timesExecuteThresholdReached: row.times_execute_threshold_reached,
15299
- compartmentInProgress: row.compartment_in_progress === 1,
15300
- systemPromptHash: String(row.system_prompt_hash)
15301
- };
15302
- }
15303
-
15304
- // src/features/magic-context/storage-meta-persisted.ts
15305
- function isPersistedUsageRow(row) {
15306
- if (row === null || typeof row !== "object")
15307
- return false;
15308
- const r = row;
15309
- return typeof r.last_context_percentage === "number" && typeof r.last_input_tokens === "number" && typeof r.last_response_time === "number";
15310
- }
15311
- function isPersistedNudgePlacementRow(row) {
15312
- if (row === null || typeof row !== "object")
15313
- return false;
15314
- const r = row;
15315
- return typeof r.nudge_anchor_message_id === "string" && typeof r.nudge_anchor_text === "string";
15316
- }
15317
- function isPersistedStickyTurnReminderRow(row) {
15318
- if (row === null || typeof row !== "object")
15319
- return false;
15320
- const r = row;
15321
- return typeof r.sticky_turn_reminder_text === "string" && typeof r.sticky_turn_reminder_message_id === "string";
15322
- }
15323
- function loadPersistedUsage(db, sessionId) {
15324
- const result = db.prepare("SELECT last_context_percentage, last_input_tokens, last_response_time FROM session_meta WHERE session_id = ?").get(sessionId);
15325
- if (!isPersistedUsageRow(result) || result.last_context_percentage === 0 && result.last_input_tokens === 0) {
15326
- return null;
15327
- }
15328
- return {
15329
- usage: {
15330
- percentage: result.last_context_percentage,
15331
- inputTokens: result.last_input_tokens
15332
- },
15333
- updatedAt: result.last_response_time || Date.now()
15334
- };
15335
- }
15336
- function getPersistedNudgePlacement(db, sessionId) {
15337
- const result = db.prepare("SELECT nudge_anchor_message_id, nudge_anchor_text FROM session_meta WHERE session_id = ?").get(sessionId);
15338
- if (!isPersistedNudgePlacementRow(result)) {
15339
- return null;
15250
+ } finally {
15251
+ releaseLease(args.db, holderId);
15252
+ log(`[dreamer] lease released: ${holderId}`);
15340
15253
  }
15341
- if (result.nudge_anchor_message_id.length === 0 || result.nudge_anchor_text.length === 0) {
15342
- return null;
15254
+ result.finishedAt = Date.now();
15255
+ const hasSuccessfulTask = result.tasks.some((t) => !t.error);
15256
+ if (hasSuccessfulTask) {
15257
+ setDreamState(args.db, `last_dream_at:${args.projectIdentity}`, String(result.finishedAt));
15258
+ setDreamState(args.db, "last_dream_at", String(result.finishedAt));
15343
15259
  }
15344
- return {
15345
- messageId: result.nudge_anchor_message_id,
15346
- nudgeText: result.nudge_anchor_text
15347
- };
15348
- }
15349
- function setPersistedNudgePlacement(db, sessionId, messageId, nudgeText) {
15350
- db.transaction(() => {
15351
- ensureSessionMetaRow(db, sessionId);
15352
- db.prepare("UPDATE session_meta SET nudge_anchor_message_id = ?, nudge_anchor_text = ? WHERE session_id = ?").run(messageId, nudgeText, sessionId);
15353
- })();
15354
- }
15355
- function clearPersistedNudgePlacement(db, sessionId) {
15356
- db.prepare("UPDATE session_meta SET nudge_anchor_message_id = '', nudge_anchor_text = '' WHERE session_id = ?").run(sessionId);
15260
+ const totalDuration = ((result.finishedAt - startedAt) / 1000).toFixed(1);
15261
+ const succeeded = result.tasks.filter((t) => !t.error).length;
15262
+ const failed = result.tasks.filter((t) => t.error).length;
15263
+ log(`[dreamer] dream run finished in ${totalDuration}s: ${succeeded} succeeded, ${failed} failed`);
15264
+ return result;
15357
15265
  }
15358
- function getPersistedStickyTurnReminder(db, sessionId) {
15359
- const result = db.prepare("SELECT sticky_turn_reminder_text, sticky_turn_reminder_message_id FROM session_meta WHERE session_id = ?").get(sessionId);
15360
- if (!isPersistedStickyTurnReminderRow(result)) {
15266
+ var MAX_LEASE_RETRIES = 3;
15267
+ async function processDreamQueue(args) {
15268
+ clearStaleEntries(args.db, 2 * 60 * 60 * 1000);
15269
+ const entry = dequeueNext(args.db);
15270
+ if (!entry) {
15361
15271
  return null;
15362
15272
  }
15363
- if (result.sticky_turn_reminder_text.length === 0) {
15273
+ const projectDirectory = resolveDreamSessionDirectory(entry.projectIdentity);
15274
+ log(`[dreamer] dequeued project ${entry.projectIdentity} (dir=${projectDirectory}), starting dream run`);
15275
+ let result;
15276
+ try {
15277
+ result = await runDream({
15278
+ db: args.db,
15279
+ client: args.client,
15280
+ projectIdentity: entry.projectIdentity,
15281
+ tasks: args.tasks,
15282
+ taskTimeoutMinutes: args.taskTimeoutMinutes,
15283
+ maxRuntimeMinutes: args.maxRuntimeMinutes,
15284
+ sessionDirectory: projectDirectory
15285
+ });
15286
+ } catch (error48) {
15287
+ log(`[dreamer] runDream threw for ${entry.projectIdentity}: ${getErrorMessage(error48)}`);
15288
+ removeDreamEntry(args.db, entry.id);
15364
15289
  return null;
15365
15290
  }
15366
- return {
15367
- text: result.sticky_turn_reminder_text,
15368
- messageId: result.sticky_turn_reminder_message_id.length > 0 ? result.sticky_turn_reminder_message_id : null
15369
- };
15370
- }
15371
- function setPersistedStickyTurnReminder(db, sessionId, text, messageId = "") {
15372
- db.transaction(() => {
15373
- ensureSessionMetaRow(db, sessionId);
15374
- db.prepare("UPDATE session_meta SET sticky_turn_reminder_text = ?, sticky_turn_reminder_message_id = ? WHERE session_id = ?").run(text, messageId, sessionId);
15375
- })();
15376
- }
15377
- function clearPersistedStickyTurnReminder(db, sessionId) {
15378
- db.prepare("UPDATE session_meta SET sticky_turn_reminder_text = '', sticky_turn_reminder_message_id = '' WHERE session_id = ?").run(sessionId);
15379
- }
15380
- // src/features/magic-context/storage-meta-session.ts
15381
- function getOrCreateSessionMeta(db, sessionId) {
15382
- 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 FROM session_meta WHERE session_id = ?").get(sessionId);
15383
- if (isSessionMetaRow(result)) {
15384
- return toSessionMeta(result);
15385
- }
15386
- const defaults = getDefaultSessionMeta(sessionId);
15387
- ensureSessionMetaRow(db, sessionId);
15388
- return defaults;
15389
- }
15390
- function updateSessionMeta(db, sessionId, updates) {
15391
- const setClauses = [];
15392
- const values = [];
15393
- for (const [key, column] of Object.entries(META_COLUMNS)) {
15394
- const value = updates[key];
15395
- if (value === undefined)
15396
- continue;
15397
- if (value === null) {
15398
- setClauses.push(`${column} = ?`);
15399
- values.push("");
15400
- } else if (BOOLEAN_META_KEYS.has(key)) {
15401
- setClauses.push(`${column} = ?`);
15402
- values.push(value ? 1 : 0);
15403
- } else if (typeof value === "string" || typeof value === "number") {
15404
- setClauses.push(`${column} = ?`);
15405
- values.push(value);
15291
+ const leaseError = result.tasks.find((t) => t.name === "lease" && t.error);
15292
+ if (leaseError) {
15293
+ const retryCount = getEntryRetryCount(args.db, entry.id);
15294
+ if (retryCount >= MAX_LEASE_RETRIES) {
15295
+ log(`[dreamer] lease acquisition failed ${retryCount + 1} times for ${entry.projectIdentity} \u2014 removing queue entry`);
15296
+ removeDreamEntry(args.db, entry.id);
15297
+ } else {
15298
+ log(`[dreamer] lease acquisition failed for ${entry.projectIdentity} (attempt ${retryCount + 1}/${MAX_LEASE_RETRIES}) \u2014 keeping for retry`);
15299
+ resetDreamEntry(args.db, entry.id);
15406
15300
  }
15301
+ } else {
15302
+ removeDreamEntry(args.db, entry.id);
15407
15303
  }
15408
- if (setClauses.length === 0) {
15409
- return;
15410
- }
15411
- db.transaction(() => {
15412
- ensureSessionMetaRow(db, sessionId);
15413
- db.prepare(`UPDATE session_meta SET ${setClauses.join(", ")} WHERE session_id = ?`).run(...values, sessionId);
15414
- })();
15415
- }
15416
- function clearSession(db, sessionId) {
15417
- db.transaction(() => {
15418
- db.prepare("DELETE FROM pending_ops WHERE session_id = ?").run(sessionId);
15419
- db.prepare("DELETE FROM source_contents WHERE session_id = ?").run(sessionId);
15420
- db.prepare("DELETE FROM tags WHERE session_id = ?").run(sessionId);
15421
- db.prepare("DELETE FROM session_meta WHERE session_id = ?").run(sessionId);
15422
- db.prepare("DELETE FROM compartments WHERE session_id = ?").run(sessionId);
15423
- db.prepare("DELETE FROM session_facts WHERE session_id = ?").run(sessionId);
15424
- db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
15425
- db.prepare("DELETE FROM recomp_compartments WHERE session_id = ?").run(sessionId);
15426
- db.prepare("DELETE FROM recomp_facts WHERE session_id = ?").run(sessionId);
15427
- })();
15428
- }
15429
- // src/features/magic-context/storage-notes.ts
15430
- function isSessionNoteRow(row) {
15431
- if (row === null || typeof row !== "object")
15432
- return false;
15433
- const candidate = row;
15434
- return typeof candidate.id === "number" && typeof candidate.session_id === "string" && typeof candidate.content === "string" && typeof candidate.created_at === "number";
15435
- }
15436
- function toSessionNote(row) {
15437
- return {
15438
- id: row.id,
15439
- sessionId: row.session_id,
15440
- content: row.content,
15441
- createdAt: row.created_at
15442
- };
15443
- }
15444
- function getSessionNotes(db, sessionId) {
15445
- const rows = db.prepare("SELECT * FROM session_notes WHERE session_id = ? ORDER BY id ASC").all(sessionId).filter(isSessionNoteRow);
15446
- return rows.map(toSessionNote);
15447
- }
15448
- function addSessionNote(db, sessionId, content) {
15449
- db.prepare("INSERT INTO session_notes (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, Date.now());
15450
- }
15451
- function clearSessionNotes(db, sessionId) {
15452
- db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
15453
- }
15454
- // src/features/magic-context/storage-ops.ts
15455
- function isPendingOpRow(row) {
15456
- if (row === null || typeof row !== "object")
15457
- return false;
15458
- const r = row;
15459
- return typeof r.id === "number" && typeof r.session_id === "string" && typeof r.tag_id === "number" && typeof r.operation === "string" && typeof r.queued_at === "number";
15304
+ return result;
15460
15305
  }
15461
- function toPendingOp(row) {
15462
- if (row.operation !== "drop") {
15463
- sessionLog(row.session_id, `unknown pending operation "${row.operation}"; ignoring`);
15306
+ // src/features/magic-context/dreamer/scheduler.ts
15307
+ function parseScheduleWindow(schedule) {
15308
+ const match = /^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/.exec(schedule.trim());
15309
+ if (!match)
15310
+ return null;
15311
+ const startHour = Number(match[1]);
15312
+ const startMin = Number(match[2]);
15313
+ const endHour = Number(match[3]);
15314
+ const endMin = Number(match[4]);
15315
+ if (startHour >= 24 || startMin >= 60 || endHour >= 24 || endMin >= 60) {
15464
15316
  return null;
15465
15317
  }
15466
- return {
15467
- id: row.id,
15468
- sessionId: row.session_id,
15469
- tagId: row.tag_id,
15470
- operation: row.operation,
15471
- queuedAt: row.queued_at
15472
- };
15473
- }
15474
- function queuePendingOp(db, sessionId, tagId, operation, queuedAt = Date.now()) {
15475
- db.prepare("INSERT INTO pending_ops (session_id, tag_id, operation, queued_at) VALUES (?, ?, ?, ?)").run(sessionId, tagId, operation, queuedAt);
15476
- }
15477
- function getPendingOps(db, sessionId) {
15478
- const rows = db.prepare("SELECT id, session_id, tag_id, operation, queued_at FROM pending_ops WHERE session_id = ? ORDER BY queued_at ASC, id ASC").all(sessionId).filter(isPendingOpRow);
15479
- return rows.map(toPendingOp).filter((op) => op !== null);
15480
- }
15481
- function removePendingOp(db, sessionId, tagId) {
15482
- db.prepare("DELETE FROM pending_ops WHERE session_id = ? AND tag_id = ?").run(sessionId, tagId);
15318
+ const startMinutes = startHour * 60 + startMin;
15319
+ const endMinutes = endHour * 60 + endMin;
15320
+ return { startMinutes, endMinutes };
15483
15321
  }
15484
- // src/features/magic-context/storage-source.ts
15485
- function isSourceContentRow(row) {
15486
- if (row === null || typeof row !== "object")
15322
+ function isInScheduleWindow(schedule, now = new Date) {
15323
+ const window = parseScheduleWindow(schedule);
15324
+ if (!window)
15487
15325
  return false;
15488
- const r = row;
15489
- return typeof r.tag_id === "number" && typeof r.content === "string";
15490
- }
15491
- function saveSourceContent(db, sessionId, tagId, content) {
15492
- db.prepare("INSERT OR IGNORE INTO source_contents (tag_id, session_id, content, created_at) VALUES (?, ?, ?, ?)").run(tagId, sessionId, content, Date.now());
15493
- }
15494
- function replaceSourceContent(db, sessionId, tagId, content) {
15495
- db.prepare(`INSERT INTO source_contents (tag_id, session_id, content, created_at)
15496
- VALUES (?, ?, ?, ?)
15497
- ON CONFLICT(session_id, tag_id)
15498
- DO UPDATE SET content = excluded.content, created_at = excluded.created_at`).run(tagId, sessionId, content, Date.now());
15499
- }
15500
- function getSourceContents(db, sessionId, tagIds) {
15501
- if (tagIds.length === 0) {
15502
- return new Map;
15503
- }
15504
- const placeholders = tagIds.map(() => "?").join(", ");
15505
- const rows = db.prepare(`SELECT tag_id, content FROM source_contents WHERE session_id = ? AND tag_id IN (${placeholders})`).all(sessionId, ...tagIds).filter(isSourceContentRow);
15506
- const sources = new Map;
15507
- for (const row of rows) {
15508
- sources.set(row.tag_id, row.content);
15326
+ const currentMinutes = now.getHours() * 60 + now.getMinutes();
15327
+ if (window.startMinutes <= window.endMinutes) {
15328
+ return currentMinutes >= window.startMinutes && currentMinutes < window.endMinutes;
15509
15329
  }
15510
- return sources;
15330
+ return currentMinutes >= window.startMinutes || currentMinutes < window.endMinutes;
15511
15331
  }
15512
- // src/features/magic-context/storage-tags.ts
15513
- var insertTagStatements = new WeakMap;
15514
- function getInsertTagStatement(db) {
15515
- let stmt = insertTagStatements.get(db);
15516
- if (!stmt) {
15517
- stmt = db.prepare("INSERT INTO tags (session_id, message_id, type, byte_size, tag_number) VALUES (?, ?, ?, ?, ?)");
15518
- insertTagStatements.set(db, stmt);
15332
+ function findProjectsNeedingDream(db) {
15333
+ const projectRows = db.query(`SELECT DISTINCT project_path FROM memories WHERE status = 'active' ORDER BY project_path`).all();
15334
+ const projects = [];
15335
+ for (const row of projectRows) {
15336
+ const lastDreamAtStr = getDreamState(db, `last_dream_at:${row.project_path}`);
15337
+ const fallbackStr = !lastDreamAtStr ? getDreamState(db, "last_dream_at") : null;
15338
+ const lastDreamAt = Number(lastDreamAtStr ?? fallbackStr ?? "0") || 0;
15339
+ const updated = db.query(`SELECT COUNT(*) as cnt FROM memories
15340
+ WHERE project_path = ? AND status = 'active' AND updated_at > ?`).get(row.project_path, lastDreamAt);
15341
+ if (updated && updated.cnt > 0) {
15342
+ projects.push(row.project_path);
15343
+ }
15519
15344
  }
15520
- return stmt;
15521
- }
15522
- function isTagRow(row) {
15523
- if (row === null || typeof row !== "object")
15524
- return false;
15525
- const r = row;
15526
- return typeof r.id === "number" && typeof r.message_id === "string" && typeof r.type === "string" && typeof r.status === "string" && typeof r.byte_size === "number" && typeof r.session_id === "string" && typeof r.tag_number === "number";
15527
- }
15528
- function toTagEntry(row) {
15529
- const type = row.type === "tool" ? "tool" : row.type === "file" ? "file" : "message";
15530
- const status = row.status === "dropped" || row.status === "compacted" ? row.status : "active";
15531
- return {
15532
- tagNumber: row.tag_number,
15533
- messageId: row.message_id,
15534
- type,
15535
- status,
15536
- byteSize: row.byte_size,
15537
- sessionId: row.session_id
15538
- };
15539
- }
15540
- function insertTag(db, sessionId, messageId, type, byteSize, tagNumber) {
15541
- getInsertTagStatement(db).run(sessionId, messageId, type, byteSize, tagNumber);
15542
- return tagNumber;
15543
- }
15544
- function updateTagStatus(db, sessionId, tagId, status) {
15545
- db.prepare("UPDATE tags SET status = ? WHERE session_id = ? AND tag_number = ?").run(status, sessionId, tagId);
15546
- }
15547
- function updateTagMessageId(db, sessionId, tagId, messageId) {
15548
- db.prepare("UPDATE tags SET message_id = ? WHERE session_id = ? AND tag_number = ?").run(messageId, sessionId, tagId);
15549
- }
15550
- function getTagsBySession(db, sessionId) {
15551
- 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);
15552
- return rows.map(toTagEntry);
15345
+ return projects;
15553
15346
  }
15554
- function getTopNBySize(db, sessionId, n) {
15555
- if (n <= 0) {
15556
- return [];
15347
+ function checkScheduleAndEnqueue(db, schedule) {
15348
+ if (!isInScheduleWindow(schedule)) {
15349
+ return 0;
15350
+ }
15351
+ const projects = findProjectsNeedingDream(db);
15352
+ if (projects.length === 0) {
15353
+ return 0;
15354
+ }
15355
+ let enqueued = 0;
15356
+ for (const projectIdentity of projects) {
15357
+ const entry = enqueueDream(db, projectIdentity, "scheduled");
15358
+ if (entry) {
15359
+ log(`[dreamer] enqueued project for scheduled dream: ${projectIdentity}`);
15360
+ enqueued++;
15361
+ }
15557
15362
  }
15558
- 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);
15559
- return rows.map(toTagEntry);
15363
+ return enqueued;
15560
15364
  }
15561
- // src/features/magic-context/compaction.ts
15562
- function createCompactionHandler() {
15563
- return {
15564
- onCompacted(sessionId, db) {
15565
- db.transaction(() => {
15566
- db.prepare("UPDATE tags SET status = 'compacted' WHERE session_id = ? AND status IN ('active', 'dropped')").run(sessionId);
15567
- db.prepare("DELETE FROM pending_ops WHERE session_id = ?").run(sessionId);
15568
- })();
15569
- updateSessionMeta(db, sessionId, { lastNudgeBand: null });
15570
- }
15571
- };
15365
+ // src/features/magic-context/storage-db.ts
15366
+ import { Database } from "bun:sqlite";
15367
+ import { mkdirSync } from "fs";
15368
+ import { join as join5 } from "path";
15369
+
15370
+ // src/shared/data-path.ts
15371
+ import * as os2 from "os";
15372
+ import * as path2 from "path";
15373
+ function getDataDir() {
15374
+ return process.env.XDG_DATA_HOME ?? path2.join(os2.homedir(), ".local", "share");
15375
+ }
15376
+ function getOpenCodeStorageDir() {
15377
+ return path2.join(getDataDir(), "opencode", "storage");
15572
15378
  }
15573
15379
 
15574
- // src/hooks/is-anthropic-provider.ts
15575
- function isAnthropicProvider(providerID) {
15576
- return providerID === "anthropic" || providerID === "google-vertex-anthropic";
15380
+ // src/features/magic-context/storage-db.ts
15381
+ var databases = new Map;
15382
+ var FALLBACK_DATABASE_KEY = "__fallback__:memory:";
15383
+ var persistenceByDatabase = new WeakMap;
15384
+ var persistenceErrorByDatabase = new WeakMap;
15385
+ function resolveDatabasePath() {
15386
+ const dbDir = join5(getOpenCodeStorageDir(), "plugin", "magic-context");
15387
+ return { dbDir, dbPath: join5(dbDir, "context.db") };
15577
15388
  }
15389
+ function initializeDatabase(db) {
15390
+ db.run("PRAGMA journal_mode=WAL");
15391
+ db.run("PRAGMA busy_timeout=5000");
15392
+ db.run("PRAGMA foreign_keys=ON");
15393
+ db.run(`
15394
+ CREATE TABLE IF NOT EXISTS tags (
15395
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15396
+ session_id TEXT,
15397
+ message_id TEXT,
15398
+ type TEXT,
15399
+ status TEXT DEFAULT 'active',
15400
+ byte_size INTEGER,
15401
+ tag_number INTEGER,
15402
+ UNIQUE(session_id, tag_number)
15403
+ );
15578
15404
 
15579
- // src/hooks/magic-context/event-resolvers.ts
15580
- var DEFAULT_CONTEXT_LIMIT = 200000;
15581
- function resolveContextLimit(providerID, modelID, config2) {
15582
- if (!providerID) {
15583
- return DEFAULT_CONTEXT_LIMIT;
15584
- }
15585
- if (modelID) {
15586
- const modelSpecific = config2.modelContextLimitsCache?.get(`${providerID}/${modelID}`);
15587
- if (typeof modelSpecific === "number" && modelSpecific > 0) {
15588
- return modelSpecific;
15589
- }
15590
- }
15591
- if (isAnthropicProvider(providerID)) {
15592
- return 1e6;
15593
- }
15594
- return DEFAULT_CONTEXT_LIMIT;
15405
+ CREATE TABLE IF NOT EXISTS pending_ops (
15406
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15407
+ session_id TEXT,
15408
+ tag_id INTEGER,
15409
+ operation TEXT,
15410
+ queued_at INTEGER
15411
+ );
15412
+
15413
+ CREATE TABLE IF NOT EXISTS source_contents (
15414
+ tag_id INTEGER,
15415
+ session_id TEXT,
15416
+ content TEXT,
15417
+ created_at INTEGER,
15418
+ PRIMARY KEY(session_id, tag_id)
15419
+ );
15420
+
15421
+ CREATE TABLE IF NOT EXISTS compartments (
15422
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15423
+ session_id TEXT NOT NULL,
15424
+ sequence INTEGER NOT NULL,
15425
+ start_message INTEGER NOT NULL,
15426
+ end_message INTEGER NOT NULL,
15427
+ start_message_id TEXT DEFAULT '',
15428
+ end_message_id TEXT DEFAULT '',
15429
+ title TEXT NOT NULL,
15430
+ content TEXT NOT NULL,
15431
+ created_at INTEGER NOT NULL,
15432
+ UNIQUE(session_id, sequence)
15433
+ );
15434
+
15435
+ CREATE TABLE IF NOT EXISTS session_facts (
15436
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15437
+ session_id TEXT NOT NULL,
15438
+ category TEXT NOT NULL,
15439
+ content TEXT NOT NULL,
15440
+ created_at INTEGER NOT NULL,
15441
+ updated_at INTEGER NOT NULL
15442
+ );
15443
+
15444
+ CREATE TABLE IF NOT EXISTS session_notes (
15445
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15446
+ session_id TEXT NOT NULL,
15447
+ content TEXT NOT NULL,
15448
+ created_at INTEGER NOT NULL
15449
+ );
15450
+
15451
+ CREATE TABLE IF NOT EXISTS memories (
15452
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15453
+ project_path TEXT NOT NULL,
15454
+ category TEXT NOT NULL,
15455
+ content TEXT NOT NULL,
15456
+ normalized_hash TEXT NOT NULL,
15457
+ source_session_id TEXT,
15458
+ source_type TEXT DEFAULT 'historian',
15459
+ seen_count INTEGER DEFAULT 1,
15460
+ retrieval_count INTEGER DEFAULT 0,
15461
+ first_seen_at INTEGER NOT NULL,
15462
+ created_at INTEGER NOT NULL,
15463
+ updated_at INTEGER NOT NULL,
15464
+ last_seen_at INTEGER NOT NULL,
15465
+ last_retrieved_at INTEGER,
15466
+ status TEXT DEFAULT 'active',
15467
+ expires_at INTEGER,
15468
+ verification_status TEXT DEFAULT 'unverified',
15469
+ verified_at INTEGER,
15470
+ superseded_by_memory_id INTEGER,
15471
+ merged_from TEXT,
15472
+ metadata_json TEXT,
15473
+ UNIQUE(project_path, category, normalized_hash)
15474
+ );
15475
+
15476
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
15477
+ memory_id INTEGER PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
15478
+ embedding BLOB NOT NULL,
15479
+ model_id TEXT
15480
+ );
15481
+
15482
+ CREATE TABLE IF NOT EXISTS dream_state (
15483
+ key TEXT PRIMARY KEY,
15484
+ value TEXT NOT NULL
15485
+ );
15486
+
15487
+ CREATE TABLE IF NOT EXISTS dream_queue (
15488
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15489
+ project_path TEXT NOT NULL,
15490
+ reason TEXT NOT NULL,
15491
+ enqueued_at INTEGER NOT NULL,
15492
+ started_at INTEGER,
15493
+ retry_count INTEGER DEFAULT 0
15494
+ );
15495
+ CREATE INDEX IF NOT EXISTS idx_dream_queue_project ON dream_queue(project_path);
15496
+ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, enqueued_at);
15497
+
15498
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
15499
+ content,
15500
+ category,
15501
+ content='memories',
15502
+ content_rowid='id',
15503
+ tokenize='porter unicode61'
15504
+ );
15505
+
15506
+ CREATE VIRTUAL TABLE IF NOT EXISTS message_history_fts USING fts5(
15507
+ session_id UNINDEXED,
15508
+ message_ordinal UNINDEXED,
15509
+ message_id UNINDEXED,
15510
+ role,
15511
+ content,
15512
+ tokenize='porter unicode61'
15513
+ );
15514
+
15515
+ CREATE TABLE IF NOT EXISTS message_history_index (
15516
+ session_id TEXT PRIMARY KEY,
15517
+ last_indexed_ordinal INTEGER NOT NULL DEFAULT 0,
15518
+ updated_at INTEGER NOT NULL
15519
+ );
15520
+
15521
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
15522
+ INSERT INTO memories_fts(rowid, content, category) VALUES (new.id, new.content, new.category);
15523
+ END;
15524
+
15525
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
15526
+ INSERT INTO memories_fts(memories_fts, rowid, content, category) VALUES ('delete', old.id, old.content, old.category);
15527
+ END;
15528
+
15529
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
15530
+ INSERT INTO memories_fts(memories_fts, rowid, content, category) VALUES ('delete', old.id, old.content, old.category);
15531
+ INSERT INTO memories_fts(rowid, content, category) VALUES (new.id, new.content, new.category);
15532
+ END;
15533
+
15534
+ CREATE TABLE IF NOT EXISTS session_meta (
15535
+ session_id TEXT PRIMARY KEY,
15536
+ last_response_time INTEGER,
15537
+ cache_ttl TEXT,
15538
+ counter INTEGER DEFAULT 0,
15539
+ last_nudge_tokens INTEGER DEFAULT 0,
15540
+ last_nudge_band TEXT DEFAULT '',
15541
+ last_transform_error TEXT DEFAULT '',
15542
+ nudge_anchor_message_id TEXT DEFAULT '',
15543
+ nudge_anchor_text TEXT DEFAULT '',
15544
+ sticky_turn_reminder_text TEXT DEFAULT '',
15545
+ sticky_turn_reminder_message_id TEXT DEFAULT '',
15546
+ is_subagent INTEGER DEFAULT 0,
15547
+ last_context_percentage REAL DEFAULT 0,
15548
+ last_input_tokens INTEGER DEFAULT 0,
15549
+ times_execute_threshold_reached INTEGER DEFAULT 0,
15550
+ compartment_in_progress INTEGER DEFAULT 0,
15551
+ system_prompt_hash TEXT DEFAULT '',
15552
+ memory_block_cache TEXT DEFAULT '',
15553
+ memory_block_count INTEGER DEFAULT 0
15554
+ );
15555
+
15556
+ CREATE INDEX IF NOT EXISTS idx_tags_session_tag_number ON tags(session_id, tag_number);
15557
+ CREATE INDEX IF NOT EXISTS idx_pending_ops_session ON pending_ops(session_id);
15558
+ CREATE INDEX IF NOT EXISTS idx_pending_ops_session_tag_id ON pending_ops(session_id, tag_id);
15559
+ CREATE INDEX IF NOT EXISTS idx_source_contents_session ON source_contents(session_id);
15560
+
15561
+ CREATE TABLE IF NOT EXISTS recomp_compartments (
15562
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15563
+ session_id TEXT NOT NULL,
15564
+ sequence INTEGER NOT NULL,
15565
+ start_message INTEGER NOT NULL,
15566
+ end_message INTEGER NOT NULL,
15567
+ start_message_id TEXT DEFAULT '',
15568
+ end_message_id TEXT DEFAULT '',
15569
+ title TEXT NOT NULL,
15570
+ content TEXT NOT NULL,
15571
+ pass_number INTEGER NOT NULL,
15572
+ created_at INTEGER NOT NULL,
15573
+ UNIQUE(session_id, sequence)
15574
+ );
15575
+
15576
+ CREATE TABLE IF NOT EXISTS recomp_facts (
15577
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15578
+ session_id TEXT NOT NULL,
15579
+ category TEXT NOT NULL,
15580
+ content TEXT NOT NULL,
15581
+ pass_number INTEGER NOT NULL,
15582
+ created_at INTEGER NOT NULL
15583
+ );
15584
+
15585
+ CREATE INDEX IF NOT EXISTS idx_compartments_session ON compartments(session_id);
15586
+ CREATE INDEX IF NOT EXISTS idx_session_facts_session ON session_facts(session_id);
15587
+ CREATE INDEX IF NOT EXISTS idx_recomp_compartments_session ON recomp_compartments(session_id);
15588
+ CREATE INDEX IF NOT EXISTS idx_recomp_facts_session ON recomp_facts(session_id);
15589
+ CREATE INDEX IF NOT EXISTS idx_session_notes_session ON session_notes(session_id);
15590
+ CREATE INDEX IF NOT EXISTS idx_memories_project_status_category ON memories(project_path, status, category);
15591
+ CREATE INDEX IF NOT EXISTS idx_memories_project_status_expires ON memories(project_path, status, expires_at);
15592
+ CREATE INDEX IF NOT EXISTS idx_memories_project_category_hash ON memories(project_path, category, normalized_hash);
15593
+ CREATE INDEX IF NOT EXISTS idx_message_history_index_updated_at ON message_history_index(updated_at);
15594
+ `);
15595
+ ensureColumn(db, "session_meta", "last_nudge_band", "TEXT DEFAULT ''");
15596
+ ensureColumn(db, "session_meta", "last_transform_error", "TEXT DEFAULT ''");
15597
+ ensureColumn(db, "session_meta", "nudge_anchor_message_id", "TEXT DEFAULT ''");
15598
+ ensureColumn(db, "session_meta", "nudge_anchor_text", "TEXT DEFAULT ''");
15599
+ ensureColumn(db, "session_meta", "sticky_turn_reminder_text", "TEXT DEFAULT ''");
15600
+ ensureColumn(db, "session_meta", "sticky_turn_reminder_message_id", "TEXT DEFAULT ''");
15601
+ ensureColumn(db, "session_meta", "times_execute_threshold_reached", "INTEGER DEFAULT 0");
15602
+ ensureColumn(db, "session_meta", "compartment_in_progress", "INTEGER DEFAULT 0");
15603
+ ensureColumn(db, "session_meta", "system_prompt_hash", "TEXT DEFAULT ''");
15604
+ ensureColumn(db, "compartments", "start_message_id", "TEXT DEFAULT ''");
15605
+ ensureColumn(db, "compartments", "end_message_id", "TEXT DEFAULT ''");
15606
+ ensureColumn(db, "memory_embeddings", "model_id", "TEXT");
15607
+ ensureColumn(db, "session_meta", "memory_block_cache", "TEXT DEFAULT ''");
15608
+ ensureColumn(db, "session_meta", "memory_block_count", "INTEGER DEFAULT 0");
15609
+ ensureColumn(db, "dream_queue", "retry_count", "INTEGER DEFAULT 0");
15595
15610
  }
15596
- function resolveCacheTtl(cacheTtl, modelKey) {
15597
- if (typeof cacheTtl === "string") {
15598
- return cacheTtl;
15599
- }
15600
- if (modelKey && typeof cacheTtl[modelKey] === "string") {
15601
- return cacheTtl[modelKey];
15611
+ function ensureColumn(db, table, column, definition) {
15612
+ if (!/^[a-z_]+$/.test(table) || !/^[a-z_]+$/.test(column) || !/^[A-Z0-9_'(),\s]+$/i.test(definition)) {
15613
+ throw new Error(`Unsafe schema identifier: ${table}.${column} ${definition}`);
15602
15614
  }
15603
- if (modelKey) {
15604
- const bareModelId = modelKey.split("/").slice(1).join("/");
15605
- if (bareModelId && typeof cacheTtl[bareModelId] === "string") {
15606
- return cacheTtl[bareModelId];
15607
- }
15615
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all();
15616
+ if (rows.some((row) => row.name === column)) {
15617
+ return;
15608
15618
  }
15609
- return cacheTtl.default ?? "5m";
15619
+ db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
15610
15620
  }
15611
- function resolveExecuteThreshold(config2, modelKey, fallback) {
15612
- if (typeof config2 === "number") {
15613
- return config2;
15614
- }
15615
- if (modelKey && typeof config2[modelKey] === "number") {
15616
- return config2[modelKey];
15621
+ function createFallbackDatabase() {
15622
+ try {
15623
+ const fallback = new Database(":memory:");
15624
+ initializeDatabase(fallback);
15625
+ return fallback;
15626
+ } catch (error48) {
15627
+ throw new Error(`[magic-context] storage fatal: failed to initialize fallback database: ${String(error48)}`);
15617
15628
  }
15618
- if (modelKey) {
15619
- const bareModelId = modelKey.split("/").slice(1).join("/");
15620
- if (bareModelId && typeof config2[bareModelId] === "number") {
15621
- return config2[bareModelId];
15629
+ }
15630
+ function openDatabase() {
15631
+ try {
15632
+ const { dbDir, dbPath } = resolveDatabasePath();
15633
+ const existing = databases.get(dbPath);
15634
+ if (existing) {
15635
+ if (!persistenceByDatabase.has(existing)) {
15636
+ persistenceByDatabase.set(existing, true);
15637
+ }
15638
+ return existing;
15639
+ }
15640
+ mkdirSync(dbDir, { recursive: true });
15641
+ const db = new Database(dbPath);
15642
+ initializeDatabase(db);
15643
+ databases.set(dbPath, db);
15644
+ persistenceByDatabase.set(db, true);
15645
+ persistenceErrorByDatabase.delete(db);
15646
+ return db;
15647
+ } catch (error48) {
15648
+ log("[magic-context] storage error:", error48);
15649
+ const errorMessage = getErrorMessage(error48);
15650
+ const existingFallback = databases.get(FALLBACK_DATABASE_KEY);
15651
+ if (existingFallback) {
15652
+ if (!persistenceByDatabase.has(existingFallback)) {
15653
+ persistenceByDatabase.set(existingFallback, false);
15654
+ persistenceErrorByDatabase.set(existingFallback, errorMessage);
15655
+ }
15656
+ return existingFallback;
15622
15657
  }
15658
+ const fallback = createFallbackDatabase();
15659
+ databases.set(FALLBACK_DATABASE_KEY, fallback);
15660
+ persistenceByDatabase.set(fallback, false);
15661
+ persistenceErrorByDatabase.set(fallback, errorMessage);
15662
+ return fallback;
15623
15663
  }
15624
- return config2.default ?? fallback;
15625
15664
  }
15626
- function resolveModelKey(providerID, modelID) {
15627
- if (!providerID || !modelID) {
15628
- return;
15629
- }
15630
- return `${providerID}/${modelID}`;
15665
+ function isDatabasePersisted(db) {
15666
+ return persistenceByDatabase.get(db) ?? false;
15631
15667
  }
15632
- function resolveSessionId(properties) {
15633
- if (typeof properties?.sessionID === "string") {
15634
- return properties.sessionID;
15635
- }
15636
- const info = properties?.info;
15637
- if (info === null || typeof info !== "object") {
15638
- return;
15639
- }
15640
- const record2 = info;
15641
- if (typeof record2.sessionID === "string") {
15642
- return record2.sessionID;
15643
- }
15644
- if (typeof record2.id === "string") {
15645
- return record2.id;
15646
- }
15647
- return;
15668
+ function getDatabasePersistenceError(db) {
15669
+ return persistenceErrorByDatabase.get(db) ?? null;
15648
15670
  }
15649
-
15650
- // src/features/magic-context/scheduler.ts
15651
- var TTL_PATTERN = /^(\d+)([smh])$/;
15652
- var NUMERIC_PATTERN = /^\d+$/;
15653
- var UNIT_TO_MS = {
15654
- s: 1000,
15655
- m: 60 * 1000,
15656
- h: 60 * 60 * 1000
15671
+ // src/features/magic-context/storage-meta-shared.ts
15672
+ var META_COLUMNS = {
15673
+ lastResponseTime: "last_response_time",
15674
+ cacheTtl: "cache_ttl",
15675
+ counter: "counter",
15676
+ lastNudgeTokens: "last_nudge_tokens",
15677
+ lastNudgeBand: "last_nudge_band",
15678
+ lastTransformError: "last_transform_error",
15679
+ isSubagent: "is_subagent",
15680
+ lastContextPercentage: "last_context_percentage",
15681
+ lastInputTokens: "last_input_tokens",
15682
+ timesExecuteThresholdReached: "times_execute_threshold_reached",
15683
+ compartmentInProgress: "compartment_in_progress",
15684
+ systemPromptHash: "system_prompt_hash"
15657
15685
  };
15658
- function parseCacheTtl(ttl) {
15659
- const normalizedTtl = ttl.trim();
15660
- if (NUMERIC_PATTERN.test(normalizedTtl)) {
15661
- return Number(normalizedTtl);
15662
- }
15663
- const match = normalizedTtl.match(TTL_PATTERN);
15664
- if (!match) {
15665
- throw new Error(`Invalid cache TTL format: ${ttl}`);
15666
- }
15667
- const value = Number(match[1]);
15668
- const unit = match[2];
15669
- return value * UNIT_TO_MS[unit];
15686
+ var BOOLEAN_META_KEYS = new Set(["isSubagent", "compartmentInProgress"]);
15687
+ function isSessionMetaRow(row) {
15688
+ if (row === null || typeof row !== "object")
15689
+ return false;
15690
+ const r = row;
15691
+ 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");
15670
15692
  }
15671
- function createScheduler(config2) {
15693
+ function getDefaultSessionMeta(sessionId) {
15672
15694
  return {
15673
- shouldExecute(sessionMeta, contextUsage, currentTime = Date.now(), sessionId) {
15674
- const threshold = resolveExecuteThreshold(config2.executeThresholdPercentage, undefined, 65);
15675
- if (contextUsage.percentage >= threshold) {
15676
- return "execute";
15677
- }
15678
- let ttlMs;
15679
- try {
15680
- ttlMs = parseCacheTtl(sessionMeta.cacheTtl);
15681
- } catch (error48) {
15682
- if (sessionId) {
15683
- sessionLog(sessionId, `invalid cache_ttl "${sessionMeta.cacheTtl}"; falling back to default 5m`, error48);
15684
- } else {
15685
- log(`[magic-context] invalid cache_ttl "${sessionMeta.cacheTtl}"; falling back to default 5m`, error48);
15686
- }
15687
- ttlMs = parseCacheTtl("5m");
15688
- }
15689
- const elapsedTime = currentTime - sessionMeta.lastResponseTime;
15690
- if (elapsedTime > ttlMs) {
15691
- return "execute";
15692
- }
15693
- return "defer";
15694
- }
15695
+ sessionId,
15696
+ lastResponseTime: 0,
15697
+ cacheTtl: "5m",
15698
+ counter: 0,
15699
+ lastNudgeTokens: 0,
15700
+ lastNudgeBand: null,
15701
+ lastTransformError: null,
15702
+ isSubagent: false,
15703
+ lastContextPercentage: 0,
15704
+ lastInputTokens: 0,
15705
+ timesExecuteThresholdReached: 0,
15706
+ compartmentInProgress: false,
15707
+ systemPromptHash: ""
15695
15708
  };
15696
15709
  }
15697
-
15698
- // src/features/magic-context/tagger.ts
15699
- var GET_COUNTER_SQL = `SELECT counter FROM session_meta WHERE session_id = ?`;
15700
- var GET_ASSIGNMENTS_SQL = "SELECT message_id, tag_number FROM tags WHERE session_id = ? ORDER BY tag_number ASC";
15701
- function isAssignmentRow(row) {
15702
- if (row === null || typeof row !== "object") {
15703
- return false;
15704
- }
15705
- const candidate = row;
15706
- return typeof candidate.message_id === "string" && typeof candidate.tag_number === "number";
15707
- }
15708
- var UPSERT_COUNTER_SQL = `
15709
- INSERT INTO session_meta (session_id, counter)
15710
- VALUES (?, ?)
15711
- ON CONFLICT(session_id) DO UPDATE SET counter = excluded.counter
15712
- `;
15713
- var upsertCounterStatements = new WeakMap;
15714
- function getUpsertCounterStatement(db) {
15715
- let stmt = upsertCounterStatements.get(db);
15716
- if (!stmt) {
15717
- stmt = db.prepare(UPSERT_COUNTER_SQL);
15718
- upsertCounterStatements.set(db, stmt);
15719
- }
15720
- return stmt;
15710
+ function ensureSessionMetaRow(db, sessionId) {
15711
+ const defaults = getDefaultSessionMeta(sessionId);
15712
+ 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) 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 ?? "");
15721
15713
  }
15722
- function createTagger() {
15723
- const counters = new Map;
15724
- const assignments = new Map;
15725
- function getSessionAssignments(sessionId) {
15726
- let map2 = assignments.get(sessionId);
15727
- if (!map2) {
15728
- map2 = new Map;
15729
- assignments.set(sessionId, map2);
15730
- }
15731
- return map2;
15732
- }
15733
- function assignTag(sessionId, messageId, type, byteSize, db) {
15734
- const sessionAssignments = getSessionAssignments(sessionId);
15735
- const existing = sessionAssignments.get(messageId);
15736
- if (existing !== undefined) {
15737
- return existing;
15738
- }
15739
- const current = counters.get(sessionId) ?? 0;
15740
- const next = current + 1;
15741
- db.transaction(() => {
15742
- insertTag(db, sessionId, messageId, type, byteSize, next);
15743
- getUpsertCounterStatement(db).run(sessionId, next);
15744
- })();
15745
- counters.set(sessionId, next);
15746
- sessionAssignments.set(messageId, next);
15747
- return next;
15748
- }
15749
- function getTag(sessionId, messageId) {
15750
- return assignments.get(sessionId)?.get(messageId);
15751
- }
15752
- function bindTag(sessionId, messageId, tagNumber) {
15753
- getSessionAssignments(sessionId).set(messageId, tagNumber);
15754
- }
15755
- function getAssignments(sessionId) {
15756
- return getSessionAssignments(sessionId);
15714
+ function toSessionMeta(row) {
15715
+ return {
15716
+ sessionId: row.session_id,
15717
+ lastResponseTime: row.last_response_time,
15718
+ cacheTtl: row.cache_ttl,
15719
+ counter: row.counter,
15720
+ lastNudgeTokens: row.last_nudge_tokens,
15721
+ lastNudgeBand: row.last_nudge_band.length > 0 ? row.last_nudge_band : null,
15722
+ lastTransformError: row.last_transform_error.length > 0 ? row.last_transform_error : null,
15723
+ isSubagent: row.is_subagent === 1,
15724
+ lastContextPercentage: row.last_context_percentage,
15725
+ lastInputTokens: row.last_input_tokens,
15726
+ timesExecuteThresholdReached: row.times_execute_threshold_reached,
15727
+ compartmentInProgress: row.compartment_in_progress === 1,
15728
+ systemPromptHash: String(row.system_prompt_hash)
15729
+ };
15730
+ }
15731
+
15732
+ // src/features/magic-context/storage-meta-persisted.ts
15733
+ function isPersistedUsageRow(row) {
15734
+ if (row === null || typeof row !== "object")
15735
+ return false;
15736
+ const r = row;
15737
+ return typeof r.last_context_percentage === "number" && typeof r.last_input_tokens === "number" && typeof r.last_response_time === "number";
15738
+ }
15739
+ function isPersistedNudgePlacementRow(row) {
15740
+ if (row === null || typeof row !== "object")
15741
+ return false;
15742
+ const r = row;
15743
+ return typeof r.nudge_anchor_message_id === "string" && typeof r.nudge_anchor_text === "string";
15744
+ }
15745
+ function isPersistedStickyTurnReminderRow(row) {
15746
+ if (row === null || typeof row !== "object")
15747
+ return false;
15748
+ const r = row;
15749
+ return typeof r.sticky_turn_reminder_text === "string" && typeof r.sticky_turn_reminder_message_id === "string";
15750
+ }
15751
+ function loadPersistedUsage(db, sessionId) {
15752
+ const result = db.prepare("SELECT last_context_percentage, last_input_tokens, last_response_time FROM session_meta WHERE session_id = ?").get(sessionId);
15753
+ if (!isPersistedUsageRow(result) || result.last_context_percentage === 0 && result.last_input_tokens === 0) {
15754
+ return null;
15757
15755
  }
15758
- function resetCounter(sessionId, db) {
15759
- counters.set(sessionId, 0);
15760
- assignments.delete(sessionId);
15761
- getUpsertCounterStatement(db).run(sessionId, 0);
15756
+ return {
15757
+ usage: {
15758
+ percentage: result.last_context_percentage,
15759
+ inputTokens: result.last_input_tokens
15760
+ },
15761
+ updatedAt: result.last_response_time || Date.now()
15762
+ };
15763
+ }
15764
+ function getPersistedNudgePlacement(db, sessionId) {
15765
+ const result = db.prepare("SELECT nudge_anchor_message_id, nudge_anchor_text FROM session_meta WHERE session_id = ?").get(sessionId);
15766
+ if (!isPersistedNudgePlacementRow(result)) {
15767
+ return null;
15762
15768
  }
15763
- function getCounter(sessionId) {
15764
- return counters.get(sessionId) ?? 0;
15769
+ if (result.nudge_anchor_message_id.length === 0 || result.nudge_anchor_text.length === 0) {
15770
+ return null;
15765
15771
  }
15766
- function initFromDb(sessionId, db) {
15767
- if (counters.has(sessionId)) {
15768
- return;
15769
- }
15770
- const row = db.prepare(GET_COUNTER_SQL).get(sessionId);
15771
- const assignmentRows = db.prepare(GET_ASSIGNMENTS_SQL).all(sessionId).filter(isAssignmentRow);
15772
- const sessionAssignments = getSessionAssignments(sessionId);
15773
- sessionAssignments.clear();
15774
- let maxTagNumber = 0;
15775
- for (const assignment of assignmentRows) {
15776
- sessionAssignments.set(assignment.message_id, assignment.tag_number);
15777
- if (assignment.tag_number > maxTagNumber) {
15778
- maxTagNumber = assignment.tag_number;
15779
- }
15780
- }
15781
- const counter = Math.max(row?.counter ?? 0, maxTagNumber);
15782
- counters.set(sessionId, counter);
15772
+ return {
15773
+ messageId: result.nudge_anchor_message_id,
15774
+ nudgeText: result.nudge_anchor_text
15775
+ };
15776
+ }
15777
+ function setPersistedNudgePlacement(db, sessionId, messageId, nudgeText) {
15778
+ db.transaction(() => {
15779
+ ensureSessionMetaRow(db, sessionId);
15780
+ db.prepare("UPDATE session_meta SET nudge_anchor_message_id = ?, nudge_anchor_text = ? WHERE session_id = ?").run(messageId, nudgeText, sessionId);
15781
+ })();
15782
+ }
15783
+ function clearPersistedNudgePlacement(db, sessionId) {
15784
+ db.prepare("UPDATE session_meta SET nudge_anchor_message_id = '', nudge_anchor_text = '' WHERE session_id = ?").run(sessionId);
15785
+ }
15786
+ function getPersistedStickyTurnReminder(db, sessionId) {
15787
+ const result = db.prepare("SELECT sticky_turn_reminder_text, sticky_turn_reminder_message_id FROM session_meta WHERE session_id = ?").get(sessionId);
15788
+ if (!isPersistedStickyTurnReminderRow(result)) {
15789
+ return null;
15783
15790
  }
15784
- function cleanup(sessionId) {
15785
- counters.delete(sessionId);
15786
- assignments.delete(sessionId);
15791
+ if (result.sticky_turn_reminder_text.length === 0) {
15792
+ return null;
15787
15793
  }
15788
15794
  return {
15789
- assignTag,
15790
- getTag,
15791
- bindTag,
15792
- getAssignments,
15793
- resetCounter,
15794
- getCounter,
15795
- initFromDb,
15796
- cleanup
15795
+ text: result.sticky_turn_reminder_text,
15796
+ messageId: result.sticky_turn_reminder_message_id.length > 0 ? result.sticky_turn_reminder_message_id : null
15797
15797
  };
15798
15798
  }
15799
+ function setPersistedStickyTurnReminder(db, sessionId, text, messageId = "") {
15800
+ db.transaction(() => {
15801
+ ensureSessionMetaRow(db, sessionId);
15802
+ db.prepare("UPDATE session_meta SET sticky_turn_reminder_text = ?, sticky_turn_reminder_message_id = ? WHERE session_id = ?").run(text, messageId, sessionId);
15803
+ })();
15804
+ }
15805
+ function clearPersistedStickyTurnReminder(db, sessionId) {
15806
+ db.prepare("UPDATE session_meta SET sticky_turn_reminder_text = '', sticky_turn_reminder_message_id = '' WHERE session_id = ?").run(sessionId);
15807
+ }
15808
+ // src/shared/internal-initiator-marker.ts
15809
+ var OMO_INTERNAL_INITIATOR_MARKER = "<!-- OMO_INTERNAL_INITIATOR -->";
15799
15810
 
15800
- // src/features/magic-context/dreamer/storage-dream-state.ts
15801
- var getDreamStateStatements = new WeakMap;
15802
- var setDreamStateStatements = new WeakMap;
15803
- var deleteDreamStateStatements = new WeakMap;
15804
- function getGetDreamStateStatement(db) {
15805
- let stmt = getDreamStateStatements.get(db);
15806
- if (!stmt) {
15807
- stmt = db.prepare("SELECT value FROM dream_state WHERE key = ?");
15808
- getDreamStateStatements.set(db, stmt);
15811
+ // src/shared/system-directive.ts
15812
+ var SYSTEM_DIRECTIVE_PREFIX = "[SYSTEM DIRECTIVE: MAGIC-CONTEXT";
15813
+ function isSystemDirective(text) {
15814
+ return text.trimStart().startsWith(SYSTEM_DIRECTIVE_PREFIX);
15815
+ }
15816
+ function removeSystemReminders(text) {
15817
+ return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, "").trim();
15818
+ }
15819
+
15820
+ // src/hooks/magic-context/read-session-db.ts
15821
+ import { Database as Database2 } from "bun:sqlite";
15822
+ import { join as join6 } from "path";
15823
+ function getOpenCodeDbPath() {
15824
+ return join6(getDataDir(), "opencode", "opencode.db");
15825
+ }
15826
+ var cachedReadOnlyDb = null;
15827
+ function closeCachedReadOnlyDb() {
15828
+ if (!cachedReadOnlyDb) {
15829
+ return;
15830
+ }
15831
+ try {
15832
+ cachedReadOnlyDb.db.close(false);
15833
+ } catch (error48) {
15834
+ log("[magic-context] failed to close cached OpenCode read-only DB:", error48);
15835
+ } finally {
15836
+ cachedReadOnlyDb = null;
15809
15837
  }
15810
- return stmt;
15811
15838
  }
15812
- function getSetDreamStateStatement(db) {
15813
- let stmt = setDreamStateStatements.get(db);
15814
- if (!stmt) {
15815
- stmt = db.prepare("INSERT INTO dream_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value");
15816
- setDreamStateStatements.set(db, stmt);
15839
+ function getReadOnlySessionDb() {
15840
+ const dbPath = getOpenCodeDbPath();
15841
+ if (cachedReadOnlyDb?.path === dbPath) {
15842
+ return cachedReadOnlyDb.db;
15817
15843
  }
15818
- return stmt;
15844
+ closeCachedReadOnlyDb();
15845
+ const db = new Database2(dbPath, { readonly: true });
15846
+ cachedReadOnlyDb = { path: dbPath, db };
15847
+ return db;
15819
15848
  }
15820
- function getDeleteDreamStateStatement(db) {
15821
- let stmt = deleteDreamStateStatements.get(db);
15822
- if (!stmt) {
15823
- stmt = db.prepare("DELETE FROM dream_state WHERE key = ?");
15824
- deleteDreamStateStatements.set(db, stmt);
15849
+ function withReadOnlySessionDb(fn) {
15850
+ return fn(getReadOnlySessionDb());
15851
+ }
15852
+ function getRawSessionMessageCountFromDb(db, sessionId) {
15853
+ const row = db.prepare("SELECT COUNT(*) as count FROM message WHERE session_id = ?").get(sessionId);
15854
+ return typeof row?.count === "number" ? row.count : 0;
15855
+ }
15856
+
15857
+ // src/hooks/magic-context/read-session-formatting.ts
15858
+ var COMMIT_HASH_PATTERN = /`?\b([0-9a-f]{6,12})\b`?/gi;
15859
+ var COMMIT_HINT_PATTERN = /\b(commit(?:ted)?|cherry-?pick(?:ed)?|hash(?:es)?|sha)\b/i;
15860
+ var MAX_COMMITS_PER_BLOCK = 5;
15861
+ function hasMeaningfulUserText(parts) {
15862
+ for (const part of parts) {
15863
+ if (part === null || typeof part !== "object")
15864
+ continue;
15865
+ const candidate = part;
15866
+ if (candidate.type !== "text" || typeof candidate.text !== "string")
15867
+ continue;
15868
+ if (candidate.ignored === true)
15869
+ continue;
15870
+ const cleaned = removeSystemReminders(candidate.text).replace(OMO_INTERNAL_INITIATOR_MARKER, "").trim();
15871
+ if (!cleaned)
15872
+ continue;
15873
+ if (isSystemDirective(cleaned))
15874
+ continue;
15875
+ return true;
15825
15876
  }
15826
- return stmt;
15877
+ return false;
15827
15878
  }
15828
- function getDreamState(db, key) {
15829
- const row = getGetDreamStateStatement(db).get(key);
15830
- return typeof row?.value === "string" ? row.value : null;
15879
+ function extractTexts(parts) {
15880
+ const texts = [];
15881
+ for (const part of parts) {
15882
+ if (part === null || typeof part !== "object")
15883
+ continue;
15884
+ const p = part;
15885
+ if (p.type === "text" && typeof p.text === "string" && p.text.trim().length > 0) {
15886
+ texts.push(p.text.trim());
15887
+ }
15888
+ }
15889
+ return texts;
15831
15890
  }
15832
- function setDreamState(db, key, value) {
15833
- getSetDreamStateStatement(db).run(key, value);
15891
+ function estimateTokens(text) {
15892
+ return Math.ceil(text.length / 3.5);
15834
15893
  }
15835
- function deleteDreamState(db, key) {
15836
- getDeleteDreamStateStatement(db).run(key);
15894
+ function normalizeText(text) {
15895
+ return text.replace(/\s+/g, " ").trim();
15837
15896
  }
15838
-
15839
- // src/features/magic-context/dreamer/lease.ts
15840
- var LEASE_HOLDER_KEY = "dreaming_lease_holder";
15841
- var LEASE_HEARTBEAT_KEY = "dreaming_lease_heartbeat";
15842
- var LEASE_EXPIRY_KEY = "dreaming_lease_expiry";
15843
- var LEASE_DURATION_MS = 2 * 60 * 1000;
15844
- function getLeaseExpiry(db) {
15845
- const value = getDreamState(db, LEASE_EXPIRY_KEY);
15846
- if (!value) {
15847
- return null;
15897
+ function compactRole(role) {
15898
+ if (role === "assistant")
15899
+ return "A";
15900
+ if (role === "user")
15901
+ return "U";
15902
+ return role.slice(0, 1).toUpperCase() || "M";
15903
+ }
15904
+ function formatBlock(block) {
15905
+ const range = block.startOrdinal === block.endOrdinal ? `[${block.startOrdinal}]` : `[${block.startOrdinal}-${block.endOrdinal}]`;
15906
+ const commitSuffix = block.commitHashes.length > 0 ? ` commits: ${block.commitHashes.join(", ")}` : "";
15907
+ return `${range} ${block.role}:${commitSuffix} ${block.parts.join(" / ")}`;
15908
+ }
15909
+ function extractCommitHashes(text) {
15910
+ const hashes = [];
15911
+ const seen = new Set;
15912
+ for (const match of text.matchAll(COMMIT_HASH_PATTERN)) {
15913
+ const hash2 = match[1]?.toLowerCase();
15914
+ if (!hash2 || seen.has(hash2))
15915
+ continue;
15916
+ seen.add(hash2);
15917
+ hashes.push(hash2);
15918
+ if (hashes.length >= MAX_COMMITS_PER_BLOCK)
15919
+ break;
15848
15920
  }
15849
- const expiry = Number(value);
15850
- return Number.isFinite(expiry) ? expiry : null;
15921
+ return hashes;
15922
+ }
15923
+ function compactTextForSummary(text, role) {
15924
+ const commitHashes = role === "assistant" ? extractCommitHashes(text) : [];
15925
+ if (commitHashes.length === 0 || !COMMIT_HINT_PATTERN.test(text)) {
15926
+ return { text, commitHashes };
15927
+ }
15928
+ 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();
15929
+ return {
15930
+ text: withoutHashes.length > 0 ? withoutHashes : text,
15931
+ commitHashes
15932
+ };
15851
15933
  }
15852
- function isLeaseActive(db) {
15853
- const expiry = getLeaseExpiry(db);
15854
- return expiry !== null && expiry > Date.now();
15934
+ function mergeCommitHashes(existing, next) {
15935
+ if (next.length === 0)
15936
+ return existing;
15937
+ const merged = [...existing];
15938
+ for (const hash2 of next) {
15939
+ if (merged.includes(hash2))
15940
+ continue;
15941
+ merged.push(hash2);
15942
+ if (merged.length >= MAX_COMMITS_PER_BLOCK)
15943
+ break;
15944
+ }
15945
+ return merged;
15855
15946
  }
15856
- function getLeaseHolder(db) {
15857
- return getDreamState(db, LEASE_HOLDER_KEY);
15947
+
15948
+ // src/hooks/magic-context/read-session-raw.ts
15949
+ function isRawMessageRow(row) {
15950
+ if (row === null || typeof row !== "object")
15951
+ return false;
15952
+ const candidate = row;
15953
+ return typeof candidate.id === "string" && typeof candidate.data === "string";
15858
15954
  }
15859
- function acquireLease(db, holderId) {
15860
- return db.transaction(() => {
15861
- if (isLeaseActive(db)) {
15862
- const existingHolder = getLeaseHolder(db);
15863
- if (existingHolder && existingHolder !== holderId) {
15864
- return false;
15865
- }
15866
- }
15867
- const now = Date.now();
15868
- setDreamState(db, LEASE_HOLDER_KEY, holderId);
15869
- setDreamState(db, LEASE_HEARTBEAT_KEY, String(now));
15870
- setDreamState(db, LEASE_EXPIRY_KEY, String(now + LEASE_DURATION_MS));
15871
- return true;
15872
- })();
15955
+ function isRawPartRow(row) {
15956
+ if (row === null || typeof row !== "object")
15957
+ return false;
15958
+ const candidate = row;
15959
+ return typeof candidate.message_id === "string" && typeof candidate.data === "string";
15873
15960
  }
15874
- function renewLease(db, holderId) {
15875
- return db.transaction(() => {
15876
- if (getLeaseHolder(db) !== holderId || !isLeaseActive(db)) {
15877
- return false;
15878
- }
15879
- const now = Date.now();
15880
- setDreamState(db, LEASE_HEARTBEAT_KEY, String(now));
15881
- setDreamState(db, LEASE_EXPIRY_KEY, String(now + LEASE_DURATION_MS));
15882
- return true;
15883
- })();
15961
+ function parseJsonRecord(value) {
15962
+ const parsed = JSON.parse(value);
15963
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
15964
+ throw new Error("Expected JSON object");
15965
+ }
15966
+ return parsed;
15884
15967
  }
15885
- function releaseLease(db, holderId) {
15886
- db.transaction(() => {
15887
- if (getLeaseHolder(db) !== holderId) {
15888
- return;
15889
- }
15890
- deleteDreamState(db, LEASE_HOLDER_KEY);
15891
- deleteDreamState(db, LEASE_HEARTBEAT_KEY);
15892
- deleteDreamState(db, LEASE_EXPIRY_KEY);
15893
- })();
15968
+ function parseJsonUnknown(value) {
15969
+ return JSON.parse(value);
15894
15970
  }
15895
- // src/features/magic-context/dreamer/queue.ts
15896
- function enqueueDream(db, projectIdentity, reason) {
15897
- const now = Date.now();
15898
- return db.transaction(() => {
15899
- const existing = db.query("SELECT id FROM dream_queue WHERE project_path = ?").get(projectIdentity);
15900
- if (existing) {
15901
- return null;
15902
- }
15903
- const result = db.prepare("INSERT INTO dream_queue (project_path, reason, enqueued_at) VALUES (?, ?, ?)").run(projectIdentity, reason, now);
15971
+ function readRawSessionMessagesFromDb(db, sessionId) {
15972
+ const messageRows = db.prepare("SELECT id, data FROM message WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId).filter(isRawMessageRow);
15973
+ const partRows = db.prepare("SELECT message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId).filter(isRawPartRow);
15974
+ const partsByMessageId = new Map;
15975
+ for (const part of partRows) {
15976
+ const list = partsByMessageId.get(part.message_id) ?? [];
15977
+ list.push(parseJsonUnknown(part.data));
15978
+ partsByMessageId.set(part.message_id, list);
15979
+ }
15980
+ return messageRows.map((row, index) => {
15981
+ const info = parseJsonRecord(row.data);
15982
+ const role = typeof info.role === "string" ? info.role : "unknown";
15904
15983
  return {
15905
- id: Number(result.lastInsertRowid),
15906
- projectIdentity,
15907
- reason,
15908
- enqueuedAt: now,
15909
- startedAt: null
15984
+ ordinal: index + 1,
15985
+ id: row.id,
15986
+ role,
15987
+ parts: partsByMessageId.get(row.id) ?? []
15910
15988
  };
15911
- })();
15989
+ });
15912
15990
  }
15913
- function peekQueue(db) {
15914
- const row = db.query("SELECT id, project_path, reason, enqueued_at FROM dream_queue WHERE started_at IS NULL ORDER BY enqueued_at ASC LIMIT 1").get();
15915
- if (!row)
15916
- return null;
15917
- return {
15918
- id: row.id,
15919
- projectIdentity: row.project_path,
15920
- reason: row.reason,
15921
- enqueuedAt: row.enqueued_at,
15922
- startedAt: null
15923
- };
15991
+
15992
+ // src/hooks/magic-context/tag-content-primitives.ts
15993
+ var encoder = new TextEncoder;
15994
+ var TAG_PREFIX_REGEX = /^\u00A7\d+\u00A7\s*/;
15995
+ function byteSize(value) {
15996
+ return encoder.encode(value).length;
15924
15997
  }
15925
- function dequeueNext(db) {
15926
- const now = Date.now();
15927
- return db.transaction(() => {
15928
- const entry = peekQueue(db);
15929
- if (!entry)
15930
- return null;
15931
- const result = db.prepare("UPDATE dream_queue SET started_at = ? WHERE id = ? AND started_at IS NULL").run(now, entry.id);
15932
- if (result.changes === 0)
15933
- return null;
15934
- return { ...entry, startedAt: now };
15935
- })();
15998
+ function stripTagPrefix(value) {
15999
+ return value.replace(TAG_PREFIX_REGEX, "");
15936
16000
  }
15937
- function removeDreamEntry(db, id) {
15938
- db.prepare("DELETE FROM dream_queue WHERE id = ?").run(id);
16001
+ function prependTag(tagId, value) {
16002
+ const stripped = stripTagPrefix(value);
16003
+ return `\xA7${tagId}\xA7 ${stripped}`;
15939
16004
  }
15940
- function resetDreamEntry(db, id) {
15941
- db.prepare("UPDATE dream_queue SET started_at = NULL, retry_count = COALESCE(retry_count, 0) + 1 WHERE id = ?").run(id);
16005
+ function isThinkingPart(part) {
16006
+ if (part === null || typeof part !== "object")
16007
+ return false;
16008
+ const candidate = part;
16009
+ return candidate.type === "thinking" || candidate.type === "reasoning";
15942
16010
  }
15943
- function getEntryRetryCount(db, id) {
15944
- const row = db.query("SELECT retry_count FROM dream_queue WHERE id = ?").get(id);
15945
- return row?.retry_count ?? 0;
16011
+
16012
+ // src/hooks/magic-context/tag-part-guards.ts
16013
+ function isTextPart(part) {
16014
+ if (part === null || typeof part !== "object")
16015
+ return false;
16016
+ const p = part;
16017
+ return p.type === "text" && typeof p.text === "string";
15946
16018
  }
15947
- function clearStaleEntries(db, maxAgeMs) {
15948
- const cutoff = Date.now() - maxAgeMs;
15949
- const result = db.prepare("DELETE FROM dream_queue WHERE started_at IS NOT NULL AND started_at < ?").run(cutoff);
15950
- return result.changes;
16019
+ function isToolPartWithOutput(part) {
16020
+ if (part === null || typeof part !== "object")
16021
+ return false;
16022
+ const p = part;
16023
+ if (p.type !== "tool" || typeof p.callID !== "string")
16024
+ return false;
16025
+ if (p.state === null || typeof p.state !== "object")
16026
+ return false;
16027
+ return typeof p.state.output === "string";
15951
16028
  }
15952
- // src/features/magic-context/dreamer/runner.ts
15953
- import { existsSync as existsSync3 } from "fs";
15954
- import { join as join5 } from "path";
15955
- var dreamProjectDirectories = new Map;
15956
- function registerDreamProjectDirectory(projectIdentity, directory) {
15957
- dreamProjectDirectories.set(projectIdentity, directory);
16029
+ function isFilePart(part) {
16030
+ if (part === null || typeof part !== "object")
16031
+ return false;
16032
+ const p = part;
16033
+ return p.type === "file" && typeof p.url === "string";
15958
16034
  }
15959
- function resolveDreamSessionDirectory(projectIdentity) {
15960
- return dreamProjectDirectories.get(projectIdentity) ?? projectIdentity;
16035
+ function buildFileSourceContent(parts) {
16036
+ const content = parts.filter(isTextPart).map((part) => stripTagPrefix(part.text)).join(`
16037
+ `).trim();
16038
+ return content.length > 0 ? content : null;
15961
16039
  }
15962
- async function runDream(args) {
15963
- const holderId = crypto.randomUUID();
15964
- const startedAt = Date.now();
15965
- const result = {
15966
- startedAt,
15967
- finishedAt: startedAt,
15968
- holderId,
15969
- tasks: []
15970
- };
15971
- log(`[dreamer] starting dream run: ${args.tasks.length} tasks, timeout=${args.taskTimeoutMinutes}m, maxRuntime=${args.maxRuntimeMinutes}m, project=${args.projectIdentity}`);
15972
- if (!acquireLease(args.db, holderId)) {
15973
- const currentHolder = getLeaseHolder(args.db) ?? "another holder";
15974
- log(`[dreamer] lease acquisition failed \u2014 already held by ${currentHolder}`);
15975
- result.tasks.push({
15976
- name: "lease",
15977
- durationMs: 0,
15978
- result: null,
15979
- error: `Dream lease is already held by ${currentHolder}`
15980
- });
15981
- result.finishedAt = Date.now();
15982
- return result;
16040
+
16041
+ // src/hooks/magic-context/read-session-chunk.ts
16042
+ var activeRawMessageCache = null;
16043
+ function cleanUserText(text) {
16044
+ return removeSystemReminders(text).replace(OMO_INTERNAL_INITIATOR_MARKER, "").trim();
16045
+ }
16046
+ function withRawSessionMessageCache(fn) {
16047
+ const outerCache = activeRawMessageCache;
16048
+ if (!outerCache) {
16049
+ activeRawMessageCache = new Map;
15983
16050
  }
15984
- log(`[dreamer] lease acquired: ${holderId}`);
15985
- let parentSessionId = args.parentSessionId;
15986
- if (!parentSessionId) {
15987
- try {
15988
- const sessionDir = args.sessionDirectory ?? args.projectIdentity;
15989
- const listResponse = await args.client.session.list({
15990
- query: { directory: sessionDir }
15991
- });
15992
- const sessions = normalizeSDKResponse(listResponse, [], {
15993
- preferResponseOnMissingData: true
15994
- });
15995
- parentSessionId = sessions?.find((s) => typeof s?.id === "string")?.id;
15996
- if (parentSessionId) {
15997
- log(`[dreamer] resolved parent session: ${parentSessionId}`);
15998
- }
15999
- } catch {
16000
- log("[dreamer] could not resolve parent session \u2014 child sessions will be visible in UI");
16051
+ try {
16052
+ return fn();
16053
+ } finally {
16054
+ if (!outerCache) {
16055
+ activeRawMessageCache = null;
16001
16056
  }
16002
16057
  }
16003
- const deadline = startedAt + args.maxRuntimeMinutes * 60 * 1000;
16004
- const lastDreamAt = getDreamState(args.db, `last_dream_at:${args.projectIdentity}`) ?? getDreamState(args.db, "last_dream_at");
16005
- log(`[dreamer] last dream at: ${lastDreamAt ?? "never"} (project=${args.projectIdentity})`);
16006
- try {
16007
- for (const taskName of args.tasks) {
16008
- if (Date.now() > deadline) {
16009
- log(`[dreamer] deadline reached, stopping after ${result.tasks.length} tasks`);
16010
- break;
16058
+ }
16059
+ function readRawSessionMessages(sessionId) {
16060
+ if (activeRawMessageCache) {
16061
+ const cached2 = activeRawMessageCache.get(sessionId);
16062
+ if (cached2) {
16063
+ return cached2;
16064
+ }
16065
+ const messages = withReadOnlySessionDb((db) => readRawSessionMessagesFromDb(db, sessionId));
16066
+ activeRawMessageCache.set(sessionId, messages);
16067
+ return messages;
16068
+ }
16069
+ return withReadOnlySessionDb((db) => readRawSessionMessagesFromDb(db, sessionId));
16070
+ }
16071
+ function getRawSessionMessageCount(sessionId) {
16072
+ return withReadOnlySessionDb((db) => getRawSessionMessageCountFromDb(db, sessionId));
16073
+ }
16074
+ function getRawSessionTagKeysThrough(sessionId, upToMessageIndex) {
16075
+ const messages = readRawSessionMessages(sessionId);
16076
+ const keys = [];
16077
+ for (const message of messages) {
16078
+ if (message.ordinal > upToMessageIndex)
16079
+ break;
16080
+ for (const [partIndex, part] of message.parts.entries()) {
16081
+ if (isTextPart(part)) {
16082
+ keys.push(`${message.id}:p${partIndex}`);
16011
16083
  }
16012
- log(`[dreamer] starting task: ${taskName}`);
16013
- const taskStartedAt = Date.now();
16014
- let agentSessionId = null;
16015
- const taskAbortController = new AbortController;
16016
- const leaseRenewalInterval = setInterval(() => {
16017
- try {
16018
- if (!renewLease(args.db, holderId)) {
16019
- log(`[dreamer] task ${taskName}: lease renewal failed \u2014 aborting LLM call`);
16020
- taskAbortController.abort();
16021
- }
16022
- } catch (err) {
16023
- log(`[dreamer] task ${taskName}: lease renewal threw \u2014 aborting LLM call: ${err}`);
16024
- taskAbortController.abort();
16025
- }
16026
- }, 60000);
16027
- try {
16028
- const docsDir = args.sessionDirectory ?? args.projectIdentity;
16029
- const existingDocs = taskName === "maintain-docs" ? {
16030
- architecture: existsSync3(join5(docsDir, "ARCHITECTURE.md")),
16031
- structure: existsSync3(join5(docsDir, "STRUCTURE.md"))
16032
- } : undefined;
16033
- const taskPrompt = buildDreamTaskPrompt(taskName, {
16034
- projectPath: args.projectIdentity,
16035
- lastDreamAt,
16036
- existingDocs
16037
- });
16038
- const createResponse = await args.client.session.create({
16039
- body: {
16040
- ...parentSessionId ? { parentID: parentSessionId } : {},
16041
- title: `magic-context-dream-${taskName}`
16042
- },
16043
- query: { directory: args.sessionDirectory ?? args.projectIdentity }
16044
- });
16045
- const createdSession = normalizeSDKResponse(createResponse, null, { preferResponseOnMissingData: true });
16046
- agentSessionId = typeof createdSession?.id === "string" ? createdSession.id : null;
16047
- if (!agentSessionId) {
16048
- throw new Error("Dreamer could not create its child session.");
16049
- }
16050
- log(`[dreamer] task ${taskName}: child session created ${agentSessionId}`);
16051
- await promptSyncWithModelSuggestionRetry(args.client, {
16052
- path: { id: agentSessionId },
16053
- query: { directory: args.sessionDirectory ?? args.projectIdentity },
16054
- body: {
16055
- agent: DREAMER_AGENT,
16056
- system: DREAMER_SYSTEM_PROMPT,
16057
- parts: [{ type: "text", text: taskPrompt }]
16058
- }
16059
- }, {
16060
- timeoutMs: args.taskTimeoutMinutes * 60 * 1000,
16061
- signal: taskAbortController.signal
16062
- });
16063
- const messagesResponse = await args.client.session.messages({
16064
- path: { id: agentSessionId },
16065
- query: { directory: args.sessionDirectory ?? args.projectIdentity }
16066
- });
16067
- const messages = normalizeSDKResponse(messagesResponse, [], {
16068
- preferResponseOnMissingData: true
16069
- });
16070
- const taskResult = extractLatestAssistantText(messages);
16071
- if (!taskResult) {
16072
- throw new Error("Dreamer returned no assistant output.");
16073
- }
16074
- const durationMs = Date.now() - taskStartedAt;
16075
- log(`[dreamer] task ${taskName}: completed in ${(durationMs / 1000).toFixed(1)}s (result: ${String(taskResult).length} chars)`);
16076
- result.tasks.push({
16077
- name: taskName,
16078
- durationMs,
16079
- result: taskResult
16080
- });
16081
- } catch (error48) {
16082
- const durationMs = Date.now() - taskStartedAt;
16083
- const errorMsg = getErrorMessage(error48);
16084
- log(`[dreamer] task ${taskName}: failed after ${(durationMs / 1000).toFixed(1)}s \u2014 ${errorMsg}`);
16085
- result.tasks.push({
16086
- name: taskName,
16087
- durationMs,
16088
- result: null,
16089
- error: errorMsg
16090
- });
16091
- } finally {
16092
- clearInterval(leaseRenewalInterval);
16093
- if (agentSessionId) {
16094
- await args.client.session.delete({
16095
- path: { id: agentSessionId },
16096
- query: { directory: args.sessionDirectory ?? args.projectIdentity }
16097
- }).catch((error48) => {
16098
- log("[dreamer] failed to delete child session:", error48);
16099
- });
16100
- }
16084
+ if (isFilePart(part)) {
16085
+ keys.push(`${message.id}:file${partIndex}`);
16086
+ }
16087
+ if (isToolPartWithOutput(part)) {
16088
+ keys.push(part.callID);
16101
16089
  }
16102
16090
  }
16103
- } finally {
16104
- releaseLease(args.db, holderId);
16105
- log(`[dreamer] lease released: ${holderId}`);
16106
- }
16107
- result.finishedAt = Date.now();
16108
- const hasSuccessfulTask = result.tasks.some((t) => !t.error);
16109
- if (hasSuccessfulTask) {
16110
- setDreamState(args.db, `last_dream_at:${args.projectIdentity}`, String(result.finishedAt));
16111
- setDreamState(args.db, "last_dream_at", String(result.finishedAt));
16112
16091
  }
16113
- const totalDuration = ((result.finishedAt - startedAt) / 1000).toFixed(1);
16114
- const succeeded = result.tasks.filter((t) => !t.error).length;
16115
- const failed = result.tasks.filter((t) => t.error).length;
16116
- log(`[dreamer] dream run finished in ${totalDuration}s: ${succeeded} succeeded, ${failed} failed`);
16117
- return result;
16092
+ return keys;
16118
16093
  }
16119
- var MAX_LEASE_RETRIES = 3;
16120
- async function processDreamQueue(args) {
16121
- clearStaleEntries(args.db, 2 * 60 * 60 * 1000);
16122
- const entry = dequeueNext(args.db);
16123
- if (!entry) {
16124
- return null;
16094
+ var PROTECTED_TAIL_USER_TURNS = 5;
16095
+ function getProtectedTailStartOrdinal(sessionId) {
16096
+ const messages = readRawSessionMessages(sessionId);
16097
+ const userOrdinals = messages.filter((m) => m.role === "user" && hasMeaningfulUserText(m.parts)).map((m) => m.ordinal);
16098
+ if (userOrdinals.length < PROTECTED_TAIL_USER_TURNS) {
16099
+ return 1;
16125
16100
  }
16126
- const projectDirectory = resolveDreamSessionDirectory(entry.projectIdentity);
16127
- log(`[dreamer] dequeued project ${entry.projectIdentity} (dir=${projectDirectory}), starting dream run`);
16128
- let result;
16129
- try {
16130
- result = await runDream({
16131
- db: args.db,
16132
- client: args.client,
16133
- projectIdentity: entry.projectIdentity,
16134
- tasks: args.tasks,
16135
- taskTimeoutMinutes: args.taskTimeoutMinutes,
16136
- maxRuntimeMinutes: args.maxRuntimeMinutes,
16137
- sessionDirectory: projectDirectory
16138
- });
16139
- } catch (error48) {
16140
- log(`[dreamer] runDream threw for ${entry.projectIdentity}: ${getErrorMessage(error48)}`);
16141
- removeDreamEntry(args.db, entry.id);
16142
- return null;
16101
+ return userOrdinals[userOrdinals.length - PROTECTED_TAIL_USER_TURNS];
16102
+ }
16103
+ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal) {
16104
+ const messages = readRawSessionMessages(sessionId);
16105
+ const startOrdinal = Math.max(1, offset);
16106
+ const lines = [];
16107
+ const lineMeta = [];
16108
+ let totalTokens = 0;
16109
+ let messagesProcessed = 0;
16110
+ let lastOrdinal = startOrdinal - 1;
16111
+ let lastMessageId = "";
16112
+ let firstMessageId = "";
16113
+ let currentBlock = null;
16114
+ let pendingNoiseMeta = [];
16115
+ let commitClusters = 0;
16116
+ let lastFlushedRole = "";
16117
+ function flushCurrentBlock() {
16118
+ if (!currentBlock)
16119
+ return true;
16120
+ const blockText = formatBlock(currentBlock);
16121
+ const blockTokens = estimateTokens(blockText);
16122
+ if (totalTokens + blockTokens > tokenBudget && totalTokens > 0) {
16123
+ return false;
16124
+ }
16125
+ if (currentBlock.role === "A" && currentBlock.commitHashes.length > 0 && lastFlushedRole !== "A") {
16126
+ commitClusters++;
16127
+ }
16128
+ lastFlushedRole = currentBlock.role;
16129
+ if (!firstMessageId)
16130
+ firstMessageId = currentBlock.meta[0]?.messageId ?? "";
16131
+ lastOrdinal = currentBlock.meta[currentBlock.meta.length - 1]?.ordinal ?? currentBlock.endOrdinal;
16132
+ lastMessageId = currentBlock.meta[currentBlock.meta.length - 1]?.messageId ?? "";
16133
+ messagesProcessed += currentBlock.meta.length;
16134
+ lines.push(blockText);
16135
+ lineMeta.push(...currentBlock.meta);
16136
+ totalTokens += blockTokens;
16137
+ currentBlock = null;
16138
+ return true;
16143
16139
  }
16144
- const leaseError = result.tasks.find((t) => t.name === "lease" && t.error);
16145
- if (leaseError) {
16146
- const retryCount = getEntryRetryCount(args.db, entry.id);
16147
- if (retryCount >= MAX_LEASE_RETRIES) {
16148
- log(`[dreamer] lease acquisition failed ${retryCount + 1} times for ${entry.projectIdentity} \u2014 removing queue entry`);
16149
- removeDreamEntry(args.db, entry.id);
16150
- } else {
16151
- log(`[dreamer] lease acquisition failed for ${entry.projectIdentity} (attempt ${retryCount + 1}/${MAX_LEASE_RETRIES}) \u2014 keeping for retry`);
16152
- resetDreamEntry(args.db, entry.id);
16140
+ for (const msg of messages) {
16141
+ if (eligibleEndOrdinal !== undefined && msg.ordinal >= eligibleEndOrdinal)
16142
+ break;
16143
+ if (msg.ordinal < startOrdinal)
16144
+ continue;
16145
+ const meta3 = { ordinal: msg.ordinal, messageId: msg.id };
16146
+ if (msg.role === "user" && !hasMeaningfulUserText(msg.parts)) {
16147
+ pendingNoiseMeta.push(meta3);
16148
+ continue;
16153
16149
  }
16154
- } else {
16155
- removeDreamEntry(args.db, entry.id);
16150
+ const role = compactRole(msg.role);
16151
+ const compacted = compactTextForSummary(extractTexts(msg.parts).map((t) => msg.role === "user" ? cleanUserText(t) : t).map(normalizeText).filter((value) => value.length > 0).join(" / "), msg.role);
16152
+ const text = compacted.text;
16153
+ if (!text) {
16154
+ pendingNoiseMeta.push(meta3);
16155
+ continue;
16156
+ }
16157
+ if (currentBlock && currentBlock.role === role) {
16158
+ currentBlock.endOrdinal = msg.ordinal;
16159
+ currentBlock.parts.push(text);
16160
+ currentBlock.meta.push(...pendingNoiseMeta, meta3);
16161
+ currentBlock.commitHashes = mergeCommitHashes(currentBlock.commitHashes, compacted.commitHashes);
16162
+ pendingNoiseMeta = [];
16163
+ continue;
16164
+ }
16165
+ if (!flushCurrentBlock())
16166
+ break;
16167
+ currentBlock = {
16168
+ role,
16169
+ startOrdinal: pendingNoiseMeta[0]?.ordinal ?? msg.ordinal,
16170
+ endOrdinal: msg.ordinal,
16171
+ parts: [text],
16172
+ meta: [...pendingNoiseMeta, meta3],
16173
+ commitHashes: [...compacted.commitHashes]
16174
+ };
16175
+ pendingNoiseMeta = [];
16156
16176
  }
16157
- return result;
16177
+ flushCurrentBlock();
16178
+ return {
16179
+ startIndex: startOrdinal,
16180
+ endIndex: lastOrdinal,
16181
+ startMessageId: firstMessageId,
16182
+ endMessageId: lastMessageId,
16183
+ messageCount: messagesProcessed,
16184
+ tokenEstimate: totalTokens,
16185
+ hasMore: lastOrdinal < (eligibleEndOrdinal !== undefined ? Math.min(eligibleEndOrdinal - 1, messages.length) : messages.length),
16186
+ text: lines.join(`
16187
+ `),
16188
+ lines: lineMeta,
16189
+ commitClusterCount: commitClusters
16190
+ };
16158
16191
  }
16159
- // src/features/magic-context/dreamer/scheduler.ts
16160
- function parseScheduleWindow(schedule) {
16161
- const match = /^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/.exec(schedule.trim());
16162
- if (!match)
16163
- return null;
16164
- const startHour = Number(match[1]);
16165
- const startMin = Number(match[2]);
16166
- const endHour = Number(match[3]);
16167
- const endMin = Number(match[4]);
16168
- if (startHour >= 24 || startMin >= 60 || endHour >= 24 || endMin >= 60) {
16169
- return null;
16192
+
16193
+ // src/features/magic-context/message-index.ts
16194
+ var lastIndexedStatements = new WeakMap;
16195
+ var insertMessageStatements = new WeakMap;
16196
+ var upsertIndexStatements = new WeakMap;
16197
+ var deleteFtsStatements = new WeakMap;
16198
+ var deleteIndexStatements = new WeakMap;
16199
+ function normalizeIndexText(text) {
16200
+ return text.replace(/\s+/g, " ").trim();
16201
+ }
16202
+ function getLastIndexedStatement(db) {
16203
+ let stmt = lastIndexedStatements.get(db);
16204
+ if (!stmt) {
16205
+ stmt = db.prepare("SELECT last_indexed_ordinal FROM message_history_index WHERE session_id = ?");
16206
+ lastIndexedStatements.set(db, stmt);
16170
16207
  }
16171
- const startMinutes = startHour * 60 + startMin;
16172
- const endMinutes = endHour * 60 + endMin;
16173
- return { startMinutes, endMinutes };
16208
+ return stmt;
16209
+ }
16210
+ function getInsertMessageStatement(db) {
16211
+ let stmt = insertMessageStatements.get(db);
16212
+ if (!stmt) {
16213
+ stmt = db.prepare("INSERT INTO message_history_fts (session_id, message_ordinal, message_id, role, content) VALUES (?, ?, ?, ?, ?)");
16214
+ insertMessageStatements.set(db, stmt);
16215
+ }
16216
+ return stmt;
16217
+ }
16218
+ function getUpsertIndexStatement(db) {
16219
+ let stmt = upsertIndexStatements.get(db);
16220
+ if (!stmt) {
16221
+ 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");
16222
+ upsertIndexStatements.set(db, stmt);
16223
+ }
16224
+ return stmt;
16225
+ }
16226
+ function getDeleteFtsStatement(db) {
16227
+ let stmt = deleteFtsStatements.get(db);
16228
+ if (!stmt) {
16229
+ stmt = db.prepare("DELETE FROM message_history_fts WHERE session_id = ?");
16230
+ deleteFtsStatements.set(db, stmt);
16231
+ }
16232
+ return stmt;
16174
16233
  }
16175
- function isInScheduleWindow(schedule, now = new Date) {
16176
- const window = parseScheduleWindow(schedule);
16177
- if (!window)
16178
- return false;
16179
- const currentMinutes = now.getHours() * 60 + now.getMinutes();
16180
- if (window.startMinutes <= window.endMinutes) {
16181
- return currentMinutes >= window.startMinutes && currentMinutes < window.endMinutes;
16234
+ function getDeleteIndexStatement(db) {
16235
+ let stmt = deleteIndexStatements.get(db);
16236
+ if (!stmt) {
16237
+ stmt = db.prepare("DELETE FROM message_history_index WHERE session_id = ?");
16238
+ deleteIndexStatements.set(db, stmt);
16182
16239
  }
16183
- return currentMinutes >= window.startMinutes || currentMinutes < window.endMinutes;
16240
+ return stmt;
16184
16241
  }
16185
- function findProjectsNeedingDream(db) {
16186
- const projectRows = db.query(`SELECT DISTINCT project_path FROM memories WHERE status = 'active' ORDER BY project_path`).all();
16187
- const projects = [];
16188
- for (const row of projectRows) {
16189
- const lastDreamAtStr = getDreamState(db, `last_dream_at:${row.project_path}`);
16190
- const fallbackStr = !lastDreamAtStr ? getDreamState(db, "last_dream_at") : null;
16191
- const lastDreamAt = Number(lastDreamAtStr ?? fallbackStr ?? "0") || 0;
16192
- const updated = db.query(`SELECT COUNT(*) as cnt FROM memories
16193
- WHERE project_path = ? AND status = 'active' AND updated_at > ?`).get(row.project_path, lastDreamAt);
16194
- if (updated && updated.cnt > 0) {
16195
- projects.push(row.project_path);
16242
+ function getLastIndexedOrdinal(db, sessionId) {
16243
+ const row = getLastIndexedStatement(db).get(sessionId);
16244
+ return typeof row?.last_indexed_ordinal === "number" ? row.last_indexed_ordinal : 0;
16245
+ }
16246
+ function clearIndexedMessages(db, sessionId) {
16247
+ getDeleteFtsStatement(db).run(sessionId);
16248
+ getDeleteIndexStatement(db).run(sessionId);
16249
+ }
16250
+ function getIndexableContent(role, parts) {
16251
+ if (role === "user") {
16252
+ if (!hasMeaningfulUserText(parts)) {
16253
+ return "";
16196
16254
  }
16255
+ return extractTexts(parts).map(cleanUserText).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
16197
16256
  }
16198
- return projects;
16257
+ if (role === "assistant") {
16258
+ return extractTexts(parts).map(removeSystemReminders).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
16259
+ }
16260
+ return "";
16199
16261
  }
16200
- function checkScheduleAndEnqueue(db, schedule) {
16201
- if (!isInScheduleWindow(schedule)) {
16202
- return 0;
16262
+ function ensureMessagesIndexed(db, sessionId, readMessages) {
16263
+ const messages = readMessages(sessionId);
16264
+ if (messages.length === 0) {
16265
+ db.transaction(() => clearIndexedMessages(db, sessionId))();
16266
+ return;
16203
16267
  }
16204
- const projects = findProjectsNeedingDream(db);
16205
- if (projects.length === 0) {
16206
- return 0;
16268
+ let lastIndexedOrdinal = getLastIndexedOrdinal(db, sessionId);
16269
+ if (lastIndexedOrdinal > messages.length) {
16270
+ db.transaction(() => clearIndexedMessages(db, sessionId))();
16271
+ lastIndexedOrdinal = 0;
16207
16272
  }
16208
- let enqueued = 0;
16209
- for (const projectIdentity of projects) {
16210
- const entry = enqueueDream(db, projectIdentity, "scheduled");
16211
- if (entry) {
16212
- log(`[dreamer] enqueued project for scheduled dream: ${projectIdentity}`);
16213
- enqueued++;
16273
+ if (lastIndexedOrdinal >= messages.length) {
16274
+ return;
16275
+ }
16276
+ const messagesToInsert = messages.filter((message) => message.ordinal > lastIndexedOrdinal).filter((message) => message.role === "user" || message.role === "assistant").map((message) => ({
16277
+ ordinal: message.ordinal,
16278
+ id: message.id,
16279
+ role: message.role,
16280
+ content: getIndexableContent(message.role, message.parts)
16281
+ })).filter((message) => message.content.length > 0);
16282
+ const now = Date.now();
16283
+ db.transaction(() => {
16284
+ const insertMessage = getInsertMessageStatement(db);
16285
+ for (const message of messagesToInsert) {
16286
+ insertMessage.run(sessionId, message.ordinal, message.id, message.role, message.content);
16214
16287
  }
16288
+ getUpsertIndexStatement(db).run(sessionId, messages.length, now);
16289
+ })();
16290
+ }
16291
+
16292
+ // src/features/magic-context/storage-meta-session.ts
16293
+ function getOrCreateSessionMeta(db, sessionId) {
16294
+ 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 FROM session_meta WHERE session_id = ?").get(sessionId);
16295
+ if (isSessionMetaRow(result)) {
16296
+ return toSessionMeta(result);
16215
16297
  }
16216
- return enqueued;
16298
+ const defaults = getDefaultSessionMeta(sessionId);
16299
+ ensureSessionMetaRow(db, sessionId);
16300
+ return defaults;
16217
16301
  }
16218
- // src/features/magic-context/memory/project-identity.ts
16219
- import { execSync } from "child_process";
16220
- import path3 from "path";
16221
- var GIT_TIMEOUT_MS = 5000;
16222
- var resolvedCache = new Map;
16223
- function getRootCommitHash(directory) {
16224
- try {
16225
- const hash2 = execSync("git rev-list --max-parents=0 HEAD", {
16226
- cwd: directory,
16227
- encoding: "utf-8",
16228
- stdio: ["pipe", "pipe", "pipe"],
16229
- timeout: GIT_TIMEOUT_MS
16230
- }).trim();
16231
- const firstLine = hash2.split(`
16232
- `)[0]?.trim();
16233
- return firstLine && firstLine.length >= 7 ? firstLine : undefined;
16234
- } catch {
16302
+ function updateSessionMeta(db, sessionId, updates) {
16303
+ const setClauses = [];
16304
+ const values = [];
16305
+ for (const [key, column] of Object.entries(META_COLUMNS)) {
16306
+ const value = updates[key];
16307
+ if (value === undefined)
16308
+ continue;
16309
+ if (value === null) {
16310
+ setClauses.push(`${column} = ?`);
16311
+ values.push("");
16312
+ } else if (BOOLEAN_META_KEYS.has(key)) {
16313
+ setClauses.push(`${column} = ?`);
16314
+ values.push(value ? 1 : 0);
16315
+ } else if (typeof value === "string" || typeof value === "number") {
16316
+ setClauses.push(`${column} = ?`);
16317
+ values.push(value);
16318
+ }
16319
+ }
16320
+ if (setClauses.length === 0) {
16235
16321
  return;
16236
16322
  }
16323
+ db.transaction(() => {
16324
+ ensureSessionMetaRow(db, sessionId);
16325
+ db.prepare(`UPDATE session_meta SET ${setClauses.join(", ")} WHERE session_id = ?`).run(...values, sessionId);
16326
+ })();
16237
16327
  }
16238
- function directoryFallback(directory) {
16239
- const canonical = path3.resolve(directory);
16240
- const hash2 = Bun.hash(canonical).toString(16).slice(0, 12);
16241
- return `dir:${hash2}`;
16328
+ function clearSession(db, sessionId) {
16329
+ db.transaction(() => {
16330
+ db.prepare("DELETE FROM pending_ops WHERE session_id = ?").run(sessionId);
16331
+ db.prepare("DELETE FROM source_contents WHERE session_id = ?").run(sessionId);
16332
+ db.prepare("DELETE FROM tags WHERE session_id = ?").run(sessionId);
16333
+ db.prepare("DELETE FROM session_meta WHERE session_id = ?").run(sessionId);
16334
+ db.prepare("DELETE FROM compartments WHERE session_id = ?").run(sessionId);
16335
+ db.prepare("DELETE FROM session_facts WHERE session_id = ?").run(sessionId);
16336
+ db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
16337
+ db.prepare("DELETE FROM recomp_compartments WHERE session_id = ?").run(sessionId);
16338
+ db.prepare("DELETE FROM recomp_facts WHERE session_id = ?").run(sessionId);
16339
+ clearIndexedMessages(db, sessionId);
16340
+ })();
16242
16341
  }
16243
- function resolveProjectIdentity(directory) {
16244
- const resolved = path3.resolve(directory);
16245
- const cached2 = resolvedCache.get(resolved);
16246
- if (cached2 !== undefined) {
16247
- return cached2;
16248
- }
16249
- const rootHash = getRootCommitHash(resolved);
16250
- const identity = rootHash ? `git:${rootHash}` : directoryFallback(resolved);
16251
- resolvedCache.set(resolved, identity);
16252
- return identity;
16342
+ // src/features/magic-context/storage-notes.ts
16343
+ function isSessionNoteRow(row) {
16344
+ if (row === null || typeof row !== "object")
16345
+ return false;
16346
+ const candidate = row;
16347
+ return typeof candidate.id === "number" && typeof candidate.session_id === "string" && typeof candidate.content === "string" && typeof candidate.created_at === "number";
16253
16348
  }
16254
-
16255
- // src/hooks/magic-context/execute-flush.ts
16256
- function executeFlush(db, sessionId) {
16257
- try {
16258
- const pendingOps = getPendingOps(db, sessionId);
16259
- if (pendingOps.length === 0) {
16260
- return "No pending operations to flush.";
16261
- }
16262
- let dropped = 0;
16263
- db.transaction(() => {
16264
- for (const op of pendingOps) {
16265
- updateTagStatus(db, sessionId, op.tagId, "dropped");
16266
- removePendingOp(db, sessionId, op.tagId);
16267
- dropped++;
16268
- }
16269
- })();
16270
- const parts = [];
16271
- if (dropped > 0)
16272
- parts.push(`${dropped} dropped`);
16273
- if (dropped > 0) {
16274
- clearPersistedStickyTurnReminder(db, sessionId);
16275
- }
16276
- return `Flushed: ${parts.join(", ")}. Changes take effect on next message.`;
16277
- } catch (error48) {
16278
- sessionLog(sessionId, "ctx-flush failed:", error48);
16279
- return `Error: Failed to flush context operations. ${getErrorMessage(error48)}`;
16280
- }
16349
+ function toSessionNote(row) {
16350
+ return {
16351
+ id: row.id,
16352
+ sessionId: row.session_id,
16353
+ content: row.content,
16354
+ createdAt: row.created_at
16355
+ };
16281
16356
  }
16282
-
16283
- // src/shared/internal-initiator-marker.ts
16284
- var OMO_INTERNAL_INITIATOR_MARKER = "<!-- OMO_INTERNAL_INITIATOR -->";
16285
-
16286
- // src/shared/system-directive.ts
16287
- var SYSTEM_DIRECTIVE_PREFIX = "[SYSTEM DIRECTIVE: MAGIC-CONTEXT";
16288
- function isSystemDirective(text) {
16289
- return text.trimStart().startsWith(SYSTEM_DIRECTIVE_PREFIX);
16357
+ function getSessionNotes(db, sessionId) {
16358
+ const rows = db.prepare("SELECT * FROM session_notes WHERE session_id = ? ORDER BY id ASC").all(sessionId).filter(isSessionNoteRow);
16359
+ return rows.map(toSessionNote);
16290
16360
  }
16291
- function removeSystemReminders(text) {
16292
- return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, "").trim();
16361
+ function addSessionNote(db, sessionId, content) {
16362
+ db.prepare("INSERT INTO session_notes (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, Date.now());
16293
16363
  }
16294
-
16295
- // src/hooks/magic-context/read-session-db.ts
16296
- import { Database as Database2 } from "bun:sqlite";
16297
- import { join as join6 } from "path";
16298
- function getOpenCodeDbPath() {
16299
- return join6(getDataDir(), "opencode", "opencode.db");
16364
+ function clearSessionNotes(db, sessionId) {
16365
+ db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
16300
16366
  }
16301
- var cachedReadOnlyDb = null;
16302
- function closeCachedReadOnlyDb() {
16303
- if (!cachedReadOnlyDb) {
16304
- return;
16305
- }
16306
- try {
16307
- cachedReadOnlyDb.db.close(false);
16308
- } catch (error48) {
16309
- log("[magic-context] failed to close cached OpenCode read-only DB:", error48);
16310
- } finally {
16311
- cachedReadOnlyDb = null;
16367
+ // src/features/magic-context/storage-ops.ts
16368
+ var queuePendingOpStatements = new WeakMap;
16369
+ var getPendingOpsStatements = new WeakMap;
16370
+ var clearPendingOpsStatements = new WeakMap;
16371
+ var removePendingOpStatements = new WeakMap;
16372
+ function getQueuePendingOpStatement(db) {
16373
+ let stmt = queuePendingOpStatements.get(db);
16374
+ if (!stmt) {
16375
+ stmt = db.prepare("INSERT INTO pending_ops (session_id, tag_id, operation, queued_at) VALUES (?, ?, ?, ?)");
16376
+ queuePendingOpStatements.set(db, stmt);
16312
16377
  }
16378
+ return stmt;
16313
16379
  }
16314
- function getReadOnlySessionDb() {
16315
- const dbPath = getOpenCodeDbPath();
16316
- if (cachedReadOnlyDb?.path === dbPath) {
16317
- return cachedReadOnlyDb.db;
16380
+ function getPendingOpsStatement(db) {
16381
+ let stmt = getPendingOpsStatements.get(db);
16382
+ if (!stmt) {
16383
+ stmt = db.prepare("SELECT id, session_id, tag_id, operation, queued_at FROM pending_ops WHERE session_id = ? ORDER BY queued_at ASC, id ASC");
16384
+ getPendingOpsStatements.set(db, stmt);
16318
16385
  }
16319
- closeCachedReadOnlyDb();
16320
- const db = new Database2(dbPath, { readonly: true });
16321
- cachedReadOnlyDb = { path: dbPath, db };
16322
- return db;
16386
+ return stmt;
16323
16387
  }
16324
- function withReadOnlySessionDb(fn) {
16325
- return fn(getReadOnlySessionDb());
16388
+ function getRemovePendingOpStatement(db) {
16389
+ let stmt = removePendingOpStatements.get(db);
16390
+ if (!stmt) {
16391
+ stmt = db.prepare("DELETE FROM pending_ops WHERE session_id = ? AND tag_id = ?");
16392
+ removePendingOpStatements.set(db, stmt);
16393
+ }
16394
+ return stmt;
16326
16395
  }
16327
- function getRawSessionMessageCountFromDb(db, sessionId) {
16328
- const row = db.prepare("SELECT COUNT(*) as count FROM message WHERE session_id = ?").get(sessionId);
16329
- return typeof row?.count === "number" ? row.count : 0;
16396
+ function isPendingOpRow(row) {
16397
+ if (row === null || typeof row !== "object")
16398
+ return false;
16399
+ const r = row;
16400
+ return typeof r.id === "number" && typeof r.session_id === "string" && typeof r.tag_id === "number" && typeof r.operation === "string" && typeof r.queued_at === "number";
16330
16401
  }
16331
-
16332
- // src/hooks/magic-context/read-session-formatting.ts
16333
- var COMMIT_HASH_PATTERN = /`?\b([0-9a-f]{6,12})\b`?/gi;
16334
- var COMMIT_HINT_PATTERN = /\b(commit(?:ted)?|cherry-?pick(?:ed)?|hash(?:es)?|sha)\b/i;
16335
- var MAX_COMMITS_PER_BLOCK = 5;
16336
- function hasMeaningfulUserText(parts) {
16337
- for (const part of parts) {
16338
- if (part === null || typeof part !== "object")
16339
- continue;
16340
- const candidate = part;
16341
- if (candidate.type !== "text" || typeof candidate.text !== "string")
16342
- continue;
16343
- if (candidate.ignored === true)
16344
- continue;
16345
- const cleaned = removeSystemReminders(candidate.text).replace(OMO_INTERNAL_INITIATOR_MARKER, "").trim();
16346
- if (!cleaned)
16347
- continue;
16348
- if (isSystemDirective(cleaned))
16349
- continue;
16350
- return true;
16402
+ function toPendingOp(row) {
16403
+ if (row.operation !== "drop") {
16404
+ sessionLog(row.session_id, `unknown pending operation "${row.operation}"; ignoring`);
16405
+ return null;
16351
16406
  }
16352
- return false;
16407
+ return {
16408
+ id: row.id,
16409
+ sessionId: row.session_id,
16410
+ tagId: row.tag_id,
16411
+ operation: row.operation,
16412
+ queuedAt: row.queued_at
16413
+ };
16414
+ }
16415
+ function queuePendingOp(db, sessionId, tagId, operation, queuedAt = Date.now()) {
16416
+ getQueuePendingOpStatement(db).run(sessionId, tagId, operation, queuedAt);
16417
+ }
16418
+ function getPendingOps(db, sessionId) {
16419
+ const rows = getPendingOpsStatement(db).all(sessionId).filter(isPendingOpRow);
16420
+ return rows.map(toPendingOp).filter((op) => op !== null);
16353
16421
  }
16354
- function extractTexts(parts) {
16355
- const texts = [];
16356
- for (const part of parts) {
16357
- if (part === null || typeof part !== "object")
16358
- continue;
16359
- const p = part;
16360
- if (p.type === "text" && typeof p.text === "string" && p.text.trim().length > 0) {
16361
- texts.push(p.text.trim());
16362
- }
16363
- }
16364
- return texts;
16422
+ function removePendingOp(db, sessionId, tagId) {
16423
+ getRemovePendingOpStatement(db).run(sessionId, tagId);
16365
16424
  }
16366
- function estimateTokens(text) {
16367
- return Math.ceil(text.length / 3.5);
16425
+ // src/features/magic-context/storage-source.ts
16426
+ function isSourceContentRow(row) {
16427
+ if (row === null || typeof row !== "object")
16428
+ return false;
16429
+ const r = row;
16430
+ return typeof r.tag_id === "number" && typeof r.content === "string";
16368
16431
  }
16369
- function normalizeText(text) {
16370
- return text.replace(/\s+/g, " ").trim();
16432
+ function saveSourceContent(db, sessionId, tagId, content) {
16433
+ db.prepare("INSERT OR IGNORE INTO source_contents (tag_id, session_id, content, created_at) VALUES (?, ?, ?, ?)").run(tagId, sessionId, content, Date.now());
16371
16434
  }
16372
- function compactRole(role) {
16373
- if (role === "assistant")
16374
- return "A";
16375
- if (role === "user")
16376
- return "U";
16377
- return role.slice(0, 1).toUpperCase() || "M";
16435
+ function replaceSourceContent(db, sessionId, tagId, content) {
16436
+ db.prepare(`INSERT INTO source_contents (tag_id, session_id, content, created_at)
16437
+ VALUES (?, ?, ?, ?)
16438
+ ON CONFLICT(session_id, tag_id)
16439
+ DO UPDATE SET content = excluded.content, created_at = excluded.created_at`).run(tagId, sessionId, content, Date.now());
16378
16440
  }
16379
- function formatBlock(block) {
16380
- const range = block.startOrdinal === block.endOrdinal ? `[${block.startOrdinal}]` : `[${block.startOrdinal}-${block.endOrdinal}]`;
16381
- const commitSuffix = block.commitHashes.length > 0 ? ` commits: ${block.commitHashes.join(", ")}` : "";
16382
- return `${range} ${block.role}:${commitSuffix} ${block.parts.join(" / ")}`;
16441
+ function getSourceContents(db, sessionId, tagIds) {
16442
+ if (tagIds.length === 0) {
16443
+ return new Map;
16444
+ }
16445
+ const placeholders = tagIds.map(() => "?").join(", ");
16446
+ const rows = db.prepare(`SELECT tag_id, content FROM source_contents WHERE session_id = ? AND tag_id IN (${placeholders})`).all(sessionId, ...tagIds).filter(isSourceContentRow);
16447
+ const sources = new Map;
16448
+ for (const row of rows) {
16449
+ sources.set(row.tag_id, row.content);
16450
+ }
16451
+ return sources;
16383
16452
  }
16384
- function extractCommitHashes(text) {
16385
- const hashes = [];
16386
- const seen = new Set;
16387
- for (const match of text.matchAll(COMMIT_HASH_PATTERN)) {
16388
- const hash2 = match[1]?.toLowerCase();
16389
- if (!hash2 || seen.has(hash2))
16390
- continue;
16391
- seen.add(hash2);
16392
- hashes.push(hash2);
16393
- if (hashes.length >= MAX_COMMITS_PER_BLOCK)
16394
- break;
16453
+ // src/features/magic-context/storage-tags.ts
16454
+ var insertTagStatements = new WeakMap;
16455
+ var updateTagStatusStatements = new WeakMap;
16456
+ var updateTagMessageIdStatements = new WeakMap;
16457
+ function getInsertTagStatement(db) {
16458
+ let stmt = insertTagStatements.get(db);
16459
+ if (!stmt) {
16460
+ stmt = db.prepare("INSERT INTO tags (session_id, message_id, type, byte_size, tag_number) VALUES (?, ?, ?, ?, ?)");
16461
+ insertTagStatements.set(db, stmt);
16395
16462
  }
16396
- return hashes;
16463
+ return stmt;
16397
16464
  }
16398
- function compactTextForSummary(text, role) {
16399
- const commitHashes = role === "assistant" ? extractCommitHashes(text) : [];
16400
- if (commitHashes.length === 0 || !COMMIT_HINT_PATTERN.test(text)) {
16401
- return { text, commitHashes };
16465
+ function getUpdateTagStatusStatement(db) {
16466
+ let stmt = updateTagStatusStatements.get(db);
16467
+ if (!stmt) {
16468
+ stmt = db.prepare("UPDATE tags SET status = ? WHERE session_id = ? AND tag_number = ?");
16469
+ updateTagStatusStatements.set(db, stmt);
16402
16470
  }
16403
- 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();
16404
- return {
16405
- text: withoutHashes.length > 0 ? withoutHashes : text,
16406
- commitHashes
16407
- };
16471
+ return stmt;
16408
16472
  }
16409
- function mergeCommitHashes(existing, next) {
16410
- if (next.length === 0)
16411
- return existing;
16412
- const merged = [...existing];
16413
- for (const hash2 of next) {
16414
- if (merged.includes(hash2))
16415
- continue;
16416
- merged.push(hash2);
16417
- if (merged.length >= MAX_COMMITS_PER_BLOCK)
16418
- break;
16473
+ function getUpdateTagMessageIdStatement(db) {
16474
+ let stmt = updateTagMessageIdStatements.get(db);
16475
+ if (!stmt) {
16476
+ stmt = db.prepare("UPDATE tags SET message_id = ? WHERE session_id = ? AND tag_number = ?");
16477
+ updateTagMessageIdStatements.set(db, stmt);
16419
16478
  }
16420
- return merged;
16479
+ return stmt;
16421
16480
  }
16422
-
16423
- // src/hooks/magic-context/read-session-raw.ts
16424
- function isRawMessageRow(row) {
16481
+ function isTagRow(row) {
16425
16482
  if (row === null || typeof row !== "object")
16426
16483
  return false;
16427
- const candidate = row;
16428
- return typeof candidate.id === "string" && typeof candidate.data === "string";
16484
+ const r = row;
16485
+ return typeof r.id === "number" && typeof r.message_id === "string" && typeof r.type === "string" && typeof r.status === "string" && typeof r.byte_size === "number" && typeof r.session_id === "string" && typeof r.tag_number === "number";
16429
16486
  }
16430
- function isRawPartRow(row) {
16431
- if (row === null || typeof row !== "object")
16432
- return false;
16433
- const candidate = row;
16434
- return typeof candidate.message_id === "string" && typeof candidate.data === "string";
16487
+ function toTagEntry(row) {
16488
+ const type = row.type === "tool" ? "tool" : row.type === "file" ? "file" : "message";
16489
+ const status = row.status === "dropped" || row.status === "compacted" ? row.status : "active";
16490
+ return {
16491
+ tagNumber: row.tag_number,
16492
+ messageId: row.message_id,
16493
+ type,
16494
+ status,
16495
+ byteSize: row.byte_size,
16496
+ sessionId: row.session_id
16497
+ };
16435
16498
  }
16436
- function parseJsonRecord(value) {
16437
- const parsed = JSON.parse(value);
16438
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
16439
- throw new Error("Expected JSON object");
16440
- }
16441
- return parsed;
16499
+ function insertTag(db, sessionId, messageId, type, byteSize2, tagNumber) {
16500
+ getInsertTagStatement(db).run(sessionId, messageId, type, byteSize2, tagNumber);
16501
+ return tagNumber;
16442
16502
  }
16443
- function parseJsonUnknown(value) {
16444
- return JSON.parse(value);
16503
+ function updateTagStatus(db, sessionId, tagId, status) {
16504
+ getUpdateTagStatusStatement(db).run(status, sessionId, tagId);
16445
16505
  }
16446
- function readRawSessionMessagesFromDb(db, sessionId) {
16447
- const messageRows = db.prepare("SELECT id, data FROM message WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId).filter(isRawMessageRow);
16448
- const partRows = db.prepare("SELECT message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId).filter(isRawPartRow);
16449
- const partsByMessageId = new Map;
16450
- for (const part of partRows) {
16451
- const list = partsByMessageId.get(part.message_id) ?? [];
16452
- list.push(parseJsonUnknown(part.data));
16453
- partsByMessageId.set(part.message_id, list);
16454
- }
16455
- return messageRows.map((row, index) => {
16456
- const info = parseJsonRecord(row.data);
16457
- const role = typeof info.role === "string" ? info.role : "unknown";
16458
- return {
16459
- ordinal: index + 1,
16460
- id: row.id,
16461
- role,
16462
- parts: partsByMessageId.get(row.id) ?? []
16463
- };
16464
- });
16506
+ function updateTagMessageId(db, sessionId, tagId, messageId) {
16507
+ getUpdateTagMessageIdStatement(db).run(messageId, sessionId, tagId);
16465
16508
  }
16466
-
16467
- // src/hooks/magic-context/tag-content-primitives.ts
16468
- var encoder = new TextEncoder;
16469
- var TAG_PREFIX_REGEX = /^\u00A7\d+\u00A7\s*/;
16470
- function byteSize(value) {
16471
- return encoder.encode(value).length;
16509
+ function getTagsBySession(db, sessionId) {
16510
+ 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);
16511
+ return rows.map(toTagEntry);
16472
16512
  }
16473
- function stripTagPrefix(value) {
16474
- return value.replace(TAG_PREFIX_REGEX, "");
16513
+ function getTopNBySize(db, sessionId, n) {
16514
+ if (n <= 0) {
16515
+ return [];
16516
+ }
16517
+ 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);
16518
+ return rows.map(toTagEntry);
16475
16519
  }
16476
- function prependTag(tagId, value) {
16477
- const stripped = stripTagPrefix(value);
16478
- return `\xA7${tagId}\xA7 ${stripped}`;
16520
+ // src/plugin/dream-timer.ts
16521
+ var DREAM_TIMER_INTERVAL_MS = 15 * 60 * 1000;
16522
+ function startDreamScheduleTimer(args) {
16523
+ const { client, dreamerConfig } = args;
16524
+ if (!dreamerConfig.enabled || !dreamerConfig.schedule?.trim()) {
16525
+ return;
16526
+ }
16527
+ const timer = setInterval(() => {
16528
+ try {
16529
+ const db = openDatabase();
16530
+ checkScheduleAndEnqueue(db, dreamerConfig.schedule);
16531
+ processDreamQueue({
16532
+ db,
16533
+ client,
16534
+ tasks: dreamerConfig.tasks,
16535
+ taskTimeoutMinutes: dreamerConfig.task_timeout_minutes,
16536
+ maxRuntimeMinutes: dreamerConfig.max_runtime_minutes
16537
+ }).catch((error48) => {
16538
+ log("[dreamer] timer-triggered queue processing failed:", error48);
16539
+ });
16540
+ } catch (error48) {
16541
+ log("[dreamer] timer-triggered schedule check failed:", error48);
16542
+ }
16543
+ }, DREAM_TIMER_INTERVAL_MS);
16544
+ if (typeof timer === "object" && "unref" in timer) {
16545
+ timer.unref();
16546
+ }
16547
+ log(`[dreamer] started independent schedule timer (every ${DREAM_TIMER_INTERVAL_MS / 60000}m)`);
16548
+ return () => {
16549
+ clearInterval(timer);
16550
+ log("[dreamer] stopped dream schedule timer");
16551
+ };
16479
16552
  }
16480
- function isThinkingPart(part) {
16481
- if (part === null || typeof part !== "object")
16482
- return false;
16483
- const candidate = part;
16484
- return candidate.type === "thinking" || candidate.type === "reasoning";
16553
+
16554
+ // src/plugin/event.ts
16555
+ function createEventHandler(args) {
16556
+ return async (input) => {
16557
+ await args.magicContext?.event?.(input);
16558
+ };
16485
16559
  }
16486
16560
 
16487
- // src/hooks/magic-context/tag-part-guards.ts
16488
- function isTextPart(part) {
16489
- if (part === null || typeof part !== "object")
16490
- return false;
16491
- const p = part;
16492
- return p.type === "text" && typeof p.text === "string";
16561
+ // src/features/magic-context/compaction.ts
16562
+ function createCompactionHandler() {
16563
+ return {
16564
+ onCompacted(sessionId, db) {
16565
+ db.transaction(() => {
16566
+ db.prepare("UPDATE tags SET status = 'compacted' WHERE session_id = ? AND status IN ('active', 'dropped')").run(sessionId);
16567
+ db.prepare("DELETE FROM pending_ops WHERE session_id = ?").run(sessionId);
16568
+ updateSessionMeta(db, sessionId, { lastNudgeBand: null });
16569
+ })();
16570
+ }
16571
+ };
16493
16572
  }
16494
- function isToolPartWithOutput(part) {
16495
- if (part === null || typeof part !== "object")
16496
- return false;
16497
- const p = part;
16498
- if (p.type !== "tool" || typeof p.callID !== "string")
16499
- return false;
16500
- if (p.state === null || typeof p.state !== "object")
16501
- return false;
16502
- return typeof p.state.output === "string";
16573
+
16574
+ // src/hooks/is-anthropic-provider.ts
16575
+ function isAnthropicProvider(providerID) {
16576
+ return providerID === "anthropic" || providerID === "google-vertex-anthropic";
16503
16577
  }
16504
- function isFilePart(part) {
16505
- if (part === null || typeof part !== "object")
16506
- return false;
16507
- const p = part;
16508
- return p.type === "file" && typeof p.url === "string";
16578
+
16579
+ // src/hooks/magic-context/event-resolvers.ts
16580
+ var DEFAULT_CONTEXT_LIMIT = 200000;
16581
+ function resolveContextLimit(providerID, modelID, config2) {
16582
+ if (!providerID) {
16583
+ return DEFAULT_CONTEXT_LIMIT;
16584
+ }
16585
+ if (modelID) {
16586
+ const modelSpecific = config2.modelContextLimitsCache?.get(`${providerID}/${modelID}`);
16587
+ if (typeof modelSpecific === "number" && modelSpecific > 0) {
16588
+ return modelSpecific;
16589
+ }
16590
+ }
16591
+ if (isAnthropicProvider(providerID)) {
16592
+ return 1e6;
16593
+ }
16594
+ return DEFAULT_CONTEXT_LIMIT;
16595
+ }
16596
+ function resolveCacheTtl(cacheTtl, modelKey) {
16597
+ if (typeof cacheTtl === "string") {
16598
+ return cacheTtl;
16599
+ }
16600
+ if (modelKey && typeof cacheTtl[modelKey] === "string") {
16601
+ return cacheTtl[modelKey];
16602
+ }
16603
+ if (modelKey) {
16604
+ const bareModelId = modelKey.split("/").slice(1).join("/");
16605
+ if (bareModelId && typeof cacheTtl[bareModelId] === "string") {
16606
+ return cacheTtl[bareModelId];
16607
+ }
16608
+ }
16609
+ return cacheTtl.default ?? "5m";
16509
16610
  }
16510
- function buildFileSourceContent(parts) {
16511
- const content = parts.filter(isTextPart).map((part) => stripTagPrefix(part.text)).join(`
16512
- `).trim();
16513
- return content.length > 0 ? content : null;
16611
+ function resolveExecuteThreshold(config2, modelKey, fallback) {
16612
+ if (typeof config2 === "number") {
16613
+ return config2;
16614
+ }
16615
+ if (modelKey && typeof config2[modelKey] === "number") {
16616
+ return config2[modelKey];
16617
+ }
16618
+ if (modelKey) {
16619
+ const bareModelId = modelKey.split("/").slice(1).join("/");
16620
+ if (bareModelId && typeof config2[bareModelId] === "number") {
16621
+ return config2[bareModelId];
16622
+ }
16623
+ }
16624
+ return config2.default ?? fallback;
16514
16625
  }
16515
-
16516
- // src/hooks/magic-context/read-session-chunk.ts
16517
- function cleanUserText(text) {
16518
- return removeSystemReminders(text).replace(OMO_INTERNAL_INITIATOR_MARKER, "").trim();
16626
+ function resolveModelKey(providerID, modelID) {
16627
+ if (!providerID || !modelID) {
16628
+ return;
16629
+ }
16630
+ return `${providerID}/${modelID}`;
16519
16631
  }
16520
- function readRawSessionMessages(sessionId) {
16521
- return withReadOnlySessionDb((db) => readRawSessionMessagesFromDb(db, sessionId));
16632
+ function resolveSessionId(properties) {
16633
+ if (typeof properties?.sessionID === "string") {
16634
+ return properties.sessionID;
16635
+ }
16636
+ const info = properties?.info;
16637
+ if (info === null || typeof info !== "object") {
16638
+ return;
16639
+ }
16640
+ const record2 = info;
16641
+ if (typeof record2.sessionID === "string") {
16642
+ return record2.sessionID;
16643
+ }
16644
+ if (typeof record2.id === "string") {
16645
+ return record2.id;
16646
+ }
16647
+ return;
16522
16648
  }
16523
- function getRawSessionMessageCount(sessionId) {
16524
- return withReadOnlySessionDb((db) => getRawSessionMessageCountFromDb(db, sessionId));
16649
+
16650
+ // src/features/magic-context/scheduler.ts
16651
+ var TTL_PATTERN = /^(\d+)([smh])$/;
16652
+ var NUMERIC_PATTERN = /^\d+$/;
16653
+ var UNIT_TO_MS = {
16654
+ s: 1000,
16655
+ m: 60 * 1000,
16656
+ h: 60 * 60 * 1000
16657
+ };
16658
+ function parseCacheTtl(ttl) {
16659
+ const normalizedTtl = ttl.trim();
16660
+ if (NUMERIC_PATTERN.test(normalizedTtl)) {
16661
+ return Number(normalizedTtl);
16662
+ }
16663
+ const match = normalizedTtl.match(TTL_PATTERN);
16664
+ if (!match) {
16665
+ throw new Error(`Invalid cache TTL format: ${ttl}`);
16666
+ }
16667
+ const value = Number(match[1]);
16668
+ const unit = match[2];
16669
+ return value * UNIT_TO_MS[unit];
16525
16670
  }
16526
- function getRawSessionTagKeysThrough(sessionId, upToMessageIndex) {
16527
- return withReadOnlySessionDb((db) => {
16528
- const messages = readRawSessionMessagesFromDb(db, sessionId);
16529
- const keys = [];
16530
- for (const message of messages) {
16531
- if (message.ordinal > upToMessageIndex)
16532
- break;
16533
- for (const [partIndex, part] of message.parts.entries()) {
16534
- if (isTextPart(part)) {
16535
- keys.push(`${message.id}:p${partIndex}`);
16536
- }
16537
- if (isFilePart(part)) {
16538
- keys.push(`${message.id}:file${partIndex}`);
16539
- }
16540
- if (isToolPartWithOutput(part)) {
16541
- keys.push(part.callID);
16671
+ function createScheduler(config2) {
16672
+ return {
16673
+ shouldExecute(sessionMeta, contextUsage, currentTime = Date.now(), sessionId, modelKey) {
16674
+ const threshold = resolveExecuteThreshold(config2.executeThresholdPercentage, modelKey, 65);
16675
+ if (contextUsage.percentage >= threshold) {
16676
+ return "execute";
16677
+ }
16678
+ let ttlMs;
16679
+ try {
16680
+ ttlMs = parseCacheTtl(sessionMeta.cacheTtl);
16681
+ } catch (error48) {
16682
+ if (sessionId) {
16683
+ sessionLog(sessionId, `invalid cache_ttl "${sessionMeta.cacheTtl}"; falling back to default 5m`, error48);
16684
+ } else {
16685
+ log(`[magic-context] invalid cache_ttl "${sessionMeta.cacheTtl}"; falling back to default 5m`, error48);
16542
16686
  }
16687
+ ttlMs = parseCacheTtl("5m");
16688
+ }
16689
+ const elapsedTime = currentTime - sessionMeta.lastResponseTime;
16690
+ if (elapsedTime > ttlMs) {
16691
+ return "execute";
16543
16692
  }
16693
+ return "defer";
16544
16694
  }
16545
- return keys;
16546
- });
16695
+ };
16547
16696
  }
16548
- var PROTECTED_TAIL_USER_TURNS = 5;
16549
- function getProtectedTailStartOrdinal(sessionId) {
16550
- return withReadOnlySessionDb((db) => {
16551
- const messages = readRawSessionMessagesFromDb(db, sessionId);
16552
- const userOrdinals = messages.filter((m) => m.role === "user" && hasMeaningfulUserText(m.parts)).map((m) => m.ordinal);
16553
- if (userOrdinals.length < PROTECTED_TAIL_USER_TURNS) {
16554
- return 1;
16555
- }
16556
- return userOrdinals[userOrdinals.length - PROTECTED_TAIL_USER_TURNS];
16557
- });
16697
+
16698
+ // src/features/magic-context/tagger.ts
16699
+ var GET_COUNTER_SQL = `SELECT counter FROM session_meta WHERE session_id = ?`;
16700
+ var GET_ASSIGNMENTS_SQL = "SELECT message_id, tag_number FROM tags WHERE session_id = ? ORDER BY tag_number ASC";
16701
+ function isAssignmentRow(row) {
16702
+ if (row === null || typeof row !== "object") {
16703
+ return false;
16704
+ }
16705
+ const candidate = row;
16706
+ return typeof candidate.message_id === "string" && typeof candidate.tag_number === "number";
16558
16707
  }
16559
- function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal) {
16560
- const messages = readRawSessionMessages(sessionId);
16561
- const startOrdinal = Math.max(1, offset);
16562
- const lines = [];
16563
- const lineMeta = [];
16564
- let totalTokens = 0;
16565
- let messagesProcessed = 0;
16566
- let lastOrdinal = startOrdinal - 1;
16567
- let lastMessageId = "";
16568
- let firstMessageId = "";
16569
- let currentBlock = null;
16570
- let pendingNoiseMeta = [];
16571
- let commitClusters = 0;
16572
- let lastFlushedRole = "";
16573
- function flushCurrentBlock() {
16574
- if (!currentBlock)
16575
- return true;
16576
- const blockText = formatBlock(currentBlock);
16577
- const blockTokens = estimateTokens(blockText);
16578
- if (totalTokens + blockTokens > tokenBudget && totalTokens > 0) {
16579
- return false;
16708
+ var UPSERT_COUNTER_SQL = `
16709
+ INSERT INTO session_meta (session_id, counter)
16710
+ VALUES (?, ?)
16711
+ ON CONFLICT(session_id) DO UPDATE SET counter = excluded.counter
16712
+ `;
16713
+ var upsertCounterStatements = new WeakMap;
16714
+ function getUpsertCounterStatement(db) {
16715
+ let stmt = upsertCounterStatements.get(db);
16716
+ if (!stmt) {
16717
+ stmt = db.prepare(UPSERT_COUNTER_SQL);
16718
+ upsertCounterStatements.set(db, stmt);
16719
+ }
16720
+ return stmt;
16721
+ }
16722
+ function createTagger() {
16723
+ const counters = new Map;
16724
+ const assignments = new Map;
16725
+ function getSessionAssignments(sessionId) {
16726
+ let map2 = assignments.get(sessionId);
16727
+ if (!map2) {
16728
+ map2 = new Map;
16729
+ assignments.set(sessionId, map2);
16580
16730
  }
16581
- if (currentBlock.role === "A" && currentBlock.commitHashes.length > 0 && lastFlushedRole !== "A") {
16582
- commitClusters++;
16731
+ return map2;
16732
+ }
16733
+ function assignTag(sessionId, messageId, type, byteSize2, db) {
16734
+ const sessionAssignments = getSessionAssignments(sessionId);
16735
+ const existing = sessionAssignments.get(messageId);
16736
+ if (existing !== undefined) {
16737
+ return existing;
16583
16738
  }
16584
- lastFlushedRole = currentBlock.role;
16585
- if (!firstMessageId)
16586
- firstMessageId = currentBlock.meta[0]?.messageId ?? "";
16587
- lastOrdinal = currentBlock.meta[currentBlock.meta.length - 1]?.ordinal ?? currentBlock.endOrdinal;
16588
- lastMessageId = currentBlock.meta[currentBlock.meta.length - 1]?.messageId ?? "";
16589
- messagesProcessed += currentBlock.meta.length;
16590
- lines.push(blockText);
16591
- lineMeta.push(...currentBlock.meta);
16592
- totalTokens += blockTokens;
16593
- currentBlock = null;
16594
- return true;
16739
+ const current = counters.get(sessionId) ?? 0;
16740
+ const next = current + 1;
16741
+ db.transaction(() => {
16742
+ insertTag(db, sessionId, messageId, type, byteSize2, next);
16743
+ getUpsertCounterStatement(db).run(sessionId, next);
16744
+ })();
16745
+ counters.set(sessionId, next);
16746
+ sessionAssignments.set(messageId, next);
16747
+ return next;
16748
+ }
16749
+ function getTag(sessionId, messageId) {
16750
+ return assignments.get(sessionId)?.get(messageId);
16751
+ }
16752
+ function bindTag(sessionId, messageId, tagNumber) {
16753
+ getSessionAssignments(sessionId).set(messageId, tagNumber);
16754
+ }
16755
+ function getAssignments(sessionId) {
16756
+ return getSessionAssignments(sessionId);
16757
+ }
16758
+ function resetCounter(sessionId, db) {
16759
+ counters.set(sessionId, 0);
16760
+ assignments.delete(sessionId);
16761
+ getUpsertCounterStatement(db).run(sessionId, 0);
16762
+ }
16763
+ function getCounter(sessionId) {
16764
+ return counters.get(sessionId) ?? 0;
16765
+ }
16766
+ function initFromDb(sessionId, db) {
16767
+ if (counters.has(sessionId)) {
16768
+ return;
16769
+ }
16770
+ const row = db.prepare(GET_COUNTER_SQL).get(sessionId);
16771
+ const assignmentRows = db.prepare(GET_ASSIGNMENTS_SQL).all(sessionId).filter(isAssignmentRow);
16772
+ const sessionAssignments = getSessionAssignments(sessionId);
16773
+ sessionAssignments.clear();
16774
+ let maxTagNumber = 0;
16775
+ for (const assignment of assignmentRows) {
16776
+ sessionAssignments.set(assignment.message_id, assignment.tag_number);
16777
+ if (assignment.tag_number > maxTagNumber) {
16778
+ maxTagNumber = assignment.tag_number;
16779
+ }
16780
+ }
16781
+ const counter = Math.max(row?.counter ?? 0, maxTagNumber);
16782
+ counters.set(sessionId, counter);
16783
+ }
16784
+ function cleanup(sessionId) {
16785
+ counters.delete(sessionId);
16786
+ assignments.delete(sessionId);
16787
+ }
16788
+ return {
16789
+ assignTag,
16790
+ getTag,
16791
+ bindTag,
16792
+ getAssignments,
16793
+ resetCounter,
16794
+ getCounter,
16795
+ initFromDb,
16796
+ cleanup
16797
+ };
16798
+ }
16799
+
16800
+ // src/features/magic-context/memory/project-identity.ts
16801
+ import { execSync } from "child_process";
16802
+ import path3 from "path";
16803
+ var GIT_TIMEOUT_MS = 5000;
16804
+ var resolvedCache = new Map;
16805
+ function getRootCommitHash(directory) {
16806
+ try {
16807
+ const hash2 = execSync("git rev-list --max-parents=0 HEAD", {
16808
+ cwd: directory,
16809
+ encoding: "utf-8",
16810
+ stdio: ["pipe", "pipe", "pipe"],
16811
+ timeout: GIT_TIMEOUT_MS
16812
+ }).trim();
16813
+ const firstLine = hash2.split(`
16814
+ `)[0]?.trim();
16815
+ return firstLine && firstLine.length >= 7 ? firstLine : undefined;
16816
+ } catch {
16817
+ return;
16818
+ }
16819
+ }
16820
+ function directoryFallback(directory) {
16821
+ const canonical = path3.resolve(directory);
16822
+ const hash2 = Bun.hash(canonical).toString(16).slice(0, 12);
16823
+ return `dir:${hash2}`;
16824
+ }
16825
+ function resolveProjectIdentity(directory) {
16826
+ const resolved = path3.resolve(directory);
16827
+ const cached2 = resolvedCache.get(resolved);
16828
+ if (cached2 !== undefined) {
16829
+ return cached2;
16595
16830
  }
16596
- for (const msg of messages) {
16597
- if (eligibleEndOrdinal !== undefined && msg.ordinal >= eligibleEndOrdinal)
16598
- break;
16599
- if (msg.ordinal < startOrdinal)
16600
- continue;
16601
- const meta3 = { ordinal: msg.ordinal, messageId: msg.id };
16602
- if (msg.role === "user" && !hasMeaningfulUserText(msg.parts)) {
16603
- pendingNoiseMeta.push(meta3);
16604
- continue;
16605
- }
16606
- const role = compactRole(msg.role);
16607
- const compacted = compactTextForSummary(extractTexts(msg.parts).map((t) => msg.role === "user" ? cleanUserText(t) : t).map(normalizeText).filter((value) => value.length > 0).join(" / "), msg.role);
16608
- const text = compacted.text;
16609
- if (!text) {
16610
- pendingNoiseMeta.push(meta3);
16611
- continue;
16831
+ const rootHash = getRootCommitHash(resolved);
16832
+ const identity = rootHash ? `git:${rootHash}` : directoryFallback(resolved);
16833
+ resolvedCache.set(resolved, identity);
16834
+ return identity;
16835
+ }
16836
+
16837
+ // src/hooks/magic-context/execute-flush.ts
16838
+ function executeFlush(db, sessionId) {
16839
+ try {
16840
+ const pendingOps = getPendingOps(db, sessionId);
16841
+ if (pendingOps.length === 0) {
16842
+ return "No pending operations to flush.";
16612
16843
  }
16613
- if (currentBlock && currentBlock.role === role) {
16614
- currentBlock.endOrdinal = msg.ordinal;
16615
- currentBlock.parts.push(text);
16616
- currentBlock.meta.push(...pendingNoiseMeta, meta3);
16617
- currentBlock.commitHashes = mergeCommitHashes(currentBlock.commitHashes, compacted.commitHashes);
16618
- pendingNoiseMeta = [];
16619
- continue;
16844
+ let dropped = 0;
16845
+ db.transaction(() => {
16846
+ for (const op of pendingOps) {
16847
+ updateTagStatus(db, sessionId, op.tagId, "dropped");
16848
+ removePendingOp(db, sessionId, op.tagId);
16849
+ dropped++;
16850
+ }
16851
+ })();
16852
+ const parts = [];
16853
+ if (dropped > 0)
16854
+ parts.push(`${dropped} dropped`);
16855
+ if (dropped > 0) {
16856
+ clearPersistedStickyTurnReminder(db, sessionId);
16620
16857
  }
16621
- if (!flushCurrentBlock())
16622
- break;
16623
- currentBlock = {
16624
- role,
16625
- startOrdinal: pendingNoiseMeta[0]?.ordinal ?? msg.ordinal,
16626
- endOrdinal: msg.ordinal,
16627
- parts: [text],
16628
- meta: [...pendingNoiseMeta, meta3],
16629
- commitHashes: [...compacted.commitHashes]
16630
- };
16631
- pendingNoiseMeta = [];
16858
+ return `Flushed: ${parts.join(", ")}. Changes take effect on next message.`;
16859
+ } catch (error48) {
16860
+ sessionLog(sessionId, "ctx-flush failed:", error48);
16861
+ return `Error: Failed to flush context operations. ${getErrorMessage(error48)}`;
16632
16862
  }
16633
- flushCurrentBlock();
16634
- return {
16635
- startIndex: startOrdinal,
16636
- endIndex: lastOrdinal,
16637
- startMessageId: firstMessageId,
16638
- endMessageId: lastMessageId,
16639
- messageCount: messagesProcessed,
16640
- tokenEstimate: totalTokens,
16641
- hasMore: lastOrdinal < (eligibleEndOrdinal !== undefined ? Math.min(eligibleEndOrdinal - 1, messages.length) : messages.length),
16642
- text: lines.join(`
16643
- `),
16644
- lines: lineMeta,
16645
- commitClusterCount: commitClusters
16646
- };
16647
16863
  }
16648
16864
 
16649
16865
  // src/hooks/magic-context/compartment-trigger.ts
@@ -16680,29 +16896,31 @@ var TAIL_INFO_DEFAULTS = {
16680
16896
  commitClusterCount: 0
16681
16897
  };
16682
16898
  function getUnsummarizedTailInfo(db, sessionId, compartmentTokenBudget) {
16683
- try {
16684
- const lastCompartmentEnd = getLastCompartmentEndMessage(db, sessionId);
16685
- const nextStartOrdinal = Math.max(1, lastCompartmentEnd + 1);
16686
- const rawMessageCount = getRawSessionMessageCount(sessionId);
16687
- const protectedTailStart = getProtectedTailStartOrdinal(sessionId);
16688
- const hasEligibleHistory = rawMessageCount >= nextStartOrdinal && nextStartOrdinal < protectedTailStart;
16689
- if (!hasEligibleHistory) {
16690
- return { ...TAIL_INFO_DEFAULTS, nextStartOrdinal };
16899
+ return withRawSessionMessageCache(() => {
16900
+ try {
16901
+ const lastCompartmentEnd = getLastCompartmentEndMessage(db, sessionId);
16902
+ const nextStartOrdinal = Math.max(1, lastCompartmentEnd + 1);
16903
+ const rawMessageCount = getRawSessionMessageCount(sessionId);
16904
+ const protectedTailStart = getProtectedTailStartOrdinal(sessionId);
16905
+ const hasEligibleHistory = rawMessageCount >= nextStartOrdinal && nextStartOrdinal < protectedTailStart;
16906
+ if (!hasEligibleHistory) {
16907
+ return { ...TAIL_INFO_DEFAULTS, nextStartOrdinal };
16908
+ }
16909
+ const scanBudget = Math.max(MIN_PROACTIVE_TAIL_TOKEN_ESTIMATE, compartmentTokenBudget * TAIL_SIZE_TRIGGER_MULTIPLIER);
16910
+ const chunk = readSessionChunk(sessionId, scanBudget, nextStartOrdinal, protectedTailStart);
16911
+ const isMeaningful = chunk.hasMore || chunk.tokenEstimate >= MIN_PROACTIVE_TAIL_TOKEN_ESTIMATE || chunk.messageCount >= MIN_PROACTIVE_TAIL_MESSAGE_COUNT;
16912
+ return {
16913
+ nextStartOrdinal,
16914
+ hasNewRawHistory: true,
16915
+ isMeaningful,
16916
+ tokenEstimate: chunk.tokenEstimate,
16917
+ commitClusterCount: chunk.commitClusterCount
16918
+ };
16919
+ } catch (error48) {
16920
+ sessionLog(sessionId, "compartment trigger: raw tail inspection failed:", error48);
16921
+ return TAIL_INFO_DEFAULTS;
16691
16922
  }
16692
- const scanBudget = Math.max(MIN_PROACTIVE_TAIL_TOKEN_ESTIMATE, compartmentTokenBudget * TAIL_SIZE_TRIGGER_MULTIPLIER);
16693
- const chunk = readSessionChunk(sessionId, scanBudget, nextStartOrdinal, protectedTailStart);
16694
- const isMeaningful = chunk.hasMore || chunk.tokenEstimate >= MIN_PROACTIVE_TAIL_TOKEN_ESTIMATE || chunk.messageCount >= MIN_PROACTIVE_TAIL_MESSAGE_COUNT;
16695
- return {
16696
- nextStartOrdinal,
16697
- hasNewRawHistory: true,
16698
- isMeaningful,
16699
- tokenEstimate: chunk.tokenEstimate,
16700
- commitClusterCount: chunk.commitClusterCount
16701
- };
16702
- } catch (error48) {
16703
- sessionLog(sessionId, "compartment trigger: raw tail inspection failed:", error48);
16704
- return TAIL_INFO_DEFAULTS;
16705
- }
16923
+ });
16706
16924
  }
16707
16925
  function checkCompartmentTrigger(db, sessionId, sessionMeta, usage, _previousPercentage, executeThresholdPercentage, compartmentTokenBudget = DEFAULT_COMPARTMENT_TOKEN_BUDGET) {
16708
16926
  if (sessionMeta.compartmentInProgress) {
@@ -17315,6 +17533,118 @@ var CATEGORY_DEFAULT_TTL = {
17315
17533
  KNOWN_ISSUES: 30 * 24 * 60 * 60 * 1000
17316
17534
  };
17317
17535
 
17536
+ // src/features/magic-context/memory/storage-memory-embeddings.ts
17537
+ var saveEmbeddingStatements = new WeakMap;
17538
+ var loadAllEmbeddingsStatements = new WeakMap;
17539
+ var deleteEmbeddingStatements = new WeakMap;
17540
+ var getStoredModelIdStatements = new WeakMap;
17541
+ var clearAllEmbeddingsStatements = new WeakMap;
17542
+ function isEmbeddingBlob(value) {
17543
+ return value instanceof Uint8Array || value instanceof ArrayBuffer;
17544
+ }
17545
+ function isEmbeddingRow(row) {
17546
+ if (row === null || typeof row !== "object")
17547
+ return false;
17548
+ const candidate = row;
17549
+ return typeof candidate.memoryId === "number" && isEmbeddingBlob(candidate.embedding);
17550
+ }
17551
+ function toFloat32Array(blob) {
17552
+ if (blob instanceof Uint8Array) {
17553
+ const buffer2 = blob.buffer.slice(blob.byteOffset, blob.byteOffset + blob.byteLength);
17554
+ return new Float32Array(buffer2);
17555
+ }
17556
+ return new Float32Array(blob.slice(0));
17557
+ }
17558
+ function getSaveEmbeddingStatement(db) {
17559
+ let stmt = saveEmbeddingStatements.get(db);
17560
+ if (!stmt) {
17561
+ stmt = db.prepare("INSERT INTO memory_embeddings (memory_id, embedding, model_id) VALUES (?, ?, ?) ON CONFLICT(memory_id) DO UPDATE SET embedding = excluded.embedding, model_id = excluded.model_id");
17562
+ saveEmbeddingStatements.set(db, stmt);
17563
+ }
17564
+ return stmt;
17565
+ }
17566
+ function getLoadAllEmbeddingsStatement(db) {
17567
+ let stmt = loadAllEmbeddingsStatements.get(db);
17568
+ if (!stmt) {
17569
+ stmt = db.prepare("SELECT memory_embeddings.memory_id AS memoryId, memory_embeddings.embedding AS embedding FROM memory_embeddings INNER JOIN memories ON memories.id = memory_embeddings.memory_id WHERE memories.project_path = ? ORDER BY memory_embeddings.memory_id ASC");
17570
+ loadAllEmbeddingsStatements.set(db, stmt);
17571
+ }
17572
+ return stmt;
17573
+ }
17574
+ function getStoredModelIdStatement(db) {
17575
+ let stmt = getStoredModelIdStatements.get(db);
17576
+ if (!stmt) {
17577
+ stmt = db.prepare("SELECT memory_embeddings.model_id AS modelId FROM memory_embeddings INNER JOIN memories ON memories.id = memory_embeddings.memory_id WHERE memories.project_path = ? AND memory_embeddings.model_id IS NOT NULL LIMIT 1");
17578
+ getStoredModelIdStatements.set(db, stmt);
17579
+ }
17580
+ return stmt;
17581
+ }
17582
+ function getClearAllEmbeddingsStatement(db) {
17583
+ let stmt = clearAllEmbeddingsStatements.get(db);
17584
+ if (!stmt) {
17585
+ stmt = db.prepare("DELETE FROM memory_embeddings WHERE memory_id IN (SELECT id FROM memories WHERE project_path = ?)");
17586
+ clearAllEmbeddingsStatements.set(db, stmt);
17587
+ }
17588
+ return stmt;
17589
+ }
17590
+ function saveEmbedding(db, memoryId, embedding, modelId) {
17591
+ const blob = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
17592
+ getSaveEmbeddingStatement(db).run(memoryId, blob, modelId);
17593
+ }
17594
+ function loadAllEmbeddings(db, projectPath) {
17595
+ const rows = getLoadAllEmbeddingsStatement(db).all(projectPath).filter(isEmbeddingRow);
17596
+ const embeddings = new Map;
17597
+ for (const row of rows) {
17598
+ embeddings.set(row.memoryId, toFloat32Array(row.embedding));
17599
+ }
17600
+ return embeddings;
17601
+ }
17602
+ function getStoredModelId(db, projectPath) {
17603
+ const row = getStoredModelIdStatement(db).get(projectPath);
17604
+ return typeof row?.modelId === "string" ? row.modelId : null;
17605
+ }
17606
+ function clearEmbeddingsForProject(db, projectPath) {
17607
+ getClearAllEmbeddingsStatement(db).run(projectPath);
17608
+ }
17609
+
17610
+ // src/features/magic-context/memory/embedding-cache.ts
17611
+ var DEFAULT_EMBEDDING_CACHE_TTL_MS = 60000;
17612
+ var projectEmbeddingCache = new Map;
17613
+ var embeddingCacheTtlMs = DEFAULT_EMBEDDING_CACHE_TTL_MS;
17614
+ function getValidCacheEntry(projectPath) {
17615
+ const entry = projectEmbeddingCache.get(projectPath);
17616
+ if (!entry) {
17617
+ return null;
17618
+ }
17619
+ if (entry.expiresAt <= Date.now()) {
17620
+ projectEmbeddingCache.delete(projectPath);
17621
+ return null;
17622
+ }
17623
+ return entry;
17624
+ }
17625
+ function getProjectEmbeddings(db, projectPath) {
17626
+ const cached2 = getValidCacheEntry(projectPath);
17627
+ if (cached2) {
17628
+ return cached2.embeddings;
17629
+ }
17630
+ const embeddings = loadAllEmbeddings(db, projectPath);
17631
+ projectEmbeddingCache.set(projectPath, {
17632
+ embeddings,
17633
+ expiresAt: Date.now() + embeddingCacheTtlMs
17634
+ });
17635
+ return embeddings;
17636
+ }
17637
+ function peekProjectEmbeddings(projectPath) {
17638
+ return getValidCacheEntry(projectPath)?.embeddings ?? null;
17639
+ }
17640
+ function invalidateProject(projectPath) {
17641
+ projectEmbeddingCache.delete(projectPath);
17642
+ }
17643
+ function invalidateMemory(projectPath, memoryId) {
17644
+ const cached2 = getValidCacheEntry(projectPath);
17645
+ cached2?.embeddings.delete(memoryId);
17646
+ }
17647
+
17318
17648
  // src/features/magic-context/memory/normalize-hash.ts
17319
17649
  function normalizeMemoryContent(content) {
17320
17650
  return content.toLowerCase().replace(/\s+/g, " ").trim();
@@ -17479,7 +17809,7 @@ function getMemoriesByProjectStatement(db, statuses) {
17479
17809
  let stmt = statements.get(db);
17480
17810
  if (!stmt) {
17481
17811
  const placeholders = statuses.map(() => "?").join(", ");
17482
- stmt = db.prepare(`SELECT ${getMemorySelectColumns()} FROM memories WHERE project_path = ? AND status IN (${placeholders}) ORDER BY category ASC, updated_at DESC, id ASC`);
17812
+ stmt = db.prepare(`SELECT ${getMemorySelectColumns()} FROM memories WHERE project_path = ? AND status IN (${placeholders}) AND (expires_at IS NULL OR expires_at > ?) ORDER BY category ASC, updated_at DESC, id ASC`);
17483
17813
  statements.set(db, stmt);
17484
17814
  }
17485
17815
  return stmt;
@@ -17549,6 +17879,7 @@ function insertMemory(db, input) {
17549
17879
  if (!inserted) {
17550
17880
  throw new Error("Failed to load inserted memory row");
17551
17881
  }
17882
+ invalidateProject(input.projectPath);
17552
17883
  return inserted;
17553
17884
  }
17554
17885
  function getMemoryByHash(db, projectPath, category, normalizedHash) {
@@ -17562,7 +17893,7 @@ function getMemoriesByProject(db, projectPath, statuses = ["active", "permanent"
17562
17893
  if (statuses.length === 0) {
17563
17894
  return [];
17564
17895
  }
17565
- const rows = getMemoriesByProjectStatement(db, statuses).all(projectPath, ...statuses).filter(isMemoryRow);
17896
+ const rows = getMemoriesByProjectStatement(db, statuses).all(projectPath, ...statuses, Date.now()).filter(isMemoryRow);
17566
17897
  return rows.map(toMemory);
17567
17898
  }
17568
17899
  function getMemoryById(db, id) {
@@ -17598,6 +17929,7 @@ function mergeMetadataJson(existing, patch) {
17598
17929
  return JSON.stringify({ ...base, ...patch });
17599
17930
  }
17600
17931
  function updateMemoryContent(db, id, content, normalizedHash) {
17932
+ const memory = getMemoryById(db, id);
17601
17933
  db.transaction(() => {
17602
17934
  getUpdateMemoryContentStatement(db).run(content, normalizedHash, Date.now(), id);
17603
17935
  let stmt = deleteEmbeddingOnContentUpdateStatements.get(db);
@@ -17607,6 +17939,9 @@ function updateMemoryContent(db, id, content, normalizedHash) {
17607
17939
  }
17608
17940
  stmt.run(id);
17609
17941
  })();
17942
+ if (memory) {
17943
+ invalidateMemory(memory.projectPath, id);
17944
+ }
17610
17945
  }
17611
17946
  function supersededMemory(db, id, supersededById) {
17612
17947
  getSupersededMemoryStatement(db).run(supersededById, Date.now(), id);
@@ -18288,19 +18623,19 @@ function cosineSimilarity(a, b) {
18288
18623
  function isArrayLikeNumber(value) {
18289
18624
  return typeof value === "object" && value !== null && "length" in value;
18290
18625
  }
18291
- function toFloat32Array(values) {
18626
+ function toFloat32Array2(values) {
18292
18627
  return values instanceof Float32Array ? new Float32Array(values) : Float32Array.from(Array.from(values));
18293
18628
  }
18294
18629
  function extractBatchEmbeddings(result, expectedCount) {
18295
18630
  const { data } = result;
18296
18631
  if (Array.isArray(data) && data.length === expectedCount && data.every((entry) => typeof entry !== "number" && isArrayLikeNumber(entry))) {
18297
- return data.map((entry) => toFloat32Array(entry));
18632
+ return data.map((entry) => toFloat32Array2(entry));
18298
18633
  }
18299
18634
  if (!isArrayLikeNumber(data)) {
18300
18635
  log("[magic-context] embedding batch returned unexpected data shape");
18301
18636
  return Array.from({ length: expectedCount }, () => null);
18302
18637
  }
18303
- const flatData = toFloat32Array(data);
18638
+ const flatData = toFloat32Array2(data);
18304
18639
  const dimension = result.dims?.at(-1) ?? flatData.length / expectedCount;
18305
18640
  if (!Number.isInteger(dimension) || dimension <= 0 || flatData.length !== expectedCount * dimension) {
18306
18641
  log("[magic-context] embedding batch returned invalid dimensions");
@@ -18516,7 +18851,8 @@ function resolveModelId(config2) {
18516
18851
  if (config2.provider === "openai-compatible") {
18517
18852
  const endpoint = config2.endpoint.trim();
18518
18853
  const model = config2.model.trim();
18519
- return `openai-compat:${endpoint}:${model}`;
18854
+ const keyHash = config2.api_key ? computeNormalizedHash(config2.api_key) : "nokey";
18855
+ return `openai-compat:${endpoint}:${model}:${keyHash}`;
18520
18856
  }
18521
18857
  return config2.model.trim() || DEFAULT_LOCAL_EMBEDDING_MODEL;
18522
18858
  }
@@ -18581,85 +18917,11 @@ async function embedBatch(texts) {
18581
18917
  if (!await currentProvider.initialize()) {
18582
18918
  return Array.from({ length: texts.length }, () => null);
18583
18919
  }
18584
- return currentProvider.embedBatch(texts);
18585
- }
18586
- function getEmbeddingModelId() {
18587
- return getOrCreateProvider()?.modelId ?? "off";
18588
- }
18589
- // src/features/magic-context/memory/storage-memory-embeddings.ts
18590
- var saveEmbeddingStatements = new WeakMap;
18591
- var loadAllEmbeddingsStatements = new WeakMap;
18592
- var deleteEmbeddingStatements = new WeakMap;
18593
- var getStoredModelIdStatements = new WeakMap;
18594
- var clearAllEmbeddingsStatements = new WeakMap;
18595
- function isEmbeddingBlob(value) {
18596
- return value instanceof Uint8Array || value instanceof ArrayBuffer;
18597
- }
18598
- function isEmbeddingRow(row) {
18599
- if (row === null || typeof row !== "object")
18600
- return false;
18601
- const candidate = row;
18602
- return typeof candidate.memoryId === "number" && isEmbeddingBlob(candidate.embedding);
18603
- }
18604
- function toFloat32Array2(blob) {
18605
- if (blob instanceof Uint8Array) {
18606
- const buffer2 = blob.buffer.slice(blob.byteOffset, blob.byteOffset + blob.byteLength);
18607
- return new Float32Array(buffer2);
18608
- }
18609
- return new Float32Array(blob.slice(0));
18610
- }
18611
- function getSaveEmbeddingStatement(db) {
18612
- let stmt = saveEmbeddingStatements.get(db);
18613
- if (!stmt) {
18614
- stmt = db.prepare("INSERT INTO memory_embeddings (memory_id, embedding, model_id) VALUES (?, ?, ?) ON CONFLICT(memory_id) DO UPDATE SET embedding = excluded.embedding, model_id = excluded.model_id");
18615
- saveEmbeddingStatements.set(db, stmt);
18616
- }
18617
- return stmt;
18618
- }
18619
- function getLoadAllEmbeddingsStatement(db) {
18620
- let stmt = loadAllEmbeddingsStatements.get(db);
18621
- if (!stmt) {
18622
- stmt = db.prepare("SELECT memory_embeddings.memory_id AS memoryId, memory_embeddings.embedding AS embedding FROM memory_embeddings INNER JOIN memories ON memories.id = memory_embeddings.memory_id WHERE memories.project_path = ? ORDER BY memory_embeddings.memory_id ASC");
18623
- loadAllEmbeddingsStatements.set(db, stmt);
18624
- }
18625
- return stmt;
18626
- }
18627
- function getStoredModelIdStatement(db) {
18628
- let stmt = getStoredModelIdStatements.get(db);
18629
- if (!stmt) {
18630
- stmt = db.prepare("SELECT memory_embeddings.model_id AS modelId FROM memory_embeddings INNER JOIN memories ON memories.id = memory_embeddings.memory_id WHERE memories.project_path = ? AND memory_embeddings.model_id IS NOT NULL LIMIT 1");
18631
- getStoredModelIdStatements.set(db, stmt);
18632
- }
18633
- return stmt;
18634
- }
18635
- function getClearAllEmbeddingsStatement(db) {
18636
- let stmt = clearAllEmbeddingsStatements.get(db);
18637
- if (!stmt) {
18638
- stmt = db.prepare("DELETE FROM memory_embeddings WHERE memory_id IN (SELECT id FROM memories WHERE project_path = ?)");
18639
- clearAllEmbeddingsStatements.set(db, stmt);
18640
- }
18641
- return stmt;
18642
- }
18643
- function saveEmbedding(db, memoryId, embedding, modelId) {
18644
- const blob = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
18645
- getSaveEmbeddingStatement(db).run(memoryId, blob, modelId);
18646
- }
18647
- function loadAllEmbeddings(db, projectPath) {
18648
- const rows = getLoadAllEmbeddingsStatement(db).all(projectPath).filter(isEmbeddingRow);
18649
- const embeddings = new Map;
18650
- for (const row of rows) {
18651
- embeddings.set(row.memoryId, toFloat32Array2(row.embedding));
18652
- }
18653
- return embeddings;
18654
- }
18655
- function getStoredModelId(db, projectPath) {
18656
- const row = getStoredModelIdStatement(db).get(projectPath);
18657
- return typeof row?.modelId === "string" ? row.modelId : null;
18920
+ return currentProvider.embedBatch(texts);
18658
18921
  }
18659
- function clearEmbeddingsForProject(db, projectPath) {
18660
- getClearAllEmbeddingsStatement(db).run(projectPath);
18922
+ function getEmbeddingModelId() {
18923
+ return getOrCreateProvider()?.modelId ?? "off";
18661
18924
  }
18662
-
18663
18925
  // src/features/magic-context/memory/embedding-backfill.ts
18664
18926
  async function ensureMemoryEmbeddings(args) {
18665
18927
  if (!isEmbeddingEnabled()) {
@@ -18743,7 +19005,7 @@ function getSearchStatement(db) {
18743
19005
  let stmt = searchStatements.get(db);
18744
19006
  if (!stmt) {
18745
19007
  const selectColumns = Object.entries(COLUMN_MAP).map(([property, column]) => `memories.${column} AS ${property}`).join(", ");
18746
- stmt = db.prepare(`SELECT ${selectColumns} FROM memories_fts INNER JOIN memories ON memories.id = memories_fts.rowid WHERE memories.project_path = ? AND memories.status IN ('active', 'permanent') AND memories_fts MATCH ? ORDER BY bm25(memories_fts), memories.updated_at DESC, memories.id ASC LIMIT ?`);
19008
+ stmt = db.prepare(`SELECT ${selectColumns} FROM memories_fts INNER JOIN memories ON memories.id = memories_fts.rowid WHERE memories.project_path = ? AND memories.status IN ('active', 'permanent') AND (memories.expires_at IS NULL OR memories.expires_at > ?) AND memories_fts MATCH ? ORDER BY bm25(memories_fts), memories.updated_at DESC, memories.id ASC LIMIT ?`);
18747
19009
  searchStatements.set(db, stmt);
18748
19010
  }
18749
19011
  return stmt;
@@ -18763,7 +19025,7 @@ function searchMemoriesFTS(db, projectPath, query, limit = DEFAULT_SEARCH_LIMIT)
18763
19025
  if (sanitized.length === 0) {
18764
19026
  return [];
18765
19027
  }
18766
- const rows = getSearchStatement(db).all(projectPath, sanitized, limit).filter(isMemoryRow);
19028
+ const rows = getSearchStatement(db).all(projectPath, Date.now(), sanitized, limit).filter(isMemoryRow);
18767
19029
  return rows.map((row) => ({ ...row }));
18768
19030
  }
18769
19031
  // src/hooks/magic-context/compartment-parser.ts
@@ -19838,9 +20100,9 @@ function loadContextUsage(contextUsageMap, db, sessionId) {
19838
20100
  }
19839
20101
  return contextUsageEntry?.usage ?? { percentage: 0, inputTokens: 0 };
19840
20102
  }
19841
- function resolveSchedulerDecision(scheduler2, sessionMeta, contextUsage, sessionId) {
20103
+ function resolveSchedulerDecision(scheduler2, sessionMeta, contextUsage, sessionId, modelKey) {
19842
20104
  try {
19843
- const schedulerDecision = scheduler2.shouldExecute(sessionMeta, contextUsage, undefined, sessionId);
20105
+ const schedulerDecision = scheduler2.shouldExecute(sessionMeta, contextUsage, undefined, sessionId, modelKey);
19844
20106
  sessionLog(sessionId, `transform scheduler: percentage=${contextUsage.percentage.toFixed(1)}% inputTokens=${contextUsage.inputTokens} cacheTtl=${sessionMeta.cacheTtl} lastResponseTime=${sessionMeta.lastResponseTime} decision=${schedulerDecision}`);
19845
20107
  return schedulerDecision;
19846
20108
  } catch (error48) {
@@ -20082,9 +20344,17 @@ function createScopedAssignments(assignments) {
20082
20344
  return scoped;
20083
20345
  }
20084
20346
  function createExistingTagResolver(sessionId, tagger, db) {
20085
- const assignments = new Map(tagger.getAssignments(sessionId));
20086
- const scopedAssignments = createScopedAssignments(assignments);
20347
+ const assignments = tagger.getAssignments(sessionId);
20348
+ let cachedAssignmentSize = -1;
20349
+ let cachedScopedAssignments = null;
20087
20350
  const usedTagNumbers = new Set;
20351
+ function getScopedAssignments() {
20352
+ if (!cachedScopedAssignments || cachedAssignmentSize !== assignments.size) {
20353
+ cachedScopedAssignments = createScopedAssignments(assignments);
20354
+ cachedAssignmentSize = assignments.size;
20355
+ }
20356
+ return cachedScopedAssignments;
20357
+ }
20088
20358
  return {
20089
20359
  resolve(messageId, type, currentContentId, ordinal) {
20090
20360
  const exactTagId = assignments.get(currentContentId);
@@ -20092,13 +20362,12 @@ function createExistingTagResolver(sessionId, tagger, db) {
20092
20362
  usedTagNumbers.add(exactTagId);
20093
20363
  return exactTagId;
20094
20364
  }
20095
- const fallback = scopedAssignments.get(messageId)?.[type][ordinal];
20365
+ const fallback = getScopedAssignments().get(messageId)?.[type][ordinal];
20096
20366
  if (!fallback || usedTagNumbers.has(fallback.tagNumber)) {
20097
20367
  return;
20098
20368
  }
20099
20369
  updateTagMessageId(db, sessionId, fallback.tagNumber, currentContentId);
20100
20370
  tagger.bindTag(sessionId, currentContentId, fallback.tagNumber);
20101
- assignments.set(currentContentId, fallback.tagNumber);
20102
20371
  usedTagNumbers.add(fallback.tagNumber);
20103
20372
  return fallback.tagNumber;
20104
20373
  }
@@ -20980,7 +21249,7 @@ function createTransform(deps) {
20980
21249
  const compartmentDirectory = deps.directory ?? "";
20981
21250
  const canRunCompartments = fullFeatureMode && deps.client !== undefined && compartmentDirectory.length > 0;
20982
21251
  const contextUsageEarly = loadContextUsage(deps.contextUsageMap, db, sessionId);
20983
- const schedulerDecisionEarly = resolveSchedulerDecision(deps.scheduler, sessionMeta, contextUsageEarly, sessionId);
21252
+ const schedulerDecisionEarly = resolveSchedulerDecision(deps.scheduler, sessionMeta, contextUsageEarly, sessionId, deps.getModelKey?.(sessionId));
20984
21253
  const isCacheBusting = deps.flushedSessions.has(sessionId) || schedulerDecisionEarly === "execute";
20985
21254
  let pendingCompartmentInjection = null;
20986
21255
  if (fullFeatureMode) {
@@ -21120,11 +21389,13 @@ function createChatMessageHook(args) {
21120
21389
  const sessionId = input.sessionID;
21121
21390
  if (!sessionId)
21122
21391
  return;
21123
- const sessionMeta = getOrCreateSessionMeta(args.db, sessionId);
21124
- const turnUsage = args.toolUsageSinceUserTurn.get(sessionId);
21125
- const agentAlreadyReduced = args.recentReduceBySession.has(sessionId);
21126
- if (!sessionMeta.isSubagent && !agentAlreadyReduced && getPersistedStickyTurnReminder(args.db, sessionId) === null && turnUsage !== undefined && turnUsage >= TOOL_HEAVY_TURN_REMINDER_THRESHOLD) {
21127
- setPersistedStickyTurnReminder(args.db, sessionId, TOOL_HEAVY_TURN_REMINDER_TEXT);
21392
+ if (args.ctxReduceEnabled !== false) {
21393
+ const sessionMeta = getOrCreateSessionMeta(args.db, sessionId);
21394
+ const turnUsage = args.toolUsageSinceUserTurn.get(sessionId);
21395
+ const agentAlreadyReduced = args.recentReduceBySession.has(sessionId);
21396
+ if (!sessionMeta.isSubagent && !agentAlreadyReduced && getPersistedStickyTurnReminder(args.db, sessionId) === null && turnUsage !== undefined && turnUsage >= TOOL_HEAVY_TURN_REMINDER_THRESHOLD) {
21397
+ setPersistedStickyTurnReminder(args.db, sessionId, TOOL_HEAVY_TURN_REMINDER_TEXT);
21398
+ }
21128
21399
  }
21129
21400
  args.toolUsageSinceUserTurn.set(sessionId, 0);
21130
21401
  const previousVariant = args.variantBySession.get(sessionId);
@@ -21170,6 +21441,8 @@ function createEventHook(args) {
21170
21441
  args.emergencyNudgeFired.delete(sessionId);
21171
21442
  return;
21172
21443
  }
21444
+ if (args.ctxReduceEnabled === false)
21445
+ return;
21173
21446
  if (args.emergencyNudgeFired.has(sessionId))
21174
21447
  return;
21175
21448
  const meta3 = getOrCreateSessionMeta(args.db, sessionId);
@@ -21242,12 +21515,18 @@ Use \`ctx_reduce\` to manage context size. It supports one operation:
21242
21515
  - \`drop\`: Remove entirely (best for tool outputs you already acted on).
21243
21516
  Syntax: "3-5", "1,2,9", or "1-5,8,12-15". Last ${protectedTags} tags are protected.
21244
21517
  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).
21245
- Use \`ctx_memory\` to manage cross-session project memories. Write new memories, delete stale ones, or search stored memories by category. Memories persist across sessions and are automatically injected into new sessions.
21518
+ 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.
21519
+ Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
21246
21520
  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.
21247
21521
  NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
21248
21522
  NEVER drop user messages \u2014 they are short and will be summarized by compartmentalization automatically. Dropping them loses context the historian needs.
21249
21523
  NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
21250
21524
  Before your turn finishes, consider using \`ctx_reduce\` to drop large tool outputs you no longer need.`;
21525
+ var BASE_INTRO_NO_REDUCE = `Messages and tool outputs are tagged with \xA7N\xA7 identifiers (e.g., \xA71\xA7, \xA742\xA7).
21526
+ 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).
21527
+ 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.
21528
+ Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
21529
+ 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.`;
21251
21530
  var SISYPHUS_SECTION = `
21252
21531
  ### Reduction Triggers
21253
21532
  - After collecting background agent results (explore/librarian) \u2014 drop raw outputs once you extracted what you need.
@@ -21384,7 +21663,12 @@ function detectAgentFromSystemPrompt(systemPrompt) {
21384
21663
  }
21385
21664
  return null;
21386
21665
  }
21387
- function buildMagicContextSection(agent, protectedTags) {
21666
+ function buildMagicContextSection(agent, protectedTags, ctxReduceEnabled = true) {
21667
+ if (!ctxReduceEnabled) {
21668
+ return `## Magic Context
21669
+
21670
+ ${BASE_INTRO_NO_REDUCE}`;
21671
+ }
21388
21672
  const section = agent ? AGENT_SECTIONS[agent] : GENERIC_SECTION;
21389
21673
  return `## Magic Context
21390
21674
 
@@ -21405,7 +21689,7 @@ function createSystemPromptHashHandler(deps) {
21405
21689
  `);
21406
21690
  if (fullPrompt.length > 0 && !fullPrompt.includes(MAGIC_CONTEXT_MARKER)) {
21407
21691
  const detectedAgent = detectAgentFromSystemPrompt(fullPrompt);
21408
- const guidance = buildMagicContextSection(detectedAgent, deps.protectedTags);
21692
+ const guidance = buildMagicContextSection(detectedAgent, deps.protectedTags, deps.ctxReduceEnabled);
21409
21693
  output.system.push(guidance);
21410
21694
  sessionLog(sessionId, `injected ${detectedAgent ?? "generic"} guidance into system prompt`);
21411
21695
  }
@@ -21480,13 +21764,14 @@ function createMagicContextHook(deps) {
21480
21764
  const liveModelBySession = new Map;
21481
21765
  const recentReduceBySession = new Map;
21482
21766
  const toolUsageSinceUserTurn = new Map;
21483
- const nudgerWithRecentReduce = createNudger({
21767
+ const ctxReduceEnabled = deps.config.ctx_reduce_enabled !== false;
21768
+ const nudgerWithRecentReduce = ctxReduceEnabled ? createNudger({
21484
21769
  protected_tags: deps.config.protected_tags,
21485
21770
  nudge_interval_tokens: deps.config.nudge_interval_tokens ?? DEFAULT_NUDGE_INTERVAL_TOKENS,
21486
21771
  iteration_nudge_threshold: deps.config.iteration_nudge_threshold ?? 15,
21487
21772
  execute_threshold_percentage: deps.config.execute_threshold_percentage ?? 65,
21488
21773
  recentReduceBySession
21489
- });
21774
+ }) : () => null;
21490
21775
  const transform2 = createTransform({
21491
21776
  tagger: deps.tagger,
21492
21777
  scheduler: deps.scheduler,
@@ -21509,7 +21794,11 @@ function createMagicContextHook(deps) {
21509
21794
  compartmentTokenBudget: deps.config.compartment_token_budget ?? DEFAULT_COMPARTMENT_TOKEN_BUDGET,
21510
21795
  historyBudgetPercentage: deps.config.history_budget_percentage,
21511
21796
  historianTimeoutMs: deps.config.historian_timeout_ms ?? DEFAULT_HISTORIAN_TIMEOUT_MS,
21512
- getNotificationParams: (sessionId) => getLiveNotificationParams(sessionId, liveModelBySession, variantBySession)
21797
+ getNotificationParams: (sessionId) => getLiveNotificationParams(sessionId, liveModelBySession, variantBySession),
21798
+ getModelKey: (sessionId) => {
21799
+ const model = liveModelBySession.get(sessionId);
21800
+ return resolveModelKey(model?.providerID, model?.modelID);
21801
+ }
21513
21802
  });
21514
21803
  const eventHandler = createEventHandler2({
21515
21804
  contextUsageMap,
@@ -21592,6 +21881,7 @@ function createMagicContextHook(deps) {
21592
21881
  const systemPromptHashHandler = createSystemPromptHashHandler({
21593
21882
  db,
21594
21883
  protectedTags: deps.config.protected_tags,
21884
+ ctxReduceEnabled,
21595
21885
  flushedSessions,
21596
21886
  lastHeuristicsTurnId
21597
21887
  });
@@ -21608,7 +21898,8 @@ function createMagicContextHook(deps) {
21608
21898
  lastHeuristicsTurnId,
21609
21899
  commitSeenLastPass,
21610
21900
  client: deps.client,
21611
- protectedTags: deps.config.protected_tags
21901
+ protectedTags: deps.config.protected_tags,
21902
+ ctxReduceEnabled
21612
21903
  });
21613
21904
  return {
21614
21905
  "experimental.chat.messages.transform": transform2,
@@ -21620,7 +21911,8 @@ function createMagicContextHook(deps) {
21620
21911
  recentReduceBySession,
21621
21912
  variantBySession,
21622
21913
  flushedSessions,
21623
- lastHeuristicsTurnId
21914
+ lastHeuristicsTurnId,
21915
+ ctxReduceEnabled
21624
21916
  }),
21625
21917
  event: async (input) => {
21626
21918
  await eventHook(input);
@@ -21723,18 +22015,17 @@ function createCtxExpandTools() {
21723
22015
  }
21724
22016
  // src/tools/ctx-memory/constants.ts
21725
22017
  var CTX_MEMORY_TOOL_NAME = "ctx_memory";
21726
- var CTX_MEMORY_DESCRIPTION = `Manage cross-session project memories. Write new memories, delete stale ones, or search stored memories by category. Memories persist across sessions and are automatically injected into new sessions.
22018
+ var CTX_MEMORY_DESCRIPTION = `Manage cross-session project memories. Primary sessions can write new memories or delete stale ones. Dreamer sessions can also list, update, merge, and archive memories. Memories persist across sessions and are automatically injected into new sessions.
21727
22019
 
21728
- Supported actions: write, delete, search, list, update, merge, archive.`;
22020
+ Supported actions: write, delete, list, update, merge, archive.`;
21729
22021
  var DEFAULT_SEARCH_LIMIT2 = 10;
21730
22022
  // src/tools/ctx-memory/tools.ts
21731
22023
  import { tool as tool2 } from "@opencode-ai/plugin";
21732
22024
 
21733
22025
  // src/tools/ctx-memory/types.ts
21734
- var CTX_MEMORY_ACTIONS = [
21735
- "write",
21736
- "delete",
21737
- "search",
22026
+ var CTX_MEMORY_ACTIONS = ["write", "delete"];
22027
+ var CTX_MEMORY_DREAMER_ACTIONS = [
22028
+ ...CTX_MEMORY_ACTIONS,
21738
22029
  "list",
21739
22030
  "update",
21740
22031
  "merge",
@@ -21742,9 +22033,6 @@ var CTX_MEMORY_ACTIONS = [
21742
22033
  ];
21743
22034
 
21744
22035
  // src/tools/ctx-memory/tools.ts
21745
- var SEMANTIC_WEIGHT = 0.7;
21746
- var FTS_WEIGHT = 0.3;
21747
- var SINGLE_SOURCE_PENALTY = 0.8;
21748
22036
  var MEMORY_CATEGORIES = new Set(CATEGORY_PRIORITY);
21749
22037
  function isMemoryCategory2(value) {
21750
22038
  return MEMORY_CATEGORIES.has(value);
@@ -21756,32 +22044,13 @@ function normalizeLimit(limit) {
21756
22044
  return Math.max(1, Math.floor(limit));
21757
22045
  }
21758
22046
  function getAllowedActions(deps) {
21759
- const allowed = deps.allowedActions?.length ? deps.allowedActions : [...CTX_MEMORY_ACTIONS];
22047
+ const allowed = deps.allowedActions?.length ? deps.allowedActions : [...CTX_MEMORY_DREAMER_ACTIONS];
21760
22048
  return [...allowed];
21761
22049
  }
21762
22050
  function normalizeCategory(category) {
21763
22051
  const trimmed = category?.trim();
21764
22052
  return trimmed ? trimmed : undefined;
21765
22053
  }
21766
- function normalizeCosineScore(score) {
21767
- if (!Number.isFinite(score)) {
21768
- return 0;
21769
- }
21770
- return Math.min(1, Math.max(0, score));
21771
- }
21772
- function formatSearchResults(query, results) {
21773
- if (results.length === 0) {
21774
- return `No memories found matching "${query}".`;
21775
- }
21776
- const noun = results.length === 1 ? "memory" : "memories";
21777
- const body = results.map((result, index) => `[${index + 1}] (score: ${result.score.toFixed(2)}) [${result.category}]
21778
- ${result.content}`).join(`
21779
-
21780
- `);
21781
- return `Found ${results.length} ${noun} matching "${query}":
21782
-
21783
- ${body}`;
21784
- }
21785
22054
  function formatMemoryList(memories) {
21786
22055
  if (memories.length === 0) {
21787
22056
  return "No active memories found.";
@@ -21839,77 +22108,6 @@ function filterByCategory(memories, category) {
21839
22108
  }
21840
22109
  return memories.filter((memory) => memory.category === category);
21841
22110
  }
21842
- async function getSemanticScores(deps, query, memories) {
21843
- const semanticScores = new Map;
21844
- if (!deps.embeddingEnabled || !isEmbeddingEnabled() || memories.length === 0) {
21845
- return semanticScores;
21846
- }
21847
- const queryEmbedding = await embedText(query);
21848
- if (!queryEmbedding) {
21849
- return semanticScores;
21850
- }
21851
- const embeddings = await ensureMemoryEmbeddings({
21852
- db: deps.db,
21853
- memories,
21854
- existingEmbeddings: loadAllEmbeddings(deps.db, deps.projectPath)
21855
- });
21856
- for (const memory of memories) {
21857
- const memoryEmbedding = embeddings.get(memory.id);
21858
- if (!memoryEmbedding) {
21859
- continue;
21860
- }
21861
- semanticScores.set(memory.id, normalizeCosineScore(cosineSimilarity(queryEmbedding, memoryEmbedding)));
21862
- }
21863
- return semanticScores;
21864
- }
21865
- function getFtsScores(deps, query, category, limit = DEFAULT_SEARCH_LIMIT2) {
21866
- try {
21867
- const matches = filterByCategory(searchMemoriesFTS(deps.db, deps.projectPath, query, limit), category);
21868
- return new Map(matches.map((memory, rank) => [memory.id, 1 / (rank + 1)]));
21869
- } catch {
21870
- return new Map;
21871
- }
21872
- }
21873
- function mergeResults(memories, semanticScores, ftsScores, limit) {
21874
- const memoryById = new Map(memories.map((memory) => [memory.id, memory]));
21875
- const candidateIds = new Set([...semanticScores.keys(), ...ftsScores.keys()]);
21876
- const results = [];
21877
- for (const id of candidateIds) {
21878
- const memory = memoryById.get(id);
21879
- if (!memory) {
21880
- continue;
21881
- }
21882
- const semanticScore = semanticScores.get(id);
21883
- const ftsScore = ftsScores.get(id);
21884
- let score = 0;
21885
- let source = "fts";
21886
- if (semanticScore !== undefined && ftsScore !== undefined) {
21887
- score = SEMANTIC_WEIGHT * semanticScore + FTS_WEIGHT * ftsScore;
21888
- source = "hybrid";
21889
- } else if (semanticScore !== undefined) {
21890
- score = semanticScore * SINGLE_SOURCE_PENALTY;
21891
- source = "semantic";
21892
- } else if (ftsScore !== undefined) {
21893
- score = ftsScore * SINGLE_SOURCE_PENALTY;
21894
- source = "fts";
21895
- }
21896
- if (score > 0) {
21897
- results.push({
21898
- id,
21899
- category: memory.category,
21900
- content: memory.content,
21901
- score,
21902
- source
21903
- });
21904
- }
21905
- }
21906
- return results.sort((left, right) => {
21907
- if (right.score !== left.score) {
21908
- return right.score - left.score;
21909
- }
21910
- return left.id - right.id;
21911
- }).slice(0, limit);
21912
- }
21913
22111
  function queueMemoryEmbedding(deps, memoryId, content) {
21914
22112
  (async () => {
21915
22113
  const embedding2 = await embedText(content);
@@ -21942,13 +22140,12 @@ function createCtxMemoryTool(deps) {
21942
22140
  return tool2({
21943
22141
  description: CTX_MEMORY_DESCRIPTION,
21944
22142
  args: {
21945
- action: tool2.schema.enum(CTX_MEMORY_ACTIONS).describe("Action to perform on memories"),
22143
+ action: tool2.schema.enum(CTX_MEMORY_DREAMER_ACTIONS).describe("Action to perform on memories"),
21946
22144
  content: tool2.schema.string().optional().describe("Memory content (required for write, update, merge)"),
21947
- category: tool2.schema.string().optional().describe("Memory category (required for write, optional filter for search/list, optional override for merge)"),
22145
+ category: tool2.schema.string().optional().describe("Memory category (required for write, optional filter for list, optional override for merge)"),
21948
22146
  id: tool2.schema.number().optional().describe("Memory ID (required for delete, update, archive)"),
21949
22147
  ids: tool2.schema.array(tool2.schema.number()).optional().describe("Memory IDs to merge (required for merge)"),
21950
- query: tool2.schema.string().optional().describe("Natural language search query for project memories (required for search)"),
21951
- limit: tool2.schema.number().optional().describe("Maximum results to return for search/list (default: 10)"),
22148
+ limit: tool2.schema.number().optional().describe("Maximum results to return for list (default: 10)"),
21952
22149
  reason: tool2.schema.string().optional().describe("Archive reason (optional for archive)")
21953
22150
  },
21954
22151
  async execute(args, toolContext) {
@@ -22103,30 +22300,6 @@ function createCtxMemoryTool(deps) {
22103
22300
  archiveMemory(deps.db, args.id, args.reason);
22104
22301
  return args.reason?.trim() ? `Archived memory [ID: ${args.id}] (${args.reason.trim()}).` : `Archived memory [ID: ${args.id}].`;
22105
22302
  }
22106
- if (args.action === "search") {
22107
- if (typeof args.query !== "string") {
22108
- return "Error: 'query' must be provided when action is 'search'.";
22109
- }
22110
- const query = args.query.trim();
22111
- if (!query) {
22112
- return "Error: 'query' must be provided when action is 'search'.";
22113
- }
22114
- const limit = normalizeLimit(args.limit);
22115
- const category = normalizeCategory(args.category);
22116
- const projectMemories = filterByCategory(getMemoriesByProject(deps.db, deps.projectPath), category);
22117
- const ftsLimit = Math.max(limit * 5, projectMemories.length, DEFAULT_SEARCH_LIMIT2);
22118
- const semanticScores = await getSemanticScores(deps, query, projectMemories);
22119
- const ftsScores = getFtsScores(deps, query, category, ftsLimit);
22120
- const results = mergeResults(projectMemories, semanticScores, ftsScores, limit);
22121
- if (results.length > 0) {
22122
- deps.db.transaction(() => {
22123
- for (const result of results) {
22124
- updateMemoryRetrievalCount(deps.db, result.id);
22125
- }
22126
- })();
22127
- }
22128
- return formatSearchResults(query, results);
22129
- }
22130
22303
  return "Error: Unknown action.";
22131
22304
  }
22132
22305
  });
@@ -22265,8 +22438,7 @@ function createCtxReduceTool(deps) {
22265
22438
  }
22266
22439
  let dropIds = [];
22267
22440
  try {
22268
- if (args.drop)
22269
- dropIds = parseRangeString(args.drop);
22441
+ dropIds = parseRangeString(args.drop);
22270
22442
  } catch (e) {
22271
22443
  return `Error: Invalid range syntax. ${e.message}`;
22272
22444
  }
@@ -22331,8 +22503,387 @@ function createCtxReduceTools(deps) {
22331
22503
  ctx_reduce: createCtxReduceTool(deps)
22332
22504
  };
22333
22505
  }
22334
- // src/plugin/normalize-tool-arg-schemas.ts
22506
+ // src/tools/ctx-search/constants.ts
22507
+ var CTX_SEARCH_TOOL_NAME = "ctx_search";
22508
+ var CTX_SEARCH_DESCRIPTION = "Search across project memories, session facts, and conversation history. Returns ranked results from all sources. Use message ordinals with ctx_expand to retrieve full conversation context around a result.";
22509
+ var DEFAULT_CTX_SEARCH_LIMIT = 10;
22510
+ // src/tools/ctx-search/tools.ts
22335
22511
  import { tool as tool5 } from "@opencode-ai/plugin";
22512
+
22513
+ // src/features/magic-context/search.ts
22514
+ var DEFAULT_UNIFIED_SEARCH_LIMIT = 10;
22515
+ var FTS_SEMANTIC_CANDIDATE_LIMIT = 50;
22516
+ var SEMANTIC_WEIGHT = 0.7;
22517
+ var FTS_WEIGHT = 0.3;
22518
+ var SINGLE_SOURCE_PENALTY = 0.8;
22519
+ var RESULT_PREVIEW_LIMIT = 220;
22520
+ var MEMORY_SOURCE_BOOST = 1.3;
22521
+ var FACT_SOURCE_BOOST = 1.15;
22522
+ var MESSAGE_SOURCE_BOOST = 1;
22523
+ var messageSearchStatements = new WeakMap;
22524
+ function normalizeLimit2(limit) {
22525
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
22526
+ return DEFAULT_UNIFIED_SEARCH_LIMIT;
22527
+ }
22528
+ return Math.max(1, Math.floor(limit));
22529
+ }
22530
+ function normalizeCosineScore(score) {
22531
+ if (!Number.isFinite(score)) {
22532
+ return 0;
22533
+ }
22534
+ return Math.min(1, Math.max(0, score));
22535
+ }
22536
+ function previewText(text) {
22537
+ const normalized = text.replace(/\s+/g, " ").trim();
22538
+ if (normalized.length <= RESULT_PREVIEW_LIMIT) {
22539
+ return normalized;
22540
+ }
22541
+ return `${normalized.slice(0, RESULT_PREVIEW_LIMIT - 1).trimEnd()}\u2026`;
22542
+ }
22543
+ function tokenizeQuery(query) {
22544
+ return Array.from(new Set(query.toLowerCase().split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0)));
22545
+ }
22546
+ function scoreTextMatch(content, query, extraText = "") {
22547
+ const tokens = tokenizeQuery(query);
22548
+ if (tokens.length === 0) {
22549
+ return 0;
22550
+ }
22551
+ const haystack = `${content} ${extraText}`.toLowerCase();
22552
+ const queryLower = query.trim().toLowerCase();
22553
+ let matchedTokens = 0;
22554
+ for (const token of tokens) {
22555
+ if (haystack.includes(token)) {
22556
+ matchedTokens++;
22557
+ }
22558
+ }
22559
+ if (matchedTokens === 0) {
22560
+ return 0;
22561
+ }
22562
+ let score = matchedTokens / tokens.length;
22563
+ if (queryLower.length > 0 && haystack.includes(queryLower)) {
22564
+ score += 0.35;
22565
+ }
22566
+ return Math.min(1, score);
22567
+ }
22568
+ function getMessageSearchStatement(db) {
22569
+ let stmt = messageSearchStatements.get(db);
22570
+ if (!stmt) {
22571
+ stmt = db.prepare("SELECT message_ordinal AS messageOrdinal, message_id AS messageId, role, content FROM message_history_fts WHERE session_id = ? AND message_history_fts MATCH ? ORDER BY bm25(message_history_fts), CAST(message_ordinal AS INTEGER) ASC LIMIT ?");
22572
+ messageSearchStatements.set(db, stmt);
22573
+ }
22574
+ return stmt;
22575
+ }
22576
+ function getMessageOrdinal(value) {
22577
+ if (typeof value === "number" && Number.isFinite(value)) {
22578
+ return value;
22579
+ }
22580
+ if (typeof value === "string" && value.trim().length > 0) {
22581
+ const parsed = Number.parseInt(value, 10);
22582
+ return Number.isFinite(parsed) ? parsed : null;
22583
+ }
22584
+ return null;
22585
+ }
22586
+ async function getSemanticScores(args) {
22587
+ const semanticScores = new Map;
22588
+ if (!args.embeddingEnabled || !args.isEmbeddingRuntimeEnabled() || args.memories.length === 0) {
22589
+ return semanticScores;
22590
+ }
22591
+ const queryEmbedding = await args.embedQuery(args.query);
22592
+ if (!queryEmbedding) {
22593
+ return semanticScores;
22594
+ }
22595
+ const cachedEmbeddings = getProjectEmbeddings(args.db, args.projectPath);
22596
+ const embeddings = await ensureMemoryEmbeddings({
22597
+ db: args.db,
22598
+ memories: args.memories,
22599
+ existingEmbeddings: cachedEmbeddings
22600
+ });
22601
+ for (const memory of args.memories) {
22602
+ const memoryEmbedding = embeddings.get(memory.id);
22603
+ if (!memoryEmbedding) {
22604
+ continue;
22605
+ }
22606
+ semanticScores.set(memory.id, normalizeCosineScore(cosineSimilarity(queryEmbedding, memoryEmbedding)));
22607
+ }
22608
+ return semanticScores;
22609
+ }
22610
+ function getFtsMatches(args) {
22611
+ try {
22612
+ return searchMemoriesFTS(args.db, args.projectPath, args.query, args.limit);
22613
+ } catch {
22614
+ return [];
22615
+ }
22616
+ }
22617
+ function getFtsScores(matches) {
22618
+ return new Map(matches.map((memory, rank) => [memory.id, 1 / (rank + 1)]));
22619
+ }
22620
+ function selectSemanticCandidates(args) {
22621
+ if (args.ftsMatches.length === 0) {
22622
+ return args.memories;
22623
+ }
22624
+ const candidateIds = new Set(args.ftsMatches.map((memory) => memory.id));
22625
+ const cachedEmbeddings = peekProjectEmbeddings(args.projectPath);
22626
+ if (cachedEmbeddings) {
22627
+ for (const memoryId of cachedEmbeddings.keys()) {
22628
+ candidateIds.add(memoryId);
22629
+ }
22630
+ }
22631
+ return args.memories.filter((memory) => candidateIds.has(memory.id));
22632
+ }
22633
+ function mergeMemoryResults(args) {
22634
+ const memoryById = new Map(args.memories.map((memory) => [memory.id, memory]));
22635
+ const candidateIds = new Set([...args.semanticScores.keys(), ...args.ftsScores.keys()]);
22636
+ const results = [];
22637
+ for (const id of candidateIds) {
22638
+ const memory = memoryById.get(id);
22639
+ if (!memory) {
22640
+ continue;
22641
+ }
22642
+ const semanticScore = args.semanticScores.get(id);
22643
+ const ftsScore = args.ftsScores.get(id);
22644
+ let score = 0;
22645
+ let matchType = "fts";
22646
+ if (semanticScore !== undefined && ftsScore !== undefined) {
22647
+ score = SEMANTIC_WEIGHT * semanticScore + FTS_WEIGHT * ftsScore;
22648
+ matchType = "hybrid";
22649
+ } else if (semanticScore !== undefined) {
22650
+ score = semanticScore * SINGLE_SOURCE_PENALTY;
22651
+ matchType = "semantic";
22652
+ } else if (ftsScore !== undefined) {
22653
+ score = ftsScore * SINGLE_SOURCE_PENALTY;
22654
+ matchType = "fts";
22655
+ }
22656
+ if (score <= 0) {
22657
+ continue;
22658
+ }
22659
+ results.push({
22660
+ source: "memory",
22661
+ content: previewText(memory.content),
22662
+ score,
22663
+ memoryId: memory.id,
22664
+ category: memory.category,
22665
+ matchType
22666
+ });
22667
+ }
22668
+ return results.sort((left, right) => {
22669
+ if (right.score !== left.score) {
22670
+ return right.score - left.score;
22671
+ }
22672
+ return left.memoryId - right.memoryId;
22673
+ }).slice(0, args.limit);
22674
+ }
22675
+ async function searchMemories(args) {
22676
+ if (!args.memoryEnabled) {
22677
+ return [];
22678
+ }
22679
+ const memories = getMemoriesByProject(args.db, args.projectPath);
22680
+ if (memories.length === 0) {
22681
+ return [];
22682
+ }
22683
+ const ftsMatches = getFtsMatches({
22684
+ db: args.db,
22685
+ projectPath: args.projectPath,
22686
+ query: args.query,
22687
+ limit: FTS_SEMANTIC_CANDIDATE_LIMIT
22688
+ });
22689
+ const ftsScores = getFtsScores(ftsMatches);
22690
+ const semanticCandidates = selectSemanticCandidates({
22691
+ memories,
22692
+ projectPath: args.projectPath,
22693
+ ftsMatches
22694
+ });
22695
+ const semanticScores = await getSemanticScores({
22696
+ db: args.db,
22697
+ projectPath: args.projectPath,
22698
+ query: args.query,
22699
+ memories: semanticCandidates,
22700
+ embeddingEnabled: args.embeddingEnabled,
22701
+ embedQuery: args.embedQuery,
22702
+ isEmbeddingRuntimeEnabled: args.isEmbeddingRuntimeEnabled
22703
+ });
22704
+ return mergeMemoryResults({
22705
+ memories,
22706
+ semanticScores,
22707
+ ftsScores,
22708
+ limit: args.limit
22709
+ });
22710
+ }
22711
+ function searchFacts(args) {
22712
+ return getSessionFacts(args.db, args.sessionId).map((fact) => ({
22713
+ fact,
22714
+ score: scoreTextMatch(fact.content, args.query, fact.category)
22715
+ })).filter((candidate) => candidate.score > 0).sort((left, right) => {
22716
+ if (right.score !== left.score) {
22717
+ return right.score - left.score;
22718
+ }
22719
+ return left.fact.id - right.fact.id;
22720
+ }).slice(0, args.limit).map(({ fact, score }) => ({
22721
+ source: "fact",
22722
+ content: previewText(fact.content),
22723
+ score,
22724
+ factId: fact.id,
22725
+ factCategory: fact.category
22726
+ }));
22727
+ }
22728
+ function searchMessages(args) {
22729
+ ensureMessagesIndexed(args.db, args.sessionId, args.readMessages);
22730
+ const sanitizedQuery = sanitizeFtsQuery(args.query.trim());
22731
+ if (sanitizedQuery.length === 0) {
22732
+ return [];
22733
+ }
22734
+ const rows = getMessageSearchStatement(args.db).all(args.sessionId, sanitizedQuery, args.limit).map((row) => row);
22735
+ return rows.map((row, rank) => {
22736
+ const messageOrdinal = getMessageOrdinal(row.messageOrdinal);
22737
+ if (messageOrdinal === null || typeof row.messageId !== "string" || typeof row.role !== "string" || typeof row.content !== "string") {
22738
+ return null;
22739
+ }
22740
+ return {
22741
+ source: "message",
22742
+ content: previewText(row.content),
22743
+ score: 1 / (rank + 1),
22744
+ messageOrdinal,
22745
+ messageId: row.messageId,
22746
+ role: row.role
22747
+ };
22748
+ }).filter((result) => result !== null);
22749
+ }
22750
+ function getSourceBoost(result) {
22751
+ switch (result.source) {
22752
+ case "memory":
22753
+ return MEMORY_SOURCE_BOOST;
22754
+ case "fact":
22755
+ return FACT_SOURCE_BOOST;
22756
+ case "message":
22757
+ return MESSAGE_SOURCE_BOOST;
22758
+ }
22759
+ }
22760
+ function compareUnifiedResults(left, right) {
22761
+ const leftEffective = left.score * getSourceBoost(left);
22762
+ const rightEffective = right.score * getSourceBoost(right);
22763
+ if (rightEffective !== leftEffective) {
22764
+ return rightEffective - leftEffective;
22765
+ }
22766
+ if (left.source === "memory" && right.source === "memory") {
22767
+ return left.memoryId - right.memoryId;
22768
+ }
22769
+ if (left.source === "fact" && right.source === "fact") {
22770
+ return left.factId - right.factId;
22771
+ }
22772
+ if (left.source === "message" && right.source === "message") {
22773
+ return left.messageOrdinal - right.messageOrdinal;
22774
+ }
22775
+ return 0;
22776
+ }
22777
+ async function unifiedSearch(db, sessionId, projectPath, query, options = {}) {
22778
+ const trimmedQuery = query.trim();
22779
+ if (trimmedQuery.length === 0) {
22780
+ return [];
22781
+ }
22782
+ const limit = normalizeLimit2(options.limit);
22783
+ const tierLimit = Math.max(limit * 3, DEFAULT_UNIFIED_SEARCH_LIMIT);
22784
+ const [memoryResults, factResults, messageResults] = await Promise.all([
22785
+ searchMemories({
22786
+ db,
22787
+ projectPath,
22788
+ query: trimmedQuery,
22789
+ limit: tierLimit,
22790
+ memoryEnabled: options.memoryEnabled ?? true,
22791
+ embeddingEnabled: options.embeddingEnabled ?? true,
22792
+ embedQuery: options.embedQuery ?? embedText,
22793
+ isEmbeddingRuntimeEnabled: options.isEmbeddingRuntimeEnabled ?? isEmbeddingEnabled
22794
+ }),
22795
+ Promise.resolve(searchFacts({ db, sessionId, query: trimmedQuery, limit: tierLimit })),
22796
+ Promise.resolve(searchMessages({
22797
+ db,
22798
+ sessionId,
22799
+ query: trimmedQuery,
22800
+ limit: tierLimit,
22801
+ readMessages: options.readMessages ?? readRawSessionMessages
22802
+ }))
22803
+ ]);
22804
+ const results = [...memoryResults, ...factResults, ...messageResults].sort(compareUnifiedResults).slice(0, limit);
22805
+ const memoryIds = results.filter((result) => result.source === "memory").map((result) => result.memoryId);
22806
+ if (memoryIds.length > 0) {
22807
+ db.transaction(() => {
22808
+ for (const memoryId of memoryIds) {
22809
+ updateMemoryRetrievalCount(db, memoryId);
22810
+ }
22811
+ })();
22812
+ }
22813
+ return results;
22814
+ }
22815
+
22816
+ // src/tools/ctx-search/tools.ts
22817
+ function normalizeLimit3(limit) {
22818
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
22819
+ return DEFAULT_CTX_SEARCH_LIMIT;
22820
+ }
22821
+ return Math.max(1, Math.floor(limit));
22822
+ }
22823
+ function formatResult(result, index) {
22824
+ if (result.source === "memory") {
22825
+ return [
22826
+ `[${index}] [memory] score=${result.score.toFixed(2)} id=${result.memoryId} category=${result.category} match=${result.matchType}`,
22827
+ result.content
22828
+ ].join(`
22829
+ `);
22830
+ }
22831
+ if (result.source === "fact") {
22832
+ return [
22833
+ `[${index}] [fact] score=${result.score.toFixed(2)} category=${result.factCategory} id=${result.factId}`,
22834
+ result.content
22835
+ ].join(`
22836
+ `);
22837
+ }
22838
+ const expandStart = Math.max(1, result.messageOrdinal - 3);
22839
+ const expandEnd = result.messageOrdinal + 3;
22840
+ return [
22841
+ `[${index}] [message] score=${result.score.toFixed(2)} ordinal=${result.messageOrdinal} role=${result.role}`,
22842
+ result.content,
22843
+ `Expand with ctx_expand(start=${expandStart}, end=${expandEnd}).`
22844
+ ].join(`
22845
+ `);
22846
+ }
22847
+ function formatSearchResults(query, results) {
22848
+ if (results.length === 0) {
22849
+ return `No results found for "${query}" across memories, session facts, or message history.`;
22850
+ }
22851
+ const body = results.map((result, index) => formatResult(result, index + 1)).join(`
22852
+
22853
+ `);
22854
+ return `Found ${results.length} result${results.length === 1 ? "" : "s"} for "${query}":
22855
+
22856
+ ${body}`;
22857
+ }
22858
+ function createCtxSearchTool(deps) {
22859
+ return tool5({
22860
+ description: CTX_SEARCH_DESCRIPTION,
22861
+ args: {
22862
+ query: tool5.schema.string().describe("Search query across memories, facts, and conversation history."),
22863
+ limit: tool5.schema.number().optional().describe("Maximum results to return (default: 10)")
22864
+ },
22865
+ async execute(args, toolContext) {
22866
+ const query = args.query?.trim();
22867
+ if (!query) {
22868
+ return "Error: 'query' is required.";
22869
+ }
22870
+ const results = await unifiedSearch(deps.db, toolContext.sessionID, deps.projectPath, query, {
22871
+ limit: normalizeLimit3(args.limit),
22872
+ memoryEnabled: deps.memoryEnabled,
22873
+ embeddingEnabled: deps.embeddingEnabled,
22874
+ readMessages: deps.readMessages
22875
+ });
22876
+ return formatSearchResults(query, results);
22877
+ }
22878
+ });
22879
+ }
22880
+ function createCtxSearchTools(deps) {
22881
+ return {
22882
+ [CTX_SEARCH_TOOL_NAME]: createCtxSearchTool(deps)
22883
+ };
22884
+ }
22885
+ // src/plugin/normalize-tool-arg-schemas.ts
22886
+ import { tool as tool6 } from "@opencode-ai/plugin";
22336
22887
  function stripRootJsonSchemaFields(jsonSchema) {
22337
22888
  const { $schema: _schema, ...rest } = jsonSchema;
22338
22889
  return rest;
@@ -22345,7 +22896,7 @@ function attachJsonSchemaOverride(schema) {
22345
22896
  const originalOverride = schema._zod.toJSONSchema;
22346
22897
  delete schema._zod.toJSONSchema;
22347
22898
  try {
22348
- return stripRootJsonSchemaFields(tool5.schema.toJSONSchema(schema));
22899
+ return stripRootJsonSchemaFields(tool6.schema.toJSONSchema(schema));
22349
22900
  } finally {
22350
22901
  schema._zod.toJSONSchema = originalOverride;
22351
22902
  }
@@ -22386,20 +22937,27 @@ function createToolRegistry(args) {
22386
22937
  console.warn(`[magic-context] embedding model changed from ${storedModelId} to ${currentModelId}; cleared embeddings for project ${projectPath}`);
22387
22938
  }
22388
22939
  }
22940
+ const ctxReduceEnabled = pluginConfig.ctx_reduce_enabled !== false;
22389
22941
  const allTools = {
22390
- ...createCtxReduceTools({
22942
+ ...ctxReduceEnabled ? createCtxReduceTools({
22391
22943
  db,
22392
22944
  protectedTags: pluginConfig.protected_tags ?? DEFAULT_PROTECTED_TAGS
22393
- }),
22945
+ }) : {},
22394
22946
  ...createCtxExpandTools(),
22395
22947
  ...createCtxNoteTools({ db }),
22948
+ ...createCtxSearchTools({
22949
+ db,
22950
+ projectPath,
22951
+ memoryEnabled,
22952
+ embeddingEnabled: embeddingConfig2.provider !== "off"
22953
+ }),
22396
22954
  ...memoryEnabled ? {
22397
22955
  ...createCtxMemoryTools({
22398
22956
  db,
22399
22957
  projectPath,
22400
22958
  memoryEnabled: true,
22401
22959
  embeddingEnabled: embeddingConfig2.provider !== "off",
22402
- allowedActions: ["write", "delete", "search"]
22960
+ allowedActions: ["write", "delete"]
22403
22961
  })
22404
22962
  } : {}
22405
22963
  };
@@ -22499,12 +23057,18 @@ var plugin = async (ctx) => {
22499
23057
  ctx,
22500
23058
  pluginConfig
22501
23059
  });
22502
- const tools4 = createToolRegistry({
23060
+ const tools5 = createToolRegistry({
22503
23061
  ctx,
22504
23062
  pluginConfig
22505
23063
  });
23064
+ if (pluginConfig.dreamer) {
23065
+ startDreamScheduleTimer({
23066
+ client: ctx.client,
23067
+ dreamerConfig: pluginConfig.dreamer
23068
+ });
23069
+ }
22506
23070
  return {
22507
- tool: tools4,
23071
+ tool: tools5,
22508
23072
  event: createEventHandler({ magicContext: hooks.magicContext }),
22509
23073
  "experimental.chat.messages.transform": createMessagesTransformHandler({
22510
23074
  magicContext: hooks.magicContext