@cortexkit/opencode-magic-context 0.1.3 → 0.2.0

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 (50) 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 -0
  5. package/dist/config/schema/magic-context.d.ts.map +1 -1
  6. package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
  7. package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
  8. package/dist/features/magic-context/index.d.ts +1 -0
  9. package/dist/features/magic-context/index.d.ts.map +1 -1
  10. package/dist/features/magic-context/memory/storage-memory-fts.d.ts +8 -0
  11. package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
  12. package/dist/features/magic-context/message-index.d.ts +4 -0
  13. package/dist/features/magic-context/message-index.d.ts.map +1 -0
  14. package/dist/features/magic-context/search.d.ts +36 -0
  15. package/dist/features/magic-context/search.d.ts.map +1 -0
  16. package/dist/features/magic-context/sidekick/agent.d.ts +1 -1
  17. package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
  18. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  19. package/dist/features/magic-context/storage.d.ts +1 -1
  20. package/dist/features/magic-context/storage.d.ts.map +1 -1
  21. package/dist/hooks/magic-context/hook-handlers.d.ts +2 -0
  22. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  23. package/dist/hooks/magic-context/hook.d.ts +1 -0
  24. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  25. package/dist/hooks/magic-context/read-session-chunk.d.ts +5 -0
  26. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/system-prompt-hash.d.ts +1 -0
  28. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1715 -1305
  31. package/dist/plugin/dream-timer.d.ts +14 -0
  32. package/dist/plugin/dream-timer.d.ts.map +1 -0
  33. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  34. package/dist/plugin/tool-registry.d.ts.map +1 -1
  35. package/dist/tools/ctx-memory/constants.d.ts +1 -1
  36. package/dist/tools/ctx-memory/constants.d.ts.map +1 -1
  37. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  38. package/dist/tools/ctx-memory/types.d.ts +3 -10
  39. package/dist/tools/ctx-memory/types.d.ts.map +1 -1
  40. package/dist/tools/ctx-search/constants.d.ts +4 -0
  41. package/dist/tools/ctx-search/constants.d.ts.map +1 -0
  42. package/dist/tools/ctx-search/index.d.ts +4 -0
  43. package/dist/tools/ctx-search/index.d.ts.map +1 -0
  44. package/dist/tools/ctx-search/tools.d.ts +4 -0
  45. package/dist/tools/ctx-search/tools.d.ts.map +1 -0
  46. package/dist/tools/ctx-search/types.d.ts +19 -0
  47. package/dist/tools/ctx-search/types.d.ts.map +1 -0
  48. package/dist/tools/index.d.ts +1 -0
  49. package/dist/tools/index.d.ts.map +1 -1
  50. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -13799,6 +13799,7 @@ var EmbeddingConfigSchema = BaseEmbeddingConfigSchema.transform((data) => {
13799
13799
  });
13800
13800
  var MagicContextConfigSchema = exports_external.object({
13801
13801
  enabled: exports_external.boolean().default(false),
13802
+ ctx_reduce_enabled: exports_external.boolean().default(true),
13802
13803
  historian: AgentOverrideConfigSchema.optional(),
13803
13804
  dreamer: DreamerConfigSchema.optional(),
13804
13805
  cache_ttl: exports_external.union([exports_external.string(), exports_external.object({ default: exports_external.string() }).catchall(exports_external.string())]).default("5m"),
@@ -13951,7 +13952,6 @@ You run during scheduled dream windows to maintain a project's cross-session mem
13951
13952
 
13952
13953
  **Memory operations** (ctx_memory with extended dreamer actions):
13953
13954
  - \`action="list"\` \u2014 browse all active memories, optionally filter by category
13954
- - \`action="search", query="..."\` \u2014 semantic search across memories
13955
13955
  - \`action="update", id=N, content="..."\` \u2014 rewrite a memory's content
13956
13956
  - \`action="merge", ids=[N,M,...], content="...", category="..."\` \u2014 consolidate duplicates into one canonical memory
13957
13957
  - \`action="archive", id=N, reason="..."\` \u2014 archive a stale memory with provenance
@@ -14033,7 +14033,7 @@ Check verifiable memories against actual repository state. Update stale wording,
14033
14033
  ### Verification examples
14034
14034
  - Memory: "compartment_token_budget defaults to 20000" \u2192 grep schema for \`compartment_token_budget\`, check \`.default(...)\`
14035
14035
  - 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
14036
+ - Memory: "ctx_search searches memories, facts, and history" \u2192 grep for ctx_search tool definition and unified search implementation
14037
14037
 
14038
14038
  ### Success criteria
14039
14039
  - All CONFIG_DEFAULTS memories match actual schema defaults.
@@ -14549,12 +14549,12 @@ function extractLatestAssistantText(messages) {
14549
14549
  // src/features/magic-context/sidekick/agent.ts
14550
14550
  var SIDEKICK_SYSTEM_PROMPT = `You are Sidekick, a focused memory-retrieval subagent for an AI coding assistant.
14551
14551
 
14552
- Your job is to search project memories and return a concise augmentation for the user's prompt.
14552
+ Your job is to search project memories, session facts, and conversation history and return a concise augmentation for the user's prompt.
14553
14553
 
14554
14554
  Rules:
14555
- - Use ctx_memory(action="search", query="...") to look up relevant memories before answering.
14555
+ - Use ctx_search(query="...") to look up relevant memories, facts, and history before answering.
14556
14556
  - Run targeted searches only; prefer 1-3 precise queries.
14557
- - Return only memories that materially help with the user's prompt.
14557
+ - Return only findings that materially help with the user's prompt.
14558
14558
  - If nothing useful is found, respond with exactly: No relevant memories found.
14559
14559
  - Keep the response focused and concise.
14560
14560
  - Do not invent facts or speculate beyond what memories support.`;
@@ -14945,1276 +14945,1323 @@ function buildCompartmentAgentPrompt(existingState, inputSource) {
14945
14945
  `);
14946
14946
  }
14947
14947
 
14948
- // src/plugin/event.ts
14949
- function createEventHandler(args) {
14950
- return async (input) => {
14951
- await args.magicContext?.event?.(input);
14952
- };
14948
+ // src/features/magic-context/dreamer/storage-dream-state.ts
14949
+ var getDreamStateStatements = new WeakMap;
14950
+ var setDreamStateStatements = new WeakMap;
14951
+ var deleteDreamStateStatements = new WeakMap;
14952
+ function getGetDreamStateStatement(db) {
14953
+ let stmt = getDreamStateStatements.get(db);
14954
+ if (!stmt) {
14955
+ stmt = db.prepare("SELECT value FROM dream_state WHERE key = ?");
14956
+ getDreamStateStatements.set(db, stmt);
14957
+ }
14958
+ return stmt;
14953
14959
  }
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");
14960
+ function getSetDreamStateStatement(db) {
14961
+ let stmt = setDreamStateStatements.get(db);
14962
+ if (!stmt) {
14963
+ stmt = db.prepare("INSERT INTO dream_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value");
14964
+ setDreamStateStatements.set(db, stmt);
14965
+ }
14966
+ return stmt;
14964
14967
  }
14965
- function getOpenCodeStorageDir() {
14966
- return path2.join(getDataDir(), "opencode", "storage");
14968
+ function getDeleteDreamStateStatement(db) {
14969
+ let stmt = deleteDreamStateStatements.get(db);
14970
+ if (!stmt) {
14971
+ stmt = db.prepare("DELETE FROM dream_state WHERE key = ?");
14972
+ deleteDreamStateStatements.set(db, stmt);
14973
+ }
14974
+ return stmt;
14967
14975
  }
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") };
14976
+ function getDreamState(db, key) {
14977
+ const row = getGetDreamStateStatement(db).get(key);
14978
+ return typeof row?.value === "string" ? row.value : null;
14979
+ }
14980
+ function setDreamState(db, key, value) {
14981
+ getSetDreamStateStatement(db).run(key, value);
14982
+ }
14983
+ function deleteDreamState(db, key) {
14984
+ getDeleteDreamStateStatement(db).run(key);
14977
14985
  }
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
-
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
- );
15009
-
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
- );
15023
-
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
14986
 
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");
14987
+ // src/features/magic-context/dreamer/lease.ts
14988
+ var LEASE_HOLDER_KEY = "dreaming_lease_holder";
14989
+ var LEASE_HEARTBEAT_KEY = "dreaming_lease_heartbeat";
14990
+ var LEASE_EXPIRY_KEY = "dreaming_lease_expiry";
14991
+ var LEASE_DURATION_MS = 2 * 60 * 1000;
14992
+ function getLeaseExpiry(db) {
14993
+ const value = getDreamState(db, LEASE_EXPIRY_KEY);
14994
+ if (!value) {
14995
+ return null;
14996
+ }
14997
+ const expiry = Number(value);
14998
+ return Number.isFinite(expiry) ? expiry : null;
14999
+ }
15000
+ function isLeaseActive(db) {
15001
+ const expiry = getLeaseExpiry(db);
15002
+ return expiry !== null && expiry > Date.now();
15003
+ }
15004
+ function getLeaseHolder(db) {
15005
+ return getDreamState(db, LEASE_HOLDER_KEY);
15006
+ }
15007
+ function acquireLease(db, holderId) {
15008
+ return db.transaction(() => {
15009
+ if (isLeaseActive(db)) {
15010
+ const existingHolder = getLeaseHolder(db);
15011
+ if (existingHolder && existingHolder !== holderId) {
15012
+ return false;
15013
+ }
15014
+ }
15015
+ const now = Date.now();
15016
+ setDreamState(db, LEASE_HOLDER_KEY, holderId);
15017
+ setDreamState(db, LEASE_HEARTBEAT_KEY, String(now));
15018
+ setDreamState(db, LEASE_EXPIRY_KEY, String(now + LEASE_DURATION_MS));
15019
+ return true;
15020
+ })();
15182
15021
  }
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}`);
15022
+ function renewLease(db, holderId) {
15023
+ return db.transaction(() => {
15024
+ if (getLeaseHolder(db) !== holderId || !isLeaseActive(db)) {
15025
+ return false;
15026
+ }
15027
+ const now = Date.now();
15028
+ setDreamState(db, LEASE_HEARTBEAT_KEY, String(now));
15029
+ setDreamState(db, LEASE_EXPIRY_KEY, String(now + LEASE_DURATION_MS));
15030
+ return true;
15031
+ })();
15192
15032
  }
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)}`);
15200
- }
15033
+ function releaseLease(db, holderId) {
15034
+ db.transaction(() => {
15035
+ if (getLeaseHolder(db) !== holderId) {
15036
+ return;
15037
+ }
15038
+ deleteDreamState(db, LEASE_HOLDER_KEY);
15039
+ deleteDreamState(db, LEASE_HEARTBEAT_KEY);
15040
+ deleteDreamState(db, LEASE_EXPIRY_KEY);
15041
+ })();
15201
15042
  }
15202
- function openDatabase() {
15203
- try {
15204
- const { dbDir, dbPath } = resolveDatabasePath();
15205
- const existing = databases.get(dbPath);
15043
+ // src/features/magic-context/dreamer/queue.ts
15044
+ function enqueueDream(db, projectIdentity, reason) {
15045
+ const now = Date.now();
15046
+ return db.transaction(() => {
15047
+ const existing = db.query("SELECT id FROM dream_queue WHERE project_path = ?").get(projectIdentity);
15206
15048
  if (existing) {
15207
- if (!persistenceByDatabase.has(existing)) {
15208
- persistenceByDatabase.set(existing, true);
15049
+ return null;
15050
+ }
15051
+ const result = db.prepare("INSERT INTO dream_queue (project_path, reason, enqueued_at) VALUES (?, ?, ?)").run(projectIdentity, reason, now);
15052
+ return {
15053
+ id: Number(result.lastInsertRowid),
15054
+ projectIdentity,
15055
+ reason,
15056
+ enqueuedAt: now,
15057
+ startedAt: null
15058
+ };
15059
+ })();
15060
+ }
15061
+ function peekQueue(db) {
15062
+ 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();
15063
+ if (!row)
15064
+ return null;
15065
+ return {
15066
+ id: row.id,
15067
+ projectIdentity: row.project_path,
15068
+ reason: row.reason,
15069
+ enqueuedAt: row.enqueued_at,
15070
+ startedAt: null
15071
+ };
15072
+ }
15073
+ function dequeueNext(db) {
15074
+ const now = Date.now();
15075
+ return db.transaction(() => {
15076
+ const entry = peekQueue(db);
15077
+ if (!entry)
15078
+ return null;
15079
+ const result = db.prepare("UPDATE dream_queue SET started_at = ? WHERE id = ? AND started_at IS NULL").run(now, entry.id);
15080
+ if (result.changes === 0)
15081
+ return null;
15082
+ return { ...entry, startedAt: now };
15083
+ })();
15084
+ }
15085
+ function removeDreamEntry(db, id) {
15086
+ db.prepare("DELETE FROM dream_queue WHERE id = ?").run(id);
15087
+ }
15088
+ function resetDreamEntry(db, id) {
15089
+ db.prepare("UPDATE dream_queue SET started_at = NULL, retry_count = COALESCE(retry_count, 0) + 1 WHERE id = ?").run(id);
15090
+ }
15091
+ function getEntryRetryCount(db, id) {
15092
+ const row = db.query("SELECT retry_count FROM dream_queue WHERE id = ?").get(id);
15093
+ return row?.retry_count ?? 0;
15094
+ }
15095
+ function clearStaleEntries(db, maxAgeMs) {
15096
+ const cutoff = Date.now() - maxAgeMs;
15097
+ const result = db.prepare("DELETE FROM dream_queue WHERE started_at IS NOT NULL AND started_at < ?").run(cutoff);
15098
+ return result.changes;
15099
+ }
15100
+ // src/features/magic-context/dreamer/runner.ts
15101
+ import { existsSync as existsSync3 } from "fs";
15102
+ import { join as join3 } from "path";
15103
+ var dreamProjectDirectories = new Map;
15104
+ function registerDreamProjectDirectory(projectIdentity, directory) {
15105
+ dreamProjectDirectories.set(projectIdentity, directory);
15106
+ }
15107
+ function resolveDreamSessionDirectory(projectIdentity) {
15108
+ return dreamProjectDirectories.get(projectIdentity) ?? projectIdentity;
15109
+ }
15110
+ async function runDream(args) {
15111
+ const holderId = crypto.randomUUID();
15112
+ const startedAt = Date.now();
15113
+ const result = {
15114
+ startedAt,
15115
+ finishedAt: startedAt,
15116
+ holderId,
15117
+ tasks: []
15118
+ };
15119
+ log(`[dreamer] starting dream run: ${args.tasks.length} tasks, timeout=${args.taskTimeoutMinutes}m, maxRuntime=${args.maxRuntimeMinutes}m, project=${args.projectIdentity}`);
15120
+ if (!acquireLease(args.db, holderId)) {
15121
+ const currentHolder = getLeaseHolder(args.db) ?? "another holder";
15122
+ log(`[dreamer] lease acquisition failed \u2014 already held by ${currentHolder}`);
15123
+ result.tasks.push({
15124
+ name: "lease",
15125
+ durationMs: 0,
15126
+ result: null,
15127
+ error: `Dream lease is already held by ${currentHolder}`
15128
+ });
15129
+ result.finishedAt = Date.now();
15130
+ return result;
15131
+ }
15132
+ log(`[dreamer] lease acquired: ${holderId}`);
15133
+ let parentSessionId = args.parentSessionId;
15134
+ if (!parentSessionId) {
15135
+ try {
15136
+ const sessionDir = args.sessionDirectory ?? args.projectIdentity;
15137
+ const listResponse = await args.client.session.list({
15138
+ query: { directory: sessionDir }
15139
+ });
15140
+ const sessions = normalizeSDKResponse(listResponse, [], {
15141
+ preferResponseOnMissingData: true
15142
+ });
15143
+ parentSessionId = sessions?.find((s) => typeof s?.id === "string")?.id;
15144
+ if (parentSessionId) {
15145
+ log(`[dreamer] resolved parent session: ${parentSessionId}`);
15209
15146
  }
15210
- return existing;
15147
+ } catch {
15148
+ log("[dreamer] could not resolve parent session \u2014 child sessions will be visible in UI");
15211
15149
  }
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);
15150
+ }
15151
+ const deadline = startedAt + args.maxRuntimeMinutes * 60 * 1000;
15152
+ const lastDreamAt = getDreamState(args.db, `last_dream_at:${args.projectIdentity}`) ?? getDreamState(args.db, "last_dream_at");
15153
+ log(`[dreamer] last dream at: ${lastDreamAt ?? "never"} (project=${args.projectIdentity})`);
15154
+ try {
15155
+ for (const taskName of args.tasks) {
15156
+ if (Date.now() > deadline) {
15157
+ log(`[dreamer] deadline reached, stopping after ${result.tasks.length} tasks`);
15158
+ break;
15159
+ }
15160
+ log(`[dreamer] starting task: ${taskName}`);
15161
+ const taskStartedAt = Date.now();
15162
+ let agentSessionId = null;
15163
+ const taskAbortController = new AbortController;
15164
+ const leaseRenewalInterval = setInterval(() => {
15165
+ try {
15166
+ if (!renewLease(args.db, holderId)) {
15167
+ log(`[dreamer] task ${taskName}: lease renewal failed \u2014 aborting LLM call`);
15168
+ taskAbortController.abort();
15169
+ }
15170
+ } catch (err) {
15171
+ log(`[dreamer] task ${taskName}: lease renewal threw \u2014 aborting LLM call: ${err}`);
15172
+ taskAbortController.abort();
15173
+ }
15174
+ }, 60000);
15175
+ try {
15176
+ const docsDir = args.sessionDirectory ?? args.projectIdentity;
15177
+ const existingDocs = taskName === "maintain-docs" ? {
15178
+ architecture: existsSync3(join3(docsDir, "ARCHITECTURE.md")),
15179
+ structure: existsSync3(join3(docsDir, "STRUCTURE.md"))
15180
+ } : undefined;
15181
+ const taskPrompt = buildDreamTaskPrompt(taskName, {
15182
+ projectPath: args.projectIdentity,
15183
+ lastDreamAt,
15184
+ existingDocs
15185
+ });
15186
+ const createResponse = await args.client.session.create({
15187
+ body: {
15188
+ ...parentSessionId ? { parentID: parentSessionId } : {},
15189
+ title: `magic-context-dream-${taskName}`
15190
+ },
15191
+ query: { directory: args.sessionDirectory ?? args.projectIdentity }
15192
+ });
15193
+ const createdSession = normalizeSDKResponse(createResponse, null, { preferResponseOnMissingData: true });
15194
+ agentSessionId = typeof createdSession?.id === "string" ? createdSession.id : null;
15195
+ if (!agentSessionId) {
15196
+ throw new Error("Dreamer could not create its child session.");
15197
+ }
15198
+ log(`[dreamer] task ${taskName}: child session created ${agentSessionId}`);
15199
+ await promptSyncWithModelSuggestionRetry(args.client, {
15200
+ path: { id: agentSessionId },
15201
+ query: { directory: args.sessionDirectory ?? args.projectIdentity },
15202
+ body: {
15203
+ agent: DREAMER_AGENT,
15204
+ system: DREAMER_SYSTEM_PROMPT,
15205
+ parts: [{ type: "text", text: taskPrompt }]
15206
+ }
15207
+ }, {
15208
+ timeoutMs: args.taskTimeoutMinutes * 60 * 1000,
15209
+ signal: taskAbortController.signal
15210
+ });
15211
+ const messagesResponse = await args.client.session.messages({
15212
+ path: { id: agentSessionId },
15213
+ query: { directory: args.sessionDirectory ?? args.projectIdentity }
15214
+ });
15215
+ const messages = normalizeSDKResponse(messagesResponse, [], {
15216
+ preferResponseOnMissingData: true
15217
+ });
15218
+ const taskResult = extractLatestAssistantText(messages);
15219
+ if (!taskResult) {
15220
+ throw new Error("Dreamer returned no assistant output.");
15221
+ }
15222
+ const durationMs = Date.now() - taskStartedAt;
15223
+ log(`[dreamer] task ${taskName}: completed in ${(durationMs / 1000).toFixed(1)}s (result: ${String(taskResult).length} chars)`);
15224
+ result.tasks.push({
15225
+ name: taskName,
15226
+ durationMs,
15227
+ result: taskResult
15228
+ });
15229
+ } catch (error48) {
15230
+ const durationMs = Date.now() - taskStartedAt;
15231
+ const errorMsg = getErrorMessage(error48);
15232
+ log(`[dreamer] task ${taskName}: failed after ${(durationMs / 1000).toFixed(1)}s \u2014 ${errorMsg}`);
15233
+ result.tasks.push({
15234
+ name: taskName,
15235
+ durationMs,
15236
+ result: null,
15237
+ error: errorMsg
15238
+ });
15239
+ } finally {
15240
+ clearInterval(leaseRenewalInterval);
15241
+ if (agentSessionId) {
15242
+ await args.client.session.delete({
15243
+ path: { id: agentSessionId },
15244
+ query: { directory: args.sessionDirectory ?? args.projectIdentity }
15245
+ }).catch((error48) => {
15246
+ log("[dreamer] failed to delete child session:", error48);
15247
+ });
15248
+ }
15227
15249
  }
15228
- return existingFallback;
15229
15250
  }
15230
- const fallback = createFallbackDatabase();
15231
- databases.set(FALLBACK_DATABASE_KEY, fallback);
15232
- persistenceByDatabase.set(fallback, false);
15233
- persistenceErrorByDatabase.set(fallback, errorMessage);
15234
- return fallback;
15251
+ } finally {
15252
+ releaseLease(args.db, holderId);
15253
+ log(`[dreamer] lease released: ${holderId}`);
15235
15254
  }
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;
15255
+ result.finishedAt = Date.now();
15256
+ const hasSuccessfulTask = result.tasks.some((t) => !t.error);
15257
+ if (hasSuccessfulTask) {
15258
+ setDreamState(args.db, `last_dream_at:${args.projectIdentity}`, String(result.finishedAt));
15259
+ setDreamState(args.db, "last_dream_at", String(result.finishedAt));
15327
15260
  }
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
- };
15261
+ const totalDuration = ((result.finishedAt - startedAt) / 1000).toFixed(1);
15262
+ const succeeded = result.tasks.filter((t) => !t.error).length;
15263
+ const failed = result.tasks.filter((t) => t.error).length;
15264
+ log(`[dreamer] dream run finished in ${totalDuration}s: ${succeeded} succeeded, ${failed} failed`);
15265
+ return result;
15335
15266
  }
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)) {
15267
+ var MAX_LEASE_RETRIES = 3;
15268
+ async function processDreamQueue(args) {
15269
+ clearStaleEntries(args.db, 2 * 60 * 60 * 1000);
15270
+ const entry = dequeueNext(args.db);
15271
+ if (!entry) {
15339
15272
  return null;
15340
15273
  }
15341
- if (result.nudge_anchor_message_id.length === 0 || result.nudge_anchor_text.length === 0) {
15274
+ const projectDirectory = resolveDreamSessionDirectory(entry.projectIdentity);
15275
+ log(`[dreamer] dequeued project ${entry.projectIdentity} (dir=${projectDirectory}), starting dream run`);
15276
+ let result;
15277
+ try {
15278
+ result = await runDream({
15279
+ db: args.db,
15280
+ client: args.client,
15281
+ projectIdentity: entry.projectIdentity,
15282
+ tasks: args.tasks,
15283
+ taskTimeoutMinutes: args.taskTimeoutMinutes,
15284
+ maxRuntimeMinutes: args.maxRuntimeMinutes,
15285
+ sessionDirectory: projectDirectory
15286
+ });
15287
+ } catch (error48) {
15288
+ log(`[dreamer] runDream threw for ${entry.projectIdentity}: ${getErrorMessage(error48)}`);
15289
+ removeDreamEntry(args.db, entry.id);
15342
15290
  return null;
15343
15291
  }
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);
15292
+ const leaseError = result.tasks.find((t) => t.name === "lease" && t.error);
15293
+ if (leaseError) {
15294
+ const retryCount = getEntryRetryCount(args.db, entry.id);
15295
+ if (retryCount >= MAX_LEASE_RETRIES) {
15296
+ log(`[dreamer] lease acquisition failed ${retryCount + 1} times for ${entry.projectIdentity} \u2014 removing queue entry`);
15297
+ removeDreamEntry(args.db, entry.id);
15298
+ } else {
15299
+ log(`[dreamer] lease acquisition failed for ${entry.projectIdentity} (attempt ${retryCount + 1}/${MAX_LEASE_RETRIES}) \u2014 keeping for retry`);
15300
+ resetDreamEntry(args.db, entry.id);
15301
+ }
15302
+ } else {
15303
+ removeDreamEntry(args.db, entry.id);
15304
+ }
15305
+ return result;
15357
15306
  }
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)) {
15307
+ // src/features/magic-context/dreamer/scheduler.ts
15308
+ function parseScheduleWindow(schedule) {
15309
+ const match = /^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/.exec(schedule.trim());
15310
+ if (!match)
15361
15311
  return null;
15362
- }
15363
- if (result.sticky_turn_reminder_text.length === 0) {
15312
+ const startHour = Number(match[1]);
15313
+ const startMin = Number(match[2]);
15314
+ const endHour = Number(match[3]);
15315
+ const endMin = Number(match[4]);
15316
+ if (startHour >= 24 || startMin >= 60 || endHour >= 24 || endMin >= 60) {
15364
15317
  return null;
15365
15318
  }
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);
15406
- }
15407
- }
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
- })();
15319
+ const startMinutes = startHour * 60 + startMin;
15320
+ const endMinutes = endHour * 60 + endMin;
15321
+ return { startMinutes, endMinutes };
15428
15322
  }
15429
- // src/features/magic-context/storage-notes.ts
15430
- function isSessionNoteRow(row) {
15431
- if (row === null || typeof row !== "object")
15323
+ function isInScheduleWindow(schedule, now = new Date) {
15324
+ const window = parseScheduleWindow(schedule);
15325
+ if (!window)
15432
15326
  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";
15327
+ const currentMinutes = now.getHours() * 60 + now.getMinutes();
15328
+ if (window.startMinutes <= window.endMinutes) {
15329
+ return currentMinutes >= window.startMinutes && currentMinutes < window.endMinutes;
15330
+ }
15331
+ return currentMinutes >= window.startMinutes || currentMinutes < window.endMinutes;
15435
15332
  }
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
- };
15333
+ function findProjectsNeedingDream(db) {
15334
+ const projectRows = db.query(`SELECT DISTINCT project_path FROM memories WHERE status = 'active' ORDER BY project_path`).all();
15335
+ const projects = [];
15336
+ for (const row of projectRows) {
15337
+ const lastDreamAtStr = getDreamState(db, `last_dream_at:${row.project_path}`);
15338
+ const fallbackStr = !lastDreamAtStr ? getDreamState(db, "last_dream_at") : null;
15339
+ const lastDreamAt = Number(lastDreamAtStr ?? fallbackStr ?? "0") || 0;
15340
+ const updated = db.query(`SELECT COUNT(*) as cnt FROM memories
15341
+ WHERE project_path = ? AND status = 'active' AND updated_at > ?`).get(row.project_path, lastDreamAt);
15342
+ if (updated && updated.cnt > 0) {
15343
+ projects.push(row.project_path);
15344
+ }
15345
+ }
15346
+ return projects;
15443
15347
  }
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);
15348
+ function checkScheduleAndEnqueue(db, schedule) {
15349
+ if (!isInScheduleWindow(schedule)) {
15350
+ return 0;
15351
+ }
15352
+ const projects = findProjectsNeedingDream(db);
15353
+ if (projects.length === 0) {
15354
+ return 0;
15355
+ }
15356
+ let enqueued = 0;
15357
+ for (const projectIdentity of projects) {
15358
+ const entry = enqueueDream(db, projectIdentity, "scheduled");
15359
+ if (entry) {
15360
+ log(`[dreamer] enqueued project for scheduled dream: ${projectIdentity}`);
15361
+ enqueued++;
15362
+ }
15363
+ }
15364
+ return enqueued;
15447
15365
  }
15448
- function addSessionNote(db, sessionId, content) {
15449
- db.prepare("INSERT INTO session_notes (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, Date.now());
15366
+ // src/features/magic-context/storage-db.ts
15367
+ import { Database } from "bun:sqlite";
15368
+ import { mkdirSync } from "fs";
15369
+ import { join as join5 } from "path";
15370
+
15371
+ // src/shared/data-path.ts
15372
+ import * as os2 from "os";
15373
+ import * as path2 from "path";
15374
+ function getDataDir() {
15375
+ return process.env.XDG_DATA_HOME ?? path2.join(os2.homedir(), ".local", "share");
15450
15376
  }
15451
- function clearSessionNotes(db, sessionId) {
15452
- db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
15377
+ function getOpenCodeStorageDir() {
15378
+ return path2.join(getDataDir(), "opencode", "storage");
15453
15379
  }
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";
15380
+
15381
+ // src/features/magic-context/storage-db.ts
15382
+ var databases = new Map;
15383
+ var FALLBACK_DATABASE_KEY = "__fallback__:memory:";
15384
+ var persistenceByDatabase = new WeakMap;
15385
+ var persistenceErrorByDatabase = new WeakMap;
15386
+ function resolveDatabasePath() {
15387
+ const dbDir = join5(getOpenCodeStorageDir(), "plugin", "magic-context");
15388
+ return { dbDir, dbPath: join5(dbDir, "context.db") };
15389
+ }
15390
+ function initializeDatabase(db) {
15391
+ db.run("PRAGMA journal_mode=WAL");
15392
+ db.run("PRAGMA busy_timeout=5000");
15393
+ db.run("PRAGMA foreign_keys=ON");
15394
+ db.run(`
15395
+ CREATE TABLE IF NOT EXISTS tags (
15396
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15397
+ session_id TEXT,
15398
+ message_id TEXT,
15399
+ type TEXT,
15400
+ status TEXT DEFAULT 'active',
15401
+ byte_size INTEGER,
15402
+ tag_number INTEGER,
15403
+ UNIQUE(session_id, tag_number)
15404
+ );
15405
+
15406
+ CREATE TABLE IF NOT EXISTS pending_ops (
15407
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15408
+ session_id TEXT,
15409
+ tag_id INTEGER,
15410
+ operation TEXT,
15411
+ queued_at INTEGER
15412
+ );
15413
+
15414
+ CREATE TABLE IF NOT EXISTS source_contents (
15415
+ tag_id INTEGER,
15416
+ session_id TEXT,
15417
+ content TEXT,
15418
+ created_at INTEGER,
15419
+ PRIMARY KEY(session_id, tag_id)
15420
+ );
15421
+
15422
+ CREATE TABLE IF NOT EXISTS compartments (
15423
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15424
+ session_id TEXT NOT NULL,
15425
+ sequence INTEGER NOT NULL,
15426
+ start_message INTEGER NOT NULL,
15427
+ end_message INTEGER NOT NULL,
15428
+ start_message_id TEXT DEFAULT '',
15429
+ end_message_id TEXT DEFAULT '',
15430
+ title TEXT NOT NULL,
15431
+ content TEXT NOT NULL,
15432
+ created_at INTEGER NOT NULL,
15433
+ UNIQUE(session_id, sequence)
15434
+ );
15435
+
15436
+ CREATE TABLE IF NOT EXISTS session_facts (
15437
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15438
+ session_id TEXT NOT NULL,
15439
+ category TEXT NOT NULL,
15440
+ content TEXT NOT NULL,
15441
+ created_at INTEGER NOT NULL,
15442
+ updated_at INTEGER NOT NULL
15443
+ );
15444
+
15445
+ CREATE TABLE IF NOT EXISTS session_notes (
15446
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15447
+ session_id TEXT NOT NULL,
15448
+ content TEXT NOT NULL,
15449
+ created_at INTEGER NOT NULL
15450
+ );
15451
+
15452
+ CREATE TABLE IF NOT EXISTS memories (
15453
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15454
+ project_path TEXT NOT NULL,
15455
+ category TEXT NOT NULL,
15456
+ content TEXT NOT NULL,
15457
+ normalized_hash TEXT NOT NULL,
15458
+ source_session_id TEXT,
15459
+ source_type TEXT DEFAULT 'historian',
15460
+ seen_count INTEGER DEFAULT 1,
15461
+ retrieval_count INTEGER DEFAULT 0,
15462
+ first_seen_at INTEGER NOT NULL,
15463
+ created_at INTEGER NOT NULL,
15464
+ updated_at INTEGER NOT NULL,
15465
+ last_seen_at INTEGER NOT NULL,
15466
+ last_retrieved_at INTEGER,
15467
+ status TEXT DEFAULT 'active',
15468
+ expires_at INTEGER,
15469
+ verification_status TEXT DEFAULT 'unverified',
15470
+ verified_at INTEGER,
15471
+ superseded_by_memory_id INTEGER,
15472
+ merged_from TEXT,
15473
+ metadata_json TEXT,
15474
+ UNIQUE(project_path, category, normalized_hash)
15475
+ );
15476
+
15477
+ CREATE TABLE IF NOT EXISTS memory_embeddings (
15478
+ memory_id INTEGER PRIMARY KEY REFERENCES memories(id) ON DELETE CASCADE,
15479
+ embedding BLOB NOT NULL,
15480
+ model_id TEXT
15481
+ );
15482
+
15483
+ CREATE TABLE IF NOT EXISTS dream_state (
15484
+ key TEXT PRIMARY KEY,
15485
+ value TEXT NOT NULL
15486
+ );
15487
+
15488
+ CREATE TABLE IF NOT EXISTS dream_queue (
15489
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15490
+ project_path TEXT NOT NULL,
15491
+ reason TEXT NOT NULL,
15492
+ enqueued_at INTEGER NOT NULL,
15493
+ started_at INTEGER,
15494
+ retry_count INTEGER DEFAULT 0
15495
+ );
15496
+ CREATE INDEX IF NOT EXISTS idx_dream_queue_project ON dream_queue(project_path);
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");
15460
15610
  }
15461
- function toPendingOp(row) {
15462
- if (row.operation !== "drop") {
15463
- sessionLog(row.session_id, `unknown pending operation "${row.operation}"; ignoring`);
15464
- return null;
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}`);
15465
15614
  }
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);
15483
- }
15484
- // src/features/magic-context/storage-source.ts
15485
- function isSourceContentRow(row) {
15486
- if (row === null || typeof row !== "object")
15487
- 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;
15615
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all();
15616
+ if (rows.some((row) => row.name === column)) {
15617
+ return;
15503
15618
  }
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);
15619
+ db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
15620
+ }
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)}`);
15509
15628
  }
15510
- return sources;
15511
15629
  }
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);
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;
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;
15519
15663
  }
15520
- return stmt;
15521
15664
  }
15522
- function isTagRow(row) {
15665
+ function isDatabasePersisted(db) {
15666
+ return persistenceByDatabase.get(db) ?? false;
15667
+ }
15668
+ function getDatabasePersistenceError(db) {
15669
+ return persistenceErrorByDatabase.get(db) ?? null;
15670
+ }
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"
15685
+ };
15686
+ var BOOLEAN_META_KEYS = new Set(["isSubagent", "compartmentInProgress"]);
15687
+ function isSessionMetaRow(row) {
15523
15688
  if (row === null || typeof row !== "object")
15524
15689
  return false;
15525
15690
  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";
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");
15527
15692
  }
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";
15693
+ function getDefaultSessionMeta(sessionId) {
15531
15694
  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
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: ""
15538
15708
  };
15539
15709
  }
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);
15553
- }
15554
- function getTopNBySize(db, sessionId, n) {
15555
- if (n <= 0) {
15556
- return [];
15557
- }
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);
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 ?? "");
15560
15713
  }
15561
- // src/features/magic-context/compaction.ts
15562
- function createCompactionHandler() {
15714
+ function toSessionMeta(row) {
15563
15715
  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
- }
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)
15571
15729
  };
15572
15730
  }
15573
-
15574
- // src/hooks/is-anthropic-provider.ts
15575
- function isAnthropicProvider(providerID) {
15576
- return providerID === "anthropic" || providerID === "google-vertex-anthropic";
15577
- }
15578
-
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;
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";
15595
15738
  }
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];
15602
- }
15603
- if (modelKey) {
15604
- const bareModelId = modelKey.split("/").slice(1).join("/");
15605
- if (bareModelId && typeof cacheTtl[bareModelId] === "string") {
15606
- return cacheTtl[bareModelId];
15607
- }
15608
- }
15609
- return cacheTtl.default ?? "5m";
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";
15610
15744
  }
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];
15617
- }
15618
- if (modelKey) {
15619
- const bareModelId = modelKey.split("/").slice(1).join("/");
15620
- if (bareModelId && typeof config2[bareModelId] === "number") {
15621
- return config2[bareModelId];
15622
- }
15623
- }
15624
- return config2.default ?? fallback;
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";
15625
15750
  }
15626
- function resolveModelKey(providerID, modelID) {
15627
- if (!providerID || !modelID) {
15628
- return;
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;
15629
15755
  }
15630
- return `${providerID}/${modelID}`;
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
+ };
15631
15763
  }
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;
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;
15643
15768
  }
15644
- if (typeof record2.id === "string") {
15645
- return record2.id;
15769
+ if (result.nudge_anchor_message_id.length === 0 || result.nudge_anchor_text.length === 0) {
15770
+ return null;
15646
15771
  }
15647
- return;
15772
+ return {
15773
+ messageId: result.nudge_anchor_message_id,
15774
+ nudgeText: result.nudge_anchor_text
15775
+ };
15648
15776
  }
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
15657
- };
15658
- function parseCacheTtl(ttl) {
15659
- const normalizedTtl = ttl.trim();
15660
- if (NUMERIC_PATTERN.test(normalizedTtl)) {
15661
- return Number(normalizedTtl);
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;
15662
15790
  }
15663
- const match = normalizedTtl.match(TTL_PATTERN);
15664
- if (!match) {
15665
- throw new Error(`Invalid cache TTL format: ${ttl}`);
15791
+ if (result.sticky_turn_reminder_text.length === 0) {
15792
+ return null;
15666
15793
  }
15667
- const value = Number(match[1]);
15668
- const unit = match[2];
15669
- return value * UNIT_TO_MS[unit];
15670
- }
15671
- function createScheduler(config2) {
15672
15794
  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
- }
15795
+ text: result.sticky_turn_reminder_text,
15796
+ messageId: result.sticky_turn_reminder_message_id.length > 0 ? result.sticky_turn_reminder_message_id : null
15695
15797
  };
15696
15798
  }
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";
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
+ })();
15707
15804
  }
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;
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);
15721
15807
  }
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);
15757
- }
15758
- function resetCounter(sessionId, db) {
15759
- counters.set(sessionId, 0);
15760
- assignments.delete(sessionId);
15761
- getUpsertCounterStatement(db).run(sessionId, 0);
15762
- }
15763
- function getCounter(sessionId) {
15764
- return counters.get(sessionId) ?? 0;
15808
+ // src/features/magic-context/storage-meta-session.ts
15809
+ function getOrCreateSessionMeta(db, sessionId) {
15810
+ 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);
15811
+ if (isSessionMetaRow(result)) {
15812
+ return toSessionMeta(result);
15765
15813
  }
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
- }
15814
+ const defaults = getDefaultSessionMeta(sessionId);
15815
+ ensureSessionMetaRow(db, sessionId);
15816
+ return defaults;
15817
+ }
15818
+ function updateSessionMeta(db, sessionId, updates) {
15819
+ const setClauses = [];
15820
+ const values = [];
15821
+ for (const [key, column] of Object.entries(META_COLUMNS)) {
15822
+ const value = updates[key];
15823
+ if (value === undefined)
15824
+ continue;
15825
+ if (value === null) {
15826
+ setClauses.push(`${column} = ?`);
15827
+ values.push("");
15828
+ } else if (BOOLEAN_META_KEYS.has(key)) {
15829
+ setClauses.push(`${column} = ?`);
15830
+ values.push(value ? 1 : 0);
15831
+ } else if (typeof value === "string" || typeof value === "number") {
15832
+ setClauses.push(`${column} = ?`);
15833
+ values.push(value);
15780
15834
  }
15781
- const counter = Math.max(row?.counter ?? 0, maxTagNumber);
15782
- counters.set(sessionId, counter);
15783
15835
  }
15784
- function cleanup(sessionId) {
15785
- counters.delete(sessionId);
15786
- assignments.delete(sessionId);
15836
+ if (setClauses.length === 0) {
15837
+ return;
15787
15838
  }
15839
+ db.transaction(() => {
15840
+ ensureSessionMetaRow(db, sessionId);
15841
+ db.prepare(`UPDATE session_meta SET ${setClauses.join(", ")} WHERE session_id = ?`).run(...values, sessionId);
15842
+ })();
15843
+ }
15844
+ function clearSession(db, sessionId) {
15845
+ db.transaction(() => {
15846
+ db.prepare("DELETE FROM pending_ops WHERE session_id = ?").run(sessionId);
15847
+ db.prepare("DELETE FROM source_contents WHERE session_id = ?").run(sessionId);
15848
+ db.prepare("DELETE FROM tags WHERE session_id = ?").run(sessionId);
15849
+ db.prepare("DELETE FROM session_meta WHERE session_id = ?").run(sessionId);
15850
+ db.prepare("DELETE FROM compartments WHERE session_id = ?").run(sessionId);
15851
+ db.prepare("DELETE FROM session_facts WHERE session_id = ?").run(sessionId);
15852
+ db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
15853
+ db.prepare("DELETE FROM recomp_compartments WHERE session_id = ?").run(sessionId);
15854
+ db.prepare("DELETE FROM recomp_facts WHERE session_id = ?").run(sessionId);
15855
+ })();
15856
+ }
15857
+ // src/features/magic-context/storage-notes.ts
15858
+ function isSessionNoteRow(row) {
15859
+ if (row === null || typeof row !== "object")
15860
+ return false;
15861
+ const candidate = row;
15862
+ return typeof candidate.id === "number" && typeof candidate.session_id === "string" && typeof candidate.content === "string" && typeof candidate.created_at === "number";
15863
+ }
15864
+ function toSessionNote(row) {
15788
15865
  return {
15789
- assignTag,
15790
- getTag,
15791
- bindTag,
15792
- getAssignments,
15793
- resetCounter,
15794
- getCounter,
15795
- initFromDb,
15796
- cleanup
15866
+ id: row.id,
15867
+ sessionId: row.session_id,
15868
+ content: row.content,
15869
+ createdAt: row.created_at
15797
15870
  };
15798
15871
  }
15799
-
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);
15872
+ function getSessionNotes(db, sessionId) {
15873
+ const rows = db.prepare("SELECT * FROM session_notes WHERE session_id = ? ORDER BY id ASC").all(sessionId).filter(isSessionNoteRow);
15874
+ return rows.map(toSessionNote);
15875
+ }
15876
+ function addSessionNote(db, sessionId, content) {
15877
+ db.prepare("INSERT INTO session_notes (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, Date.now());
15878
+ }
15879
+ function clearSessionNotes(db, sessionId) {
15880
+ db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
15881
+ }
15882
+ // src/features/magic-context/storage-ops.ts
15883
+ function isPendingOpRow(row) {
15884
+ if (row === null || typeof row !== "object")
15885
+ return false;
15886
+ const r = row;
15887
+ return typeof r.id === "number" && typeof r.session_id === "string" && typeof r.tag_id === "number" && typeof r.operation === "string" && typeof r.queued_at === "number";
15888
+ }
15889
+ function toPendingOp(row) {
15890
+ if (row.operation !== "drop") {
15891
+ sessionLog(row.session_id, `unknown pending operation "${row.operation}"; ignoring`);
15892
+ return null;
15809
15893
  }
15810
- return stmt;
15894
+ return {
15895
+ id: row.id,
15896
+ sessionId: row.session_id,
15897
+ tagId: row.tag_id,
15898
+ operation: row.operation,
15899
+ queuedAt: row.queued_at
15900
+ };
15811
15901
  }
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);
15902
+ function queuePendingOp(db, sessionId, tagId, operation, queuedAt = Date.now()) {
15903
+ db.prepare("INSERT INTO pending_ops (session_id, tag_id, operation, queued_at) VALUES (?, ?, ?, ?)").run(sessionId, tagId, operation, queuedAt);
15904
+ }
15905
+ function getPendingOps(db, sessionId) {
15906
+ 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);
15907
+ return rows.map(toPendingOp).filter((op) => op !== null);
15908
+ }
15909
+ function removePendingOp(db, sessionId, tagId) {
15910
+ db.prepare("DELETE FROM pending_ops WHERE session_id = ? AND tag_id = ?").run(sessionId, tagId);
15911
+ }
15912
+ // src/features/magic-context/storage-source.ts
15913
+ function isSourceContentRow(row) {
15914
+ if (row === null || typeof row !== "object")
15915
+ return false;
15916
+ const r = row;
15917
+ return typeof r.tag_id === "number" && typeof r.content === "string";
15918
+ }
15919
+ function saveSourceContent(db, sessionId, tagId, content) {
15920
+ db.prepare("INSERT OR IGNORE INTO source_contents (tag_id, session_id, content, created_at) VALUES (?, ?, ?, ?)").run(tagId, sessionId, content, Date.now());
15921
+ }
15922
+ function replaceSourceContent(db, sessionId, tagId, content) {
15923
+ db.prepare(`INSERT INTO source_contents (tag_id, session_id, content, created_at)
15924
+ VALUES (?, ?, ?, ?)
15925
+ ON CONFLICT(session_id, tag_id)
15926
+ DO UPDATE SET content = excluded.content, created_at = excluded.created_at`).run(tagId, sessionId, content, Date.now());
15927
+ }
15928
+ function getSourceContents(db, sessionId, tagIds) {
15929
+ if (tagIds.length === 0) {
15930
+ return new Map;
15817
15931
  }
15818
- return stmt;
15932
+ const placeholders = tagIds.map(() => "?").join(", ");
15933
+ const rows = db.prepare(`SELECT tag_id, content FROM source_contents WHERE session_id = ? AND tag_id IN (${placeholders})`).all(sessionId, ...tagIds).filter(isSourceContentRow);
15934
+ const sources = new Map;
15935
+ for (const row of rows) {
15936
+ sources.set(row.tag_id, row.content);
15937
+ }
15938
+ return sources;
15819
15939
  }
15820
- function getDeleteDreamStateStatement(db) {
15821
- let stmt = deleteDreamStateStatements.get(db);
15940
+ // src/features/magic-context/storage-tags.ts
15941
+ var insertTagStatements = new WeakMap;
15942
+ function getInsertTagStatement(db) {
15943
+ let stmt = insertTagStatements.get(db);
15822
15944
  if (!stmt) {
15823
- stmt = db.prepare("DELETE FROM dream_state WHERE key = ?");
15824
- deleteDreamStateStatements.set(db, stmt);
15945
+ stmt = db.prepare("INSERT INTO tags (session_id, message_id, type, byte_size, tag_number) VALUES (?, ?, ?, ?, ?)");
15946
+ insertTagStatements.set(db, stmt);
15825
15947
  }
15826
15948
  return stmt;
15827
15949
  }
15828
- function getDreamState(db, key) {
15829
- const row = getGetDreamStateStatement(db).get(key);
15830
- return typeof row?.value === "string" ? row.value : null;
15831
- }
15832
- function setDreamState(db, key, value) {
15833
- getSetDreamStateStatement(db).run(key, value);
15950
+ function isTagRow(row) {
15951
+ if (row === null || typeof row !== "object")
15952
+ return false;
15953
+ const r = row;
15954
+ 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";
15834
15955
  }
15835
- function deleteDreamState(db, key) {
15836
- getDeleteDreamStateStatement(db).run(key);
15956
+ function toTagEntry(row) {
15957
+ const type = row.type === "tool" ? "tool" : row.type === "file" ? "file" : "message";
15958
+ const status = row.status === "dropped" || row.status === "compacted" ? row.status : "active";
15959
+ return {
15960
+ tagNumber: row.tag_number,
15961
+ messageId: row.message_id,
15962
+ type,
15963
+ status,
15964
+ byteSize: row.byte_size,
15965
+ sessionId: row.session_id
15966
+ };
15837
15967
  }
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;
15848
- }
15849
- const expiry = Number(value);
15850
- return Number.isFinite(expiry) ? expiry : null;
15968
+ function insertTag(db, sessionId, messageId, type, byteSize, tagNumber) {
15969
+ getInsertTagStatement(db).run(sessionId, messageId, type, byteSize, tagNumber);
15970
+ return tagNumber;
15851
15971
  }
15852
- function isLeaseActive(db) {
15853
- const expiry = getLeaseExpiry(db);
15854
- return expiry !== null && expiry > Date.now();
15972
+ function updateTagStatus(db, sessionId, tagId, status) {
15973
+ db.prepare("UPDATE tags SET status = ? WHERE session_id = ? AND tag_number = ?").run(status, sessionId, tagId);
15855
15974
  }
15856
- function getLeaseHolder(db) {
15857
- return getDreamState(db, LEASE_HOLDER_KEY);
15975
+ function updateTagMessageId(db, sessionId, tagId, messageId) {
15976
+ db.prepare("UPDATE tags SET message_id = ? WHERE session_id = ? AND tag_number = ?").run(messageId, sessionId, tagId);
15858
15977
  }
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
- })();
15978
+ function getTagsBySession(db, sessionId) {
15979
+ 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);
15980
+ return rows.map(toTagEntry);
15873
15981
  }
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
- })();
15982
+ function getTopNBySize(db, sessionId, n) {
15983
+ if (n <= 0) {
15984
+ return [];
15985
+ }
15986
+ 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);
15987
+ return rows.map(toTagEntry);
15884
15988
  }
15885
- function releaseLease(db, holderId) {
15886
- db.transaction(() => {
15887
- if (getLeaseHolder(db) !== holderId) {
15888
- return;
15989
+ // src/plugin/dream-timer.ts
15990
+ var DREAM_TIMER_INTERVAL_MS = 15 * 60 * 1000;
15991
+ function startDreamScheduleTimer(args) {
15992
+ const { client, dreamerConfig } = args;
15993
+ if (!dreamerConfig.enabled || !dreamerConfig.schedule?.trim()) {
15994
+ return;
15995
+ }
15996
+ const timer = setInterval(() => {
15997
+ try {
15998
+ const db = openDatabase();
15999
+ checkScheduleAndEnqueue(db, dreamerConfig.schedule);
16000
+ processDreamQueue({
16001
+ db,
16002
+ client,
16003
+ tasks: dreamerConfig.tasks,
16004
+ taskTimeoutMinutes: dreamerConfig.task_timeout_minutes,
16005
+ maxRuntimeMinutes: dreamerConfig.max_runtime_minutes
16006
+ }).catch((error48) => {
16007
+ log("[dreamer] timer-triggered queue processing failed:", error48);
16008
+ });
16009
+ } catch (error48) {
16010
+ log("[dreamer] timer-triggered schedule check failed:", error48);
15889
16011
  }
15890
- deleteDreamState(db, LEASE_HOLDER_KEY);
15891
- deleteDreamState(db, LEASE_HEARTBEAT_KEY);
15892
- deleteDreamState(db, LEASE_EXPIRY_KEY);
15893
- })();
16012
+ }, DREAM_TIMER_INTERVAL_MS);
16013
+ if (typeof timer === "object" && "unref" in timer) {
16014
+ timer.unref();
16015
+ }
16016
+ log(`[dreamer] started independent schedule timer (every ${DREAM_TIMER_INTERVAL_MS / 60000}m)`);
15894
16017
  }
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);
15904
- return {
15905
- id: Number(result.lastInsertRowid),
15906
- projectIdentity,
15907
- reason,
15908
- enqueuedAt: now,
15909
- startedAt: null
15910
- };
15911
- })();
16018
+
16019
+ // src/plugin/event.ts
16020
+ function createEventHandler(args) {
16021
+ return async (input) => {
16022
+ await args.magicContext?.event?.(input);
16023
+ };
15912
16024
  }
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;
16025
+
16026
+ // src/features/magic-context/compaction.ts
16027
+ function createCompactionHandler() {
15917
16028
  return {
15918
- id: row.id,
15919
- projectIdentity: row.project_path,
15920
- reason: row.reason,
15921
- enqueuedAt: row.enqueued_at,
15922
- startedAt: null
16029
+ onCompacted(sessionId, db) {
16030
+ db.transaction(() => {
16031
+ db.prepare("UPDATE tags SET status = 'compacted' WHERE session_id = ? AND status IN ('active', 'dropped')").run(sessionId);
16032
+ db.prepare("DELETE FROM pending_ops WHERE session_id = ?").run(sessionId);
16033
+ })();
16034
+ updateSessionMeta(db, sessionId, { lastNudgeBand: null });
16035
+ }
15923
16036
  };
15924
16037
  }
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
- })();
15936
- }
15937
- function removeDreamEntry(db, id) {
15938
- db.prepare("DELETE FROM dream_queue WHERE id = ?").run(id);
16038
+
16039
+ // src/hooks/is-anthropic-provider.ts
16040
+ function isAnthropicProvider(providerID) {
16041
+ return providerID === "anthropic" || providerID === "google-vertex-anthropic";
15939
16042
  }
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);
16043
+
16044
+ // src/hooks/magic-context/event-resolvers.ts
16045
+ var DEFAULT_CONTEXT_LIMIT = 200000;
16046
+ function resolveContextLimit(providerID, modelID, config2) {
16047
+ if (!providerID) {
16048
+ return DEFAULT_CONTEXT_LIMIT;
16049
+ }
16050
+ if (modelID) {
16051
+ const modelSpecific = config2.modelContextLimitsCache?.get(`${providerID}/${modelID}`);
16052
+ if (typeof modelSpecific === "number" && modelSpecific > 0) {
16053
+ return modelSpecific;
16054
+ }
16055
+ }
16056
+ if (isAnthropicProvider(providerID)) {
16057
+ return 1e6;
16058
+ }
16059
+ return DEFAULT_CONTEXT_LIMIT;
15942
16060
  }
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;
16061
+ function resolveCacheTtl(cacheTtl, modelKey) {
16062
+ if (typeof cacheTtl === "string") {
16063
+ return cacheTtl;
16064
+ }
16065
+ if (modelKey && typeof cacheTtl[modelKey] === "string") {
16066
+ return cacheTtl[modelKey];
16067
+ }
16068
+ if (modelKey) {
16069
+ const bareModelId = modelKey.split("/").slice(1).join("/");
16070
+ if (bareModelId && typeof cacheTtl[bareModelId] === "string") {
16071
+ return cacheTtl[bareModelId];
16072
+ }
16073
+ }
16074
+ return cacheTtl.default ?? "5m";
15946
16075
  }
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;
16076
+ function resolveExecuteThreshold(config2, modelKey, fallback) {
16077
+ if (typeof config2 === "number") {
16078
+ return config2;
16079
+ }
16080
+ if (modelKey && typeof config2[modelKey] === "number") {
16081
+ return config2[modelKey];
16082
+ }
16083
+ if (modelKey) {
16084
+ const bareModelId = modelKey.split("/").slice(1).join("/");
16085
+ if (bareModelId && typeof config2[bareModelId] === "number") {
16086
+ return config2[bareModelId];
16087
+ }
16088
+ }
16089
+ return config2.default ?? fallback;
15951
16090
  }
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);
16091
+ function resolveModelKey(providerID, modelID) {
16092
+ if (!providerID || !modelID) {
16093
+ return;
16094
+ }
16095
+ return `${providerID}/${modelID}`;
15958
16096
  }
15959
- function resolveDreamSessionDirectory(projectIdentity) {
15960
- return dreamProjectDirectories.get(projectIdentity) ?? projectIdentity;
16097
+ function resolveSessionId(properties) {
16098
+ if (typeof properties?.sessionID === "string") {
16099
+ return properties.sessionID;
16100
+ }
16101
+ const info = properties?.info;
16102
+ if (info === null || typeof info !== "object") {
16103
+ return;
16104
+ }
16105
+ const record2 = info;
16106
+ if (typeof record2.sessionID === "string") {
16107
+ return record2.sessionID;
16108
+ }
16109
+ if (typeof record2.id === "string") {
16110
+ return record2.id;
16111
+ }
16112
+ return;
15961
16113
  }
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;
16114
+
16115
+ // src/features/magic-context/scheduler.ts
16116
+ var TTL_PATTERN = /^(\d+)([smh])$/;
16117
+ var NUMERIC_PATTERN = /^\d+$/;
16118
+ var UNIT_TO_MS = {
16119
+ s: 1000,
16120
+ m: 60 * 1000,
16121
+ h: 60 * 60 * 1000
16122
+ };
16123
+ function parseCacheTtl(ttl) {
16124
+ const normalizedTtl = ttl.trim();
16125
+ if (NUMERIC_PATTERN.test(normalizedTtl)) {
16126
+ return Number(normalizedTtl);
15983
16127
  }
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");
16001
- }
16128
+ const match = normalizedTtl.match(TTL_PATTERN);
16129
+ if (!match) {
16130
+ throw new Error(`Invalid cache TTL format: ${ttl}`);
16002
16131
  }
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;
16011
- }
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
- });
16132
+ const value = Number(match[1]);
16133
+ const unit = match[2];
16134
+ return value * UNIT_TO_MS[unit];
16135
+ }
16136
+ function createScheduler(config2) {
16137
+ return {
16138
+ shouldExecute(sessionMeta, contextUsage, currentTime = Date.now(), sessionId) {
16139
+ const threshold = resolveExecuteThreshold(config2.executeThresholdPercentage, undefined, 65);
16140
+ if (contextUsage.percentage >= threshold) {
16141
+ return "execute";
16142
+ }
16143
+ let ttlMs;
16144
+ try {
16145
+ ttlMs = parseCacheTtl(sessionMeta.cacheTtl);
16081
16146
  } 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
- });
16147
+ if (sessionId) {
16148
+ sessionLog(sessionId, `invalid cache_ttl "${sessionMeta.cacheTtl}"; falling back to default 5m`, error48);
16149
+ } else {
16150
+ log(`[magic-context] invalid cache_ttl "${sessionMeta.cacheTtl}"; falling back to default 5m`, error48);
16100
16151
  }
16152
+ ttlMs = parseCacheTtl("5m");
16153
+ }
16154
+ const elapsedTime = currentTime - sessionMeta.lastResponseTime;
16155
+ if (elapsedTime > ttlMs) {
16156
+ return "execute";
16101
16157
  }
16158
+ return "defer";
16102
16159
  }
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));
16160
+ };
16161
+ }
16162
+
16163
+ // src/features/magic-context/tagger.ts
16164
+ var GET_COUNTER_SQL = `SELECT counter FROM session_meta WHERE session_id = ?`;
16165
+ var GET_ASSIGNMENTS_SQL = "SELECT message_id, tag_number FROM tags WHERE session_id = ? ORDER BY tag_number ASC";
16166
+ function isAssignmentRow(row) {
16167
+ if (row === null || typeof row !== "object") {
16168
+ return false;
16112
16169
  }
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;
16170
+ const candidate = row;
16171
+ return typeof candidate.message_id === "string" && typeof candidate.tag_number === "number";
16118
16172
  }
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;
16173
+ var UPSERT_COUNTER_SQL = `
16174
+ INSERT INTO session_meta (session_id, counter)
16175
+ VALUES (?, ?)
16176
+ ON CONFLICT(session_id) DO UPDATE SET counter = excluded.counter
16177
+ `;
16178
+ var upsertCounterStatements = new WeakMap;
16179
+ function getUpsertCounterStatement(db) {
16180
+ let stmt = upsertCounterStatements.get(db);
16181
+ if (!stmt) {
16182
+ stmt = db.prepare(UPSERT_COUNTER_SQL);
16183
+ upsertCounterStatements.set(db, stmt);
16125
16184
  }
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;
16185
+ return stmt;
16186
+ }
16187
+ function createTagger() {
16188
+ const counters = new Map;
16189
+ const assignments = new Map;
16190
+ function getSessionAssignments(sessionId) {
16191
+ let map2 = assignments.get(sessionId);
16192
+ if (!map2) {
16193
+ map2 = new Map;
16194
+ assignments.set(sessionId, map2);
16195
+ }
16196
+ return map2;
16143
16197
  }
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);
16198
+ function assignTag(sessionId, messageId, type, byteSize, db) {
16199
+ const sessionAssignments = getSessionAssignments(sessionId);
16200
+ const existing = sessionAssignments.get(messageId);
16201
+ if (existing !== undefined) {
16202
+ return existing;
16153
16203
  }
16154
- } else {
16155
- removeDreamEntry(args.db, entry.id);
16204
+ const current = counters.get(sessionId) ?? 0;
16205
+ const next = current + 1;
16206
+ db.transaction(() => {
16207
+ insertTag(db, sessionId, messageId, type, byteSize, next);
16208
+ getUpsertCounterStatement(db).run(sessionId, next);
16209
+ })();
16210
+ counters.set(sessionId, next);
16211
+ sessionAssignments.set(messageId, next);
16212
+ return next;
16156
16213
  }
16157
- return result;
16158
- }
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;
16214
+ function getTag(sessionId, messageId) {
16215
+ return assignments.get(sessionId)?.get(messageId);
16170
16216
  }
16171
- const startMinutes = startHour * 60 + startMin;
16172
- const endMinutes = endHour * 60 + endMin;
16173
- return { startMinutes, endMinutes };
16174
- }
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;
16217
+ function bindTag(sessionId, messageId, tagNumber) {
16218
+ getSessionAssignments(sessionId).set(messageId, tagNumber);
16182
16219
  }
16183
- return currentMinutes >= window.startMinutes || currentMinutes < window.endMinutes;
16184
- }
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);
16196
- }
16220
+ function getAssignments(sessionId) {
16221
+ return getSessionAssignments(sessionId);
16197
16222
  }
16198
- return projects;
16199
- }
16200
- function checkScheduleAndEnqueue(db, schedule) {
16201
- if (!isInScheduleWindow(schedule)) {
16202
- return 0;
16223
+ function resetCounter(sessionId, db) {
16224
+ counters.set(sessionId, 0);
16225
+ assignments.delete(sessionId);
16226
+ getUpsertCounterStatement(db).run(sessionId, 0);
16203
16227
  }
16204
- const projects = findProjectsNeedingDream(db);
16205
- if (projects.length === 0) {
16206
- return 0;
16228
+ function getCounter(sessionId) {
16229
+ return counters.get(sessionId) ?? 0;
16207
16230
  }
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++;
16231
+ function initFromDb(sessionId, db) {
16232
+ if (counters.has(sessionId)) {
16233
+ return;
16234
+ }
16235
+ const row = db.prepare(GET_COUNTER_SQL).get(sessionId);
16236
+ const assignmentRows = db.prepare(GET_ASSIGNMENTS_SQL).all(sessionId).filter(isAssignmentRow);
16237
+ const sessionAssignments = getSessionAssignments(sessionId);
16238
+ sessionAssignments.clear();
16239
+ let maxTagNumber = 0;
16240
+ for (const assignment of assignmentRows) {
16241
+ sessionAssignments.set(assignment.message_id, assignment.tag_number);
16242
+ if (assignment.tag_number > maxTagNumber) {
16243
+ maxTagNumber = assignment.tag_number;
16244
+ }
16214
16245
  }
16246
+ const counter = Math.max(row?.counter ?? 0, maxTagNumber);
16247
+ counters.set(sessionId, counter);
16215
16248
  }
16216
- return enqueued;
16249
+ function cleanup(sessionId) {
16250
+ counters.delete(sessionId);
16251
+ assignments.delete(sessionId);
16252
+ }
16253
+ return {
16254
+ assignTag,
16255
+ getTag,
16256
+ bindTag,
16257
+ getAssignments,
16258
+ resetCounter,
16259
+ getCounter,
16260
+ initFromDb,
16261
+ cleanup
16262
+ };
16217
16263
  }
16264
+
16218
16265
  // src/features/magic-context/memory/project-identity.ts
16219
16266
  import { execSync } from "child_process";
16220
16267
  import path3 from "path";
@@ -21120,11 +21167,13 @@ function createChatMessageHook(args) {
21120
21167
  const sessionId = input.sessionID;
21121
21168
  if (!sessionId)
21122
21169
  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);
21170
+ if (args.ctxReduceEnabled !== false) {
21171
+ const sessionMeta = getOrCreateSessionMeta(args.db, sessionId);
21172
+ const turnUsage = args.toolUsageSinceUserTurn.get(sessionId);
21173
+ const agentAlreadyReduced = args.recentReduceBySession.has(sessionId);
21174
+ if (!sessionMeta.isSubagent && !agentAlreadyReduced && getPersistedStickyTurnReminder(args.db, sessionId) === null && turnUsage !== undefined && turnUsage >= TOOL_HEAVY_TURN_REMINDER_THRESHOLD) {
21175
+ setPersistedStickyTurnReminder(args.db, sessionId, TOOL_HEAVY_TURN_REMINDER_TEXT);
21176
+ }
21128
21177
  }
21129
21178
  args.toolUsageSinceUserTurn.set(sessionId, 0);
21130
21179
  const previousVariant = args.variantBySession.get(sessionId);
@@ -21170,6 +21219,8 @@ function createEventHook(args) {
21170
21219
  args.emergencyNudgeFired.delete(sessionId);
21171
21220
  return;
21172
21221
  }
21222
+ if (args.ctxReduceEnabled === false)
21223
+ return;
21173
21224
  if (args.emergencyNudgeFired.has(sessionId))
21174
21225
  return;
21175
21226
  const meta3 = getOrCreateSessionMeta(args.db, sessionId);
@@ -21242,12 +21293,18 @@ Use \`ctx_reduce\` to manage context size. It supports one operation:
21242
21293
  - \`drop\`: Remove entirely (best for tool outputs you already acted on).
21243
21294
  Syntax: "3-5", "1,2,9", or "1-5,8,12-15". Last ${protectedTags} tags are protected.
21244
21295
  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.
21296
+ 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.
21297
+ Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
21246
21298
  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
21299
  NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
21248
21300
  NEVER drop user messages \u2014 they are short and will be summarized by compartmentalization automatically. Dropping them loses context the historian needs.
21249
21301
  NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
21250
21302
  Before your turn finishes, consider using \`ctx_reduce\` to drop large tool outputs you no longer need.`;
21303
+ var BASE_INTRO_NO_REDUCE = `Messages and tool outputs are tagged with \xA7N\xA7 identifiers (e.g., \xA71\xA7, \xA742\xA7).
21304
+ 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).
21305
+ 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.
21306
+ Use \`ctx_search\` to search across project memories, session facts, and conversation history from one query.
21307
+ 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
21308
  var SISYPHUS_SECTION = `
21252
21309
  ### Reduction Triggers
21253
21310
  - After collecting background agent results (explore/librarian) \u2014 drop raw outputs once you extracted what you need.
@@ -21384,7 +21441,12 @@ function detectAgentFromSystemPrompt(systemPrompt) {
21384
21441
  }
21385
21442
  return null;
21386
21443
  }
21387
- function buildMagicContextSection(agent, protectedTags) {
21444
+ function buildMagicContextSection(agent, protectedTags, ctxReduceEnabled = true) {
21445
+ if (!ctxReduceEnabled) {
21446
+ return `## Magic Context
21447
+
21448
+ ${BASE_INTRO_NO_REDUCE}`;
21449
+ }
21388
21450
  const section = agent ? AGENT_SECTIONS[agent] : GENERIC_SECTION;
21389
21451
  return `## Magic Context
21390
21452
 
@@ -21405,7 +21467,7 @@ function createSystemPromptHashHandler(deps) {
21405
21467
  `);
21406
21468
  if (fullPrompt.length > 0 && !fullPrompt.includes(MAGIC_CONTEXT_MARKER)) {
21407
21469
  const detectedAgent = detectAgentFromSystemPrompt(fullPrompt);
21408
- const guidance = buildMagicContextSection(detectedAgent, deps.protectedTags);
21470
+ const guidance = buildMagicContextSection(detectedAgent, deps.protectedTags, deps.ctxReduceEnabled);
21409
21471
  output.system.push(guidance);
21410
21472
  sessionLog(sessionId, `injected ${detectedAgent ?? "generic"} guidance into system prompt`);
21411
21473
  }
@@ -21480,13 +21542,14 @@ function createMagicContextHook(deps) {
21480
21542
  const liveModelBySession = new Map;
21481
21543
  const recentReduceBySession = new Map;
21482
21544
  const toolUsageSinceUserTurn = new Map;
21483
- const nudgerWithRecentReduce = createNudger({
21545
+ const ctxReduceEnabled = deps.config.ctx_reduce_enabled !== false;
21546
+ const nudgerWithRecentReduce = ctxReduceEnabled ? createNudger({
21484
21547
  protected_tags: deps.config.protected_tags,
21485
21548
  nudge_interval_tokens: deps.config.nudge_interval_tokens ?? DEFAULT_NUDGE_INTERVAL_TOKENS,
21486
21549
  iteration_nudge_threshold: deps.config.iteration_nudge_threshold ?? 15,
21487
21550
  execute_threshold_percentage: deps.config.execute_threshold_percentage ?? 65,
21488
21551
  recentReduceBySession
21489
- });
21552
+ }) : () => null;
21490
21553
  const transform2 = createTransform({
21491
21554
  tagger: deps.tagger,
21492
21555
  scheduler: deps.scheduler,
@@ -21592,6 +21655,7 @@ function createMagicContextHook(deps) {
21592
21655
  const systemPromptHashHandler = createSystemPromptHashHandler({
21593
21656
  db,
21594
21657
  protectedTags: deps.config.protected_tags,
21658
+ ctxReduceEnabled,
21595
21659
  flushedSessions,
21596
21660
  lastHeuristicsTurnId
21597
21661
  });
@@ -21608,7 +21672,8 @@ function createMagicContextHook(deps) {
21608
21672
  lastHeuristicsTurnId,
21609
21673
  commitSeenLastPass,
21610
21674
  client: deps.client,
21611
- protectedTags: deps.config.protected_tags
21675
+ protectedTags: deps.config.protected_tags,
21676
+ ctxReduceEnabled
21612
21677
  });
21613
21678
  return {
21614
21679
  "experimental.chat.messages.transform": transform2,
@@ -21620,7 +21685,8 @@ function createMagicContextHook(deps) {
21620
21685
  recentReduceBySession,
21621
21686
  variantBySession,
21622
21687
  flushedSessions,
21623
- lastHeuristicsTurnId
21688
+ lastHeuristicsTurnId,
21689
+ ctxReduceEnabled
21624
21690
  }),
21625
21691
  event: async (input) => {
21626
21692
  await eventHook(input);
@@ -21723,18 +21789,17 @@ function createCtxExpandTools() {
21723
21789
  }
21724
21790
  // src/tools/ctx-memory/constants.ts
21725
21791
  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.
21792
+ 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
21793
 
21728
- Supported actions: write, delete, search, list, update, merge, archive.`;
21794
+ Supported actions: write, delete, list, update, merge, archive.`;
21729
21795
  var DEFAULT_SEARCH_LIMIT2 = 10;
21730
21796
  // src/tools/ctx-memory/tools.ts
21731
21797
  import { tool as tool2 } from "@opencode-ai/plugin";
21732
21798
 
21733
21799
  // src/tools/ctx-memory/types.ts
21734
- var CTX_MEMORY_ACTIONS = [
21735
- "write",
21736
- "delete",
21737
- "search",
21800
+ var CTX_MEMORY_ACTIONS = ["write", "delete"];
21801
+ var CTX_MEMORY_DREAMER_ACTIONS = [
21802
+ ...CTX_MEMORY_ACTIONS,
21738
21803
  "list",
21739
21804
  "update",
21740
21805
  "merge",
@@ -21742,9 +21807,6 @@ var CTX_MEMORY_ACTIONS = [
21742
21807
  ];
21743
21808
 
21744
21809
  // 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
21810
  var MEMORY_CATEGORIES = new Set(CATEGORY_PRIORITY);
21749
21811
  function isMemoryCategory2(value) {
21750
21812
  return MEMORY_CATEGORIES.has(value);
@@ -21756,31 +21818,12 @@ function normalizeLimit(limit) {
21756
21818
  return Math.max(1, Math.floor(limit));
21757
21819
  }
21758
21820
  function getAllowedActions(deps) {
21759
- const allowed = deps.allowedActions?.length ? deps.allowedActions : [...CTX_MEMORY_ACTIONS];
21821
+ const allowed = deps.allowedActions?.length ? deps.allowedActions : [...CTX_MEMORY_DREAMER_ACTIONS];
21760
21822
  return [...allowed];
21761
21823
  }
21762
21824
  function normalizeCategory(category) {
21763
21825
  const trimmed = category?.trim();
21764
- return trimmed ? trimmed : undefined;
21765
- }
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}`;
21826
+ return trimmed ? trimmed : undefined;
21784
21827
  }
21785
21828
  function formatMemoryList(memories) {
21786
21829
  if (memories.length === 0) {
@@ -21839,77 +21882,6 @@ function filterByCategory(memories, category) {
21839
21882
  }
21840
21883
  return memories.filter((memory) => memory.category === category);
21841
21884
  }
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
21885
  function queueMemoryEmbedding(deps, memoryId, content) {
21914
21886
  (async () => {
21915
21887
  const embedding2 = await embedText(content);
@@ -21942,13 +21914,12 @@ function createCtxMemoryTool(deps) {
21942
21914
  return tool2({
21943
21915
  description: CTX_MEMORY_DESCRIPTION,
21944
21916
  args: {
21945
- action: tool2.schema.enum(CTX_MEMORY_ACTIONS).describe("Action to perform on memories"),
21917
+ action: tool2.schema.enum(CTX_MEMORY_DREAMER_ACTIONS).describe("Action to perform on memories"),
21946
21918
  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)"),
21919
+ category: tool2.schema.string().optional().describe("Memory category (required for write, optional filter for list, optional override for merge)"),
21948
21920
  id: tool2.schema.number().optional().describe("Memory ID (required for delete, update, archive)"),
21949
21921
  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)"),
21922
+ limit: tool2.schema.number().optional().describe("Maximum results to return for list (default: 10)"),
21952
21923
  reason: tool2.schema.string().optional().describe("Archive reason (optional for archive)")
21953
21924
  },
21954
21925
  async execute(args, toolContext) {
@@ -22103,30 +22074,6 @@ function createCtxMemoryTool(deps) {
22103
22074
  archiveMemory(deps.db, args.id, args.reason);
22104
22075
  return args.reason?.trim() ? `Archived memory [ID: ${args.id}] (${args.reason.trim()}).` : `Archived memory [ID: ${args.id}].`;
22105
22076
  }
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
22077
  return "Error: Unknown action.";
22131
22078
  }
22132
22079
  });
@@ -22331,8 +22278,458 @@ function createCtxReduceTools(deps) {
22331
22278
  ctx_reduce: createCtxReduceTool(deps)
22332
22279
  };
22333
22280
  }
22334
- // src/plugin/normalize-tool-arg-schemas.ts
22281
+ // src/tools/ctx-search/constants.ts
22282
+ var CTX_SEARCH_TOOL_NAME = "ctx_search";
22283
+ 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.";
22284
+ var DEFAULT_CTX_SEARCH_LIMIT = 10;
22285
+ // src/tools/ctx-search/tools.ts
22335
22286
  import { tool as tool5 } from "@opencode-ai/plugin";
22287
+
22288
+ // src/features/magic-context/message-index.ts
22289
+ var lastIndexedStatements = new WeakMap;
22290
+ var insertMessageStatements = new WeakMap;
22291
+ var upsertIndexStatements = new WeakMap;
22292
+ var deleteFtsStatements = new WeakMap;
22293
+ var deleteIndexStatements = new WeakMap;
22294
+ function normalizeIndexText(text) {
22295
+ return text.replace(/\s+/g, " ").trim();
22296
+ }
22297
+ function getLastIndexedStatement(db) {
22298
+ let stmt = lastIndexedStatements.get(db);
22299
+ if (!stmt) {
22300
+ stmt = db.prepare("SELECT last_indexed_ordinal FROM message_history_index WHERE session_id = ?");
22301
+ lastIndexedStatements.set(db, stmt);
22302
+ }
22303
+ return stmt;
22304
+ }
22305
+ function getInsertMessageStatement(db) {
22306
+ let stmt = insertMessageStatements.get(db);
22307
+ if (!stmt) {
22308
+ stmt = db.prepare("INSERT INTO message_history_fts (session_id, message_ordinal, message_id, role, content) VALUES (?, ?, ?, ?, ?)");
22309
+ insertMessageStatements.set(db, stmt);
22310
+ }
22311
+ return stmt;
22312
+ }
22313
+ function getUpsertIndexStatement(db) {
22314
+ let stmt = upsertIndexStatements.get(db);
22315
+ if (!stmt) {
22316
+ 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");
22317
+ upsertIndexStatements.set(db, stmt);
22318
+ }
22319
+ return stmt;
22320
+ }
22321
+ function getDeleteFtsStatement(db) {
22322
+ let stmt = deleteFtsStatements.get(db);
22323
+ if (!stmt) {
22324
+ stmt = db.prepare("DELETE FROM message_history_fts WHERE session_id = ?");
22325
+ deleteFtsStatements.set(db, stmt);
22326
+ }
22327
+ return stmt;
22328
+ }
22329
+ function getDeleteIndexStatement(db) {
22330
+ let stmt = deleteIndexStatements.get(db);
22331
+ if (!stmt) {
22332
+ stmt = db.prepare("DELETE FROM message_history_index WHERE session_id = ?");
22333
+ deleteIndexStatements.set(db, stmt);
22334
+ }
22335
+ return stmt;
22336
+ }
22337
+ function getLastIndexedOrdinal(db, sessionId) {
22338
+ const row = getLastIndexedStatement(db).get(sessionId);
22339
+ return typeof row?.last_indexed_ordinal === "number" ? row.last_indexed_ordinal : 0;
22340
+ }
22341
+ function clearIndexedMessages(db, sessionId) {
22342
+ getDeleteFtsStatement(db).run(sessionId);
22343
+ getDeleteIndexStatement(db).run(sessionId);
22344
+ }
22345
+ function getIndexableContent(role, parts) {
22346
+ if (role === "user") {
22347
+ if (!hasMeaningfulUserText(parts)) {
22348
+ return "";
22349
+ }
22350
+ return extractTexts(parts).map(cleanUserText).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
22351
+ }
22352
+ if (role === "assistant") {
22353
+ return extractTexts(parts).map(removeSystemReminders).map(normalizeIndexText).filter((text) => text.length > 0).join(" / ");
22354
+ }
22355
+ return "";
22356
+ }
22357
+ function ensureMessagesIndexed(db, sessionId, readMessages) {
22358
+ const messages = readMessages(sessionId);
22359
+ if (messages.length === 0) {
22360
+ db.transaction(() => clearIndexedMessages(db, sessionId))();
22361
+ return;
22362
+ }
22363
+ let lastIndexedOrdinal = getLastIndexedOrdinal(db, sessionId);
22364
+ if (lastIndexedOrdinal > messages.length) {
22365
+ db.transaction(() => clearIndexedMessages(db, sessionId))();
22366
+ lastIndexedOrdinal = 0;
22367
+ }
22368
+ if (lastIndexedOrdinal >= messages.length) {
22369
+ return;
22370
+ }
22371
+ const messagesToInsert = messages.filter((message) => message.ordinal > lastIndexedOrdinal).filter((message) => message.role === "user" || message.role === "assistant").map((message) => ({
22372
+ ordinal: message.ordinal,
22373
+ id: message.id,
22374
+ role: message.role,
22375
+ content: getIndexableContent(message.role, message.parts)
22376
+ })).filter((message) => message.content.length > 0);
22377
+ const now = Date.now();
22378
+ db.transaction(() => {
22379
+ const insertMessage = getInsertMessageStatement(db);
22380
+ for (const message of messagesToInsert) {
22381
+ insertMessage.run(sessionId, message.ordinal, message.id, message.role, message.content);
22382
+ }
22383
+ getUpsertIndexStatement(db).run(sessionId, messages.length, now);
22384
+ })();
22385
+ }
22386
+
22387
+ // src/features/magic-context/search.ts
22388
+ var DEFAULT_UNIFIED_SEARCH_LIMIT = 10;
22389
+ var SEMANTIC_WEIGHT = 0.7;
22390
+ var FTS_WEIGHT = 0.3;
22391
+ var SINGLE_SOURCE_PENALTY = 0.8;
22392
+ var RESULT_PREVIEW_LIMIT = 220;
22393
+ var MEMORY_SOURCE_BOOST = 1.3;
22394
+ var FACT_SOURCE_BOOST = 1.15;
22395
+ var MESSAGE_SOURCE_BOOST = 1;
22396
+ var messageSearchStatements = new WeakMap;
22397
+ function normalizeLimit2(limit) {
22398
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
22399
+ return DEFAULT_UNIFIED_SEARCH_LIMIT;
22400
+ }
22401
+ return Math.max(1, Math.floor(limit));
22402
+ }
22403
+ function normalizeCosineScore(score) {
22404
+ if (!Number.isFinite(score)) {
22405
+ return 0;
22406
+ }
22407
+ return Math.min(1, Math.max(0, score));
22408
+ }
22409
+ function previewText(text) {
22410
+ const normalized = text.replace(/\s+/g, " ").trim();
22411
+ if (normalized.length <= RESULT_PREVIEW_LIMIT) {
22412
+ return normalized;
22413
+ }
22414
+ return `${normalized.slice(0, RESULT_PREVIEW_LIMIT - 1).trimEnd()}\u2026`;
22415
+ }
22416
+ function tokenizeQuery(query) {
22417
+ return Array.from(new Set(query.toLowerCase().split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0)));
22418
+ }
22419
+ function scoreTextMatch(content, query, extraText = "") {
22420
+ const tokens = tokenizeQuery(query);
22421
+ if (tokens.length === 0) {
22422
+ return 0;
22423
+ }
22424
+ const haystack = `${content} ${extraText}`.toLowerCase();
22425
+ const queryLower = query.trim().toLowerCase();
22426
+ let matchedTokens = 0;
22427
+ for (const token of tokens) {
22428
+ if (haystack.includes(token)) {
22429
+ matchedTokens++;
22430
+ }
22431
+ }
22432
+ if (matchedTokens === 0) {
22433
+ return 0;
22434
+ }
22435
+ let score = matchedTokens / tokens.length;
22436
+ if (queryLower.length > 0 && haystack.includes(queryLower)) {
22437
+ score += 0.35;
22438
+ }
22439
+ return Math.min(1, score);
22440
+ }
22441
+ function getMessageSearchStatement(db) {
22442
+ let stmt = messageSearchStatements.get(db);
22443
+ if (!stmt) {
22444
+ 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 ?");
22445
+ messageSearchStatements.set(db, stmt);
22446
+ }
22447
+ return stmt;
22448
+ }
22449
+ function getMessageOrdinal(value) {
22450
+ if (typeof value === "number" && Number.isFinite(value)) {
22451
+ return value;
22452
+ }
22453
+ if (typeof value === "string" && value.trim().length > 0) {
22454
+ const parsed = Number.parseInt(value, 10);
22455
+ return Number.isFinite(parsed) ? parsed : null;
22456
+ }
22457
+ return null;
22458
+ }
22459
+ async function getSemanticScores(args) {
22460
+ const semanticScores = new Map;
22461
+ if (!args.embeddingEnabled || !args.isEmbeddingRuntimeEnabled() || args.memories.length === 0) {
22462
+ return semanticScores;
22463
+ }
22464
+ const queryEmbedding = await args.embedQuery(args.query);
22465
+ if (!queryEmbedding) {
22466
+ return semanticScores;
22467
+ }
22468
+ const embeddings = await ensureMemoryEmbeddings({
22469
+ db: args.db,
22470
+ memories: args.memories,
22471
+ existingEmbeddings: loadAllEmbeddings(args.db, args.projectPath)
22472
+ });
22473
+ for (const memory of args.memories) {
22474
+ const memoryEmbedding = embeddings.get(memory.id);
22475
+ if (!memoryEmbedding) {
22476
+ continue;
22477
+ }
22478
+ semanticScores.set(memory.id, normalizeCosineScore(cosineSimilarity(queryEmbedding, memoryEmbedding)));
22479
+ }
22480
+ return semanticScores;
22481
+ }
22482
+ function getFtsScores(args) {
22483
+ try {
22484
+ const matches = searchMemoriesFTS(args.db, args.projectPath, args.query, args.limit);
22485
+ return new Map(matches.map((memory, rank) => [memory.id, 1 / (rank + 1)]));
22486
+ } catch {
22487
+ return new Map;
22488
+ }
22489
+ }
22490
+ function mergeMemoryResults(args) {
22491
+ const memoryById = new Map(args.memories.map((memory) => [memory.id, memory]));
22492
+ const candidateIds = new Set([...args.semanticScores.keys(), ...args.ftsScores.keys()]);
22493
+ const results = [];
22494
+ for (const id of candidateIds) {
22495
+ const memory = memoryById.get(id);
22496
+ if (!memory) {
22497
+ continue;
22498
+ }
22499
+ const semanticScore = args.semanticScores.get(id);
22500
+ const ftsScore = args.ftsScores.get(id);
22501
+ let score = 0;
22502
+ let matchType = "fts";
22503
+ if (semanticScore !== undefined && ftsScore !== undefined) {
22504
+ score = SEMANTIC_WEIGHT * semanticScore + FTS_WEIGHT * ftsScore;
22505
+ matchType = "hybrid";
22506
+ } else if (semanticScore !== undefined) {
22507
+ score = semanticScore * SINGLE_SOURCE_PENALTY;
22508
+ matchType = "semantic";
22509
+ } else if (ftsScore !== undefined) {
22510
+ score = ftsScore * SINGLE_SOURCE_PENALTY;
22511
+ matchType = "fts";
22512
+ }
22513
+ if (score <= 0) {
22514
+ continue;
22515
+ }
22516
+ results.push({
22517
+ source: "memory",
22518
+ content: previewText(memory.content),
22519
+ score,
22520
+ memoryId: memory.id,
22521
+ category: memory.category,
22522
+ matchType
22523
+ });
22524
+ }
22525
+ return results.sort((left, right) => {
22526
+ if (right.score !== left.score) {
22527
+ return right.score - left.score;
22528
+ }
22529
+ return left.memoryId - right.memoryId;
22530
+ }).slice(0, args.limit);
22531
+ }
22532
+ async function searchMemories(args) {
22533
+ if (!args.memoryEnabled) {
22534
+ return [];
22535
+ }
22536
+ const memories = getMemoriesByProject(args.db, args.projectPath);
22537
+ if (memories.length === 0) {
22538
+ return [];
22539
+ }
22540
+ const semanticScores = await getSemanticScores({
22541
+ db: args.db,
22542
+ projectPath: args.projectPath,
22543
+ query: args.query,
22544
+ memories,
22545
+ embeddingEnabled: args.embeddingEnabled,
22546
+ embedQuery: args.embedQuery,
22547
+ isEmbeddingRuntimeEnabled: args.isEmbeddingRuntimeEnabled
22548
+ });
22549
+ const ftsScores = getFtsScores(args);
22550
+ return mergeMemoryResults({
22551
+ memories,
22552
+ semanticScores,
22553
+ ftsScores,
22554
+ limit: args.limit
22555
+ });
22556
+ }
22557
+ function searchFacts(args) {
22558
+ return getSessionFacts(args.db, args.sessionId).map((fact) => ({
22559
+ fact,
22560
+ score: scoreTextMatch(fact.content, args.query, fact.category)
22561
+ })).filter((candidate) => candidate.score > 0).sort((left, right) => {
22562
+ if (right.score !== left.score) {
22563
+ return right.score - left.score;
22564
+ }
22565
+ return left.fact.id - right.fact.id;
22566
+ }).slice(0, args.limit).map(({ fact, score }) => ({
22567
+ source: "fact",
22568
+ content: previewText(fact.content),
22569
+ score,
22570
+ factId: fact.id,
22571
+ factCategory: fact.category
22572
+ }));
22573
+ }
22574
+ function searchMessages(args) {
22575
+ ensureMessagesIndexed(args.db, args.sessionId, args.readMessages);
22576
+ const sanitizedQuery = sanitizeFtsQuery(args.query.trim());
22577
+ if (sanitizedQuery.length === 0) {
22578
+ return [];
22579
+ }
22580
+ const rows = getMessageSearchStatement(args.db).all(args.sessionId, sanitizedQuery, args.limit).map((row) => row);
22581
+ return rows.map((row, rank) => {
22582
+ const messageOrdinal = getMessageOrdinal(row.messageOrdinal);
22583
+ if (messageOrdinal === null || typeof row.messageId !== "string" || typeof row.role !== "string" || typeof row.content !== "string") {
22584
+ return null;
22585
+ }
22586
+ return {
22587
+ source: "message",
22588
+ content: previewText(row.content),
22589
+ score: 1 / (rank + 1),
22590
+ messageOrdinal,
22591
+ messageId: row.messageId,
22592
+ role: row.role
22593
+ };
22594
+ }).filter((result) => result !== null);
22595
+ }
22596
+ function getSourceBoost(result) {
22597
+ switch (result.source) {
22598
+ case "memory":
22599
+ return MEMORY_SOURCE_BOOST;
22600
+ case "fact":
22601
+ return FACT_SOURCE_BOOST;
22602
+ case "message":
22603
+ return MESSAGE_SOURCE_BOOST;
22604
+ }
22605
+ }
22606
+ function compareUnifiedResults(left, right) {
22607
+ const leftEffective = left.score * getSourceBoost(left);
22608
+ const rightEffective = right.score * getSourceBoost(right);
22609
+ if (rightEffective !== leftEffective) {
22610
+ return rightEffective - leftEffective;
22611
+ }
22612
+ if (left.source === "memory" && right.source === "memory") {
22613
+ return left.memoryId - right.memoryId;
22614
+ }
22615
+ if (left.source === "fact" && right.source === "fact") {
22616
+ return left.factId - right.factId;
22617
+ }
22618
+ if (left.source === "message" && right.source === "message") {
22619
+ return left.messageOrdinal - right.messageOrdinal;
22620
+ }
22621
+ return 0;
22622
+ }
22623
+ async function unifiedSearch(db, sessionId, projectPath, query, options = {}) {
22624
+ const trimmedQuery = query.trim();
22625
+ if (trimmedQuery.length === 0) {
22626
+ return [];
22627
+ }
22628
+ const limit = normalizeLimit2(options.limit);
22629
+ const tierLimit = Math.max(limit * 3, DEFAULT_UNIFIED_SEARCH_LIMIT);
22630
+ const [memoryResults, factResults, messageResults] = await Promise.all([
22631
+ searchMemories({
22632
+ db,
22633
+ projectPath,
22634
+ query: trimmedQuery,
22635
+ limit: tierLimit,
22636
+ memoryEnabled: options.memoryEnabled ?? true,
22637
+ embeddingEnabled: options.embeddingEnabled ?? true,
22638
+ embedQuery: options.embedQuery ?? embedText,
22639
+ isEmbeddingRuntimeEnabled: options.isEmbeddingRuntimeEnabled ?? isEmbeddingEnabled
22640
+ }),
22641
+ Promise.resolve(searchFacts({ db, sessionId, query: trimmedQuery, limit: tierLimit })),
22642
+ Promise.resolve(searchMessages({
22643
+ db,
22644
+ sessionId,
22645
+ query: trimmedQuery,
22646
+ limit: tierLimit,
22647
+ readMessages: options.readMessages ?? readRawSessionMessages
22648
+ }))
22649
+ ]);
22650
+ const results = [...memoryResults, ...factResults, ...messageResults].sort(compareUnifiedResults).slice(0, limit);
22651
+ const memoryIds = results.filter((result) => result.source === "memory").map((result) => result.memoryId);
22652
+ if (memoryIds.length > 0) {
22653
+ db.transaction(() => {
22654
+ for (const memoryId of memoryIds) {
22655
+ updateMemoryRetrievalCount(db, memoryId);
22656
+ }
22657
+ })();
22658
+ }
22659
+ return results;
22660
+ }
22661
+
22662
+ // src/tools/ctx-search/tools.ts
22663
+ function normalizeLimit3(limit) {
22664
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
22665
+ return DEFAULT_CTX_SEARCH_LIMIT;
22666
+ }
22667
+ return Math.max(1, Math.floor(limit));
22668
+ }
22669
+ function formatResult(result, index) {
22670
+ if (result.source === "memory") {
22671
+ return [
22672
+ `[${index}] [memory] score=${result.score.toFixed(2)} id=${result.memoryId} category=${result.category} match=${result.matchType}`,
22673
+ result.content
22674
+ ].join(`
22675
+ `);
22676
+ }
22677
+ if (result.source === "fact") {
22678
+ return [
22679
+ `[${index}] [fact] score=${result.score.toFixed(2)} category=${result.factCategory} id=${result.factId}`,
22680
+ result.content
22681
+ ].join(`
22682
+ `);
22683
+ }
22684
+ const expandStart = Math.max(1, result.messageOrdinal - 3);
22685
+ const expandEnd = result.messageOrdinal + 3;
22686
+ return [
22687
+ `[${index}] [message] score=${result.score.toFixed(2)} ordinal=${result.messageOrdinal} role=${result.role}`,
22688
+ result.content,
22689
+ `Expand with ctx_expand(start=${expandStart}, end=${expandEnd}).`
22690
+ ].join(`
22691
+ `);
22692
+ }
22693
+ function formatSearchResults(query, results) {
22694
+ if (results.length === 0) {
22695
+ return `No results found for "${query}" across memories, session facts, or message history.`;
22696
+ }
22697
+ const body = results.map((result, index) => formatResult(result, index + 1)).join(`
22698
+
22699
+ `);
22700
+ return `Found ${results.length} result${results.length === 1 ? "" : "s"} for "${query}":
22701
+
22702
+ ${body}`;
22703
+ }
22704
+ function createCtxSearchTool(deps) {
22705
+ return tool5({
22706
+ description: CTX_SEARCH_DESCRIPTION,
22707
+ args: {
22708
+ query: tool5.schema.string().describe("Search query across memories, facts, and conversation history."),
22709
+ limit: tool5.schema.number().optional().describe("Maximum results to return (default: 10)")
22710
+ },
22711
+ async execute(args, toolContext) {
22712
+ const query = args.query?.trim();
22713
+ if (!query) {
22714
+ return "Error: 'query' is required.";
22715
+ }
22716
+ const results = await unifiedSearch(deps.db, toolContext.sessionID, deps.projectPath, query, {
22717
+ limit: normalizeLimit3(args.limit),
22718
+ memoryEnabled: deps.memoryEnabled,
22719
+ embeddingEnabled: deps.embeddingEnabled,
22720
+ readMessages: deps.readMessages
22721
+ });
22722
+ return formatSearchResults(query, results);
22723
+ }
22724
+ });
22725
+ }
22726
+ function createCtxSearchTools(deps) {
22727
+ return {
22728
+ [CTX_SEARCH_TOOL_NAME]: createCtxSearchTool(deps)
22729
+ };
22730
+ }
22731
+ // src/plugin/normalize-tool-arg-schemas.ts
22732
+ import { tool as tool6 } from "@opencode-ai/plugin";
22336
22733
  function stripRootJsonSchemaFields(jsonSchema) {
22337
22734
  const { $schema: _schema, ...rest } = jsonSchema;
22338
22735
  return rest;
@@ -22345,7 +22742,7 @@ function attachJsonSchemaOverride(schema) {
22345
22742
  const originalOverride = schema._zod.toJSONSchema;
22346
22743
  delete schema._zod.toJSONSchema;
22347
22744
  try {
22348
- return stripRootJsonSchemaFields(tool5.schema.toJSONSchema(schema));
22745
+ return stripRootJsonSchemaFields(tool6.schema.toJSONSchema(schema));
22349
22746
  } finally {
22350
22747
  schema._zod.toJSONSchema = originalOverride;
22351
22748
  }
@@ -22386,20 +22783,27 @@ function createToolRegistry(args) {
22386
22783
  console.warn(`[magic-context] embedding model changed from ${storedModelId} to ${currentModelId}; cleared embeddings for project ${projectPath}`);
22387
22784
  }
22388
22785
  }
22786
+ const ctxReduceEnabled = pluginConfig.ctx_reduce_enabled !== false;
22389
22787
  const allTools = {
22390
- ...createCtxReduceTools({
22788
+ ...ctxReduceEnabled ? createCtxReduceTools({
22391
22789
  db,
22392
22790
  protectedTags: pluginConfig.protected_tags ?? DEFAULT_PROTECTED_TAGS
22393
- }),
22791
+ }) : {},
22394
22792
  ...createCtxExpandTools(),
22395
22793
  ...createCtxNoteTools({ db }),
22794
+ ...createCtxSearchTools({
22795
+ db,
22796
+ projectPath,
22797
+ memoryEnabled,
22798
+ embeddingEnabled: embeddingConfig2.provider !== "off"
22799
+ }),
22396
22800
  ...memoryEnabled ? {
22397
22801
  ...createCtxMemoryTools({
22398
22802
  db,
22399
22803
  projectPath,
22400
22804
  memoryEnabled: true,
22401
22805
  embeddingEnabled: embeddingConfig2.provider !== "off",
22402
- allowedActions: ["write", "delete", "search"]
22806
+ allowedActions: ["write", "delete"]
22403
22807
  })
22404
22808
  } : {}
22405
22809
  };
@@ -22499,12 +22903,18 @@ var plugin = async (ctx) => {
22499
22903
  ctx,
22500
22904
  pluginConfig
22501
22905
  });
22502
- const tools4 = createToolRegistry({
22906
+ const tools5 = createToolRegistry({
22503
22907
  ctx,
22504
22908
  pluginConfig
22505
22909
  });
22910
+ if (pluginConfig.dreamer) {
22911
+ startDreamScheduleTimer({
22912
+ client: ctx.client,
22913
+ dreamerConfig: pluginConfig.dreamer
22914
+ });
22915
+ }
22506
22916
  return {
22507
- tool: tools4,
22917
+ tool: tools5,
22508
22918
  event: createEventHandler({ magicContext: hooks.magicContext }),
22509
22919
  "experimental.chat.messages.transform": createMessagesTransformHandler({
22510
22920
  magicContext: hooks.magicContext