@cortexkit/opencode-magic-context 0.1.2 → 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 (55) 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/inject-compartments.d.ts +3 -1
  26. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/read-session-chunk.d.ts +5 -0
  28. package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
  29. package/dist/hooks/magic-context/system-prompt-hash.d.ts +1 -0
  30. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1961 -1555
  36. package/dist/plugin/dream-timer.d.ts +14 -0
  37. package/dist/plugin/dream-timer.d.ts.map +1 -0
  38. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  39. package/dist/plugin/tool-registry.d.ts.map +1 -1
  40. package/dist/tools/ctx-memory/constants.d.ts +1 -1
  41. package/dist/tools/ctx-memory/constants.d.ts.map +1 -1
  42. package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
  43. package/dist/tools/ctx-memory/types.d.ts +3 -10
  44. package/dist/tools/ctx-memory/types.d.ts.map +1 -1
  45. package/dist/tools/ctx-search/constants.d.ts +4 -0
  46. package/dist/tools/ctx-search/constants.d.ts.map +1 -0
  47. package/dist/tools/ctx-search/index.d.ts +4 -0
  48. package/dist/tools/ctx-search/index.d.ts.map +1 -0
  49. package/dist/tools/ctx-search/tools.d.ts +4 -0
  50. package/dist/tools/ctx-search/tools.d.ts.map +1 -0
  51. package/dist/tools/ctx-search/types.d.ts +19 -0
  52. package/dist/tools/ctx-search/types.d.ts.map +1 -0
  53. package/dist/tools/index.d.ts +1 -0
  54. package/dist/tools/index.d.ts.map +1 -1
  55. 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
- };
14953
- }
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");
14964
- }
14965
- function getOpenCodeStorageDir() {
14966
- return path2.join(getDataDir(), "opencode", "storage");
14967
- }
14968
-
14969
- // src/features/magic-context/storage-db.ts
14970
- var databases = new Map;
14971
- var FALLBACK_DATABASE_KEY = "__fallback__:memory:";
14972
- var persistenceByDatabase = new WeakMap;
14973
- var persistenceErrorByDatabase = new WeakMap;
14974
- function resolveDatabasePath() {
14975
- const dbDir = join4(getOpenCodeStorageDir(), "plugin", "magic-context");
14976
- return { dbDir, dbPath: join4(dbDir, "context.db") };
14977
- }
14978
- function initializeDatabase(db) {
14979
- db.run("PRAGMA journal_mode=WAL");
14980
- db.run("PRAGMA busy_timeout=5000");
14981
- db.run("PRAGMA foreign_keys=ON");
14982
- db.run(`
14983
- CREATE TABLE IF NOT EXISTS tags (
14984
- id INTEGER PRIMARY KEY AUTOINCREMENT,
14985
- session_id TEXT,
14986
- message_id TEXT,
14987
- type TEXT,
14988
- status TEXT DEFAULT 'active',
14989
- byte_size INTEGER,
14990
- tag_number INTEGER,
14991
- UNIQUE(session_id, tag_number)
14992
- );
14993
-
14994
- CREATE TABLE IF NOT EXISTS pending_ops (
14995
- id INTEGER PRIMARY KEY AUTOINCREMENT,
14996
- session_id TEXT,
14997
- tag_id INTEGER,
14998
- operation TEXT,
14999
- queued_at INTEGER
15000
- );
15001
-
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
-
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");
15182
- }
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;
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);
15190
14957
  }
15191
- db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
14958
+ return stmt;
15192
14959
  }
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)}`);
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);
15200
14965
  }
14966
+ return stmt;
15201
14967
  }
15202
- function openDatabase() {
15203
- try {
15204
- const { dbDir, dbPath } = resolveDatabasePath();
15205
- const existing = databases.get(dbPath);
15206
- if (existing) {
15207
- if (!persistenceByDatabase.has(existing)) {
15208
- persistenceByDatabase.set(existing, true);
15209
- }
15210
- return existing;
15211
- }
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);
15227
- }
15228
- return existingFallback;
15229
- }
15230
- const fallback = createFallbackDatabase();
15231
- databases.set(FALLBACK_DATABASE_KEY, fallback);
15232
- persistenceByDatabase.set(fallback, false);
15233
- persistenceErrorByDatabase.set(fallback, errorMessage);
15234
- return fallback;
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);
15235
14973
  }
14974
+ return stmt;
15236
14975
  }
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
- };
14976
+ function getDreamState(db, key) {
14977
+ const row = getGetDreamStateStatement(db).get(key);
14978
+ return typeof row?.value === "string" ? row.value : null;
15281
14979
  }
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 ?? "");
14980
+ function setDreamState(db, key, value) {
14981
+ getSetDreamStateStatement(db).run(key, value);
15285
14982
  }
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
- };
14983
+ function deleteDreamState(db, key) {
14984
+ getDeleteDreamStateStatement(db).run(key);
15302
14985
  }
15303
14986
 
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";
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;
15310
14999
  }
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";
15000
+ function isLeaseActive(db) {
15001
+ const expiry = getLeaseExpiry(db);
15002
+ return expiry !== null && expiry > Date.now();
15316
15003
  }
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";
15004
+ function getLeaseHolder(db) {
15005
+ return getDreamState(db, LEASE_HOLDER_KEY);
15322
15006
  }
15323
- function loadPersistedUsage(db, sessionId) {
15324
- const result = db.prepare("SELECT last_context_percentage, last_input_tokens, last_response_time FROM session_meta WHERE session_id = ?").get(sessionId);
15325
- if (!isPersistedUsageRow(result) || result.last_context_percentage === 0 && result.last_input_tokens === 0) {
15326
- return null;
15327
- }
15328
- return {
15329
- usage: {
15330
- percentage: result.last_context_percentage,
15331
- inputTokens: result.last_input_tokens
15332
- },
15333
- updatedAt: result.last_response_time || Date.now()
15334
- };
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
+ })();
15335
15021
  }
15336
- function getPersistedNudgePlacement(db, sessionId) {
15337
- const result = db.prepare("SELECT nudge_anchor_message_id, nudge_anchor_text FROM session_meta WHERE session_id = ?").get(sessionId);
15338
- if (!isPersistedNudgePlacementRow(result)) {
15339
- return null;
15340
- }
15341
- if (result.nudge_anchor_message_id.length === 0 || result.nudge_anchor_text.length === 0) {
15342
- return null;
15343
- }
15344
- return {
15345
- messageId: result.nudge_anchor_message_id,
15346
- nudgeText: result.nudge_anchor_text
15347
- };
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
+ })();
15348
15032
  }
15349
- function setPersistedNudgePlacement(db, sessionId, messageId, nudgeText) {
15033
+ function releaseLease(db, holderId) {
15350
15034
  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);
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);
15353
15041
  })();
15354
15042
  }
15355
- function clearPersistedNudgePlacement(db, sessionId) {
15356
- db.prepare("UPDATE session_meta SET nudge_anchor_message_id = '', nudge_anchor_text = '' WHERE session_id = ?").run(sessionId);
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);
15048
+ if (existing) {
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
+ })();
15357
15060
  }
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)) {
15361
- return null;
15362
- }
15363
- if (result.sticky_turn_reminder_text.length === 0) {
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)
15364
15064
  return null;
15365
- }
15366
15065
  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
15066
+ id: row.id,
15067
+ projectIdentity: row.project_path,
15068
+ reason: row.reason,
15069
+ enqueuedAt: row.enqueued_at,
15070
+ startedAt: null
15369
15071
  };
15370
15072
  }
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);
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 };
15375
15083
  })();
15376
15084
  }
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);
15085
+ function removeDreamEntry(db, id) {
15086
+ db.prepare("DELETE FROM dream_queue WHERE id = ?").run(id);
15379
15087
  }
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;
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);
15389
15090
  }
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);
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}`);
15146
+ }
15147
+ } catch {
15148
+ log("[dreamer] could not resolve parent session \u2014 child sessions will be visible in UI");
15149
+ }
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
+ }
15249
+ }
15406
15250
  }
15251
+ } finally {
15252
+ releaseLease(args.db, holderId);
15253
+ log(`[dreamer] lease released: ${holderId}`);
15407
15254
  }
15408
- if (setClauses.length === 0) {
15409
- return;
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));
15410
15260
  }
15411
- db.transaction(() => {
15412
- ensureSessionMetaRow(db, sessionId);
15413
- db.prepare(`UPDATE session_meta SET ${setClauses.join(", ")} WHERE session_id = ?`).run(...values, sessionId);
15414
- })();
15415
- }
15416
- function clearSession(db, sessionId) {
15417
- db.transaction(() => {
15418
- db.prepare("DELETE FROM pending_ops WHERE session_id = ?").run(sessionId);
15419
- db.prepare("DELETE FROM source_contents WHERE session_id = ?").run(sessionId);
15420
- db.prepare("DELETE FROM tags WHERE session_id = ?").run(sessionId);
15421
- db.prepare("DELETE FROM session_meta WHERE session_id = ?").run(sessionId);
15422
- db.prepare("DELETE FROM compartments WHERE session_id = ?").run(sessionId);
15423
- db.prepare("DELETE FROM session_facts WHERE session_id = ?").run(sessionId);
15424
- db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
15425
- db.prepare("DELETE FROM recomp_compartments WHERE session_id = ?").run(sessionId);
15426
- db.prepare("DELETE FROM recomp_facts WHERE session_id = ?").run(sessionId);
15427
- })();
15428
- }
15429
- // src/features/magic-context/storage-notes.ts
15430
- function isSessionNoteRow(row) {
15431
- if (row === null || typeof row !== "object")
15432
- return false;
15433
- const candidate = row;
15434
- return typeof candidate.id === "number" && typeof candidate.session_id === "string" && typeof candidate.content === "string" && typeof candidate.created_at === "number";
15435
- }
15436
- function toSessionNote(row) {
15437
- return {
15438
- id: row.id,
15439
- sessionId: row.session_id,
15440
- content: row.content,
15441
- createdAt: row.created_at
15442
- };
15443
- }
15444
- function getSessionNotes(db, sessionId) {
15445
- const rows = db.prepare("SELECT * FROM session_notes WHERE session_id = ? ORDER BY id ASC").all(sessionId).filter(isSessionNoteRow);
15446
- return rows.map(toSessionNote);
15447
- }
15448
- function addSessionNote(db, sessionId, content) {
15449
- db.prepare("INSERT INTO session_notes (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, Date.now());
15450
- }
15451
- function clearSessionNotes(db, sessionId) {
15452
- db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
15453
- }
15454
- // src/features/magic-context/storage-ops.ts
15455
- function isPendingOpRow(row) {
15456
- if (row === null || typeof row !== "object")
15457
- return false;
15458
- const r = row;
15459
- return typeof r.id === "number" && typeof r.session_id === "string" && typeof r.tag_id === "number" && typeof r.operation === "string" && typeof r.queued_at === "number";
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;
15460
15266
  }
15461
- function toPendingOp(row) {
15462
- if (row.operation !== "drop") {
15463
- sessionLog(row.session_id, `unknown pending operation "${row.operation}"; ignoring`);
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) {
15464
15272
  return null;
15465
15273
  }
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;
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);
15290
+ return null;
15503
15291
  }
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);
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);
15509
15304
  }
15510
- return sources;
15305
+ return result;
15511
15306
  }
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);
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)
15311
+ return null;
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) {
15317
+ return null;
15519
15318
  }
15520
- return stmt;
15319
+ const startMinutes = startHour * 60 + startMin;
15320
+ const endMinutes = endHour * 60 + endMin;
15321
+ return { startMinutes, endMinutes };
15521
15322
  }
15522
- function isTagRow(row) {
15523
- if (row === null || typeof row !== "object")
15323
+ function isInScheduleWindow(schedule, now = new Date) {
15324
+ const window = parseScheduleWindow(schedule);
15325
+ if (!window)
15524
15326
  return false;
15525
- const r = row;
15526
- return typeof r.id === "number" && typeof r.message_id === "string" && typeof r.type === "string" && typeof r.status === "string" && typeof r.byte_size === "number" && typeof r.session_id === "string" && typeof r.tag_number === "number";
15527
- }
15528
- function toTagEntry(row) {
15529
- const type = row.type === "tool" ? "tool" : row.type === "file" ? "file" : "message";
15530
- const status = row.status === "dropped" || row.status === "compacted" ? row.status : "active";
15531
- return {
15532
- tagNumber: row.tag_number,
15533
- messageId: row.message_id,
15534
- type,
15535
- status,
15536
- byteSize: row.byte_size,
15537
- sessionId: row.session_id
15538
- };
15539
- }
15540
- function insertTag(db, sessionId, messageId, type, byteSize, tagNumber) {
15541
- getInsertTagStatement(db).run(sessionId, messageId, type, byteSize, tagNumber);
15542
- return tagNumber;
15543
- }
15544
- function updateTagStatus(db, sessionId, tagId, status) {
15545
- db.prepare("UPDATE tags SET status = ? WHERE session_id = ? AND tag_number = ?").run(status, sessionId, tagId);
15546
- }
15547
- function updateTagMessageId(db, sessionId, tagId, messageId) {
15548
- db.prepare("UPDATE tags SET message_id = ? WHERE session_id = ? AND tag_number = ?").run(messageId, sessionId, tagId);
15549
- }
15550
- function getTagsBySession(db, sessionId) {
15551
- const rows = db.prepare("SELECT id, message_id, type, status, byte_size, session_id, tag_number FROM tags WHERE session_id = ? ORDER BY tag_number ASC, id ASC").all(sessionId).filter(isTagRow);
15552
- return rows.map(toTagEntry);
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);
15560
- }
15561
- // src/features/magic-context/compaction.ts
15562
- function createCompactionHandler() {
15563
- return {
15564
- onCompacted(sessionId, db) {
15565
- db.transaction(() => {
15566
- db.prepare("UPDATE tags SET status = 'compacted' WHERE session_id = ? AND status IN ('active', 'dropped')").run(sessionId);
15567
- db.prepare("DELETE FROM pending_ops WHERE session_id = ?").run(sessionId);
15568
- })();
15569
- updateSessionMeta(db, sessionId, { lastNudgeBand: null });
15570
- }
15571
- };
15572
- }
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;
15327
+ const currentMinutes = now.getHours() * 60 + now.getMinutes();
15328
+ if (window.startMinutes <= window.endMinutes) {
15329
+ return currentMinutes >= window.startMinutes && currentMinutes < window.endMinutes;
15593
15330
  }
15594
- return DEFAULT_CONTEXT_LIMIT;
15331
+ return currentMinutes >= window.startMinutes || currentMinutes < window.endMinutes;
15595
15332
  }
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];
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);
15607
15344
  }
15608
15345
  }
15609
- return cacheTtl.default ?? "5m";
15346
+ return projects;
15610
15347
  }
15611
- function resolveExecuteThreshold(config2, modelKey, fallback) {
15612
- if (typeof config2 === "number") {
15613
- return config2;
15348
+ function checkScheduleAndEnqueue(db, schedule) {
15349
+ if (!isInScheduleWindow(schedule)) {
15350
+ return 0;
15614
15351
  }
15615
- if (modelKey && typeof config2[modelKey] === "number") {
15616
- return config2[modelKey];
15352
+ const projects = findProjectsNeedingDream(db);
15353
+ if (projects.length === 0) {
15354
+ return 0;
15617
15355
  }
15618
- if (modelKey) {
15619
- const bareModelId = modelKey.split("/").slice(1).join("/");
15620
- if (bareModelId && typeof config2[bareModelId] === "number") {
15621
- return config2[bareModelId];
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++;
15622
15362
  }
15623
15363
  }
15624
- return config2.default ?? fallback;
15364
+ return enqueued;
15625
15365
  }
15626
- function resolveModelKey(providerID, modelID) {
15627
- if (!providerID || !modelID) {
15628
- return;
15629
- }
15630
- return `${providerID}/${modelID}`;
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");
15376
+ }
15377
+ function getOpenCodeStorageDir() {
15378
+ return path2.join(getDataDir(), "opencode", "storage");
15379
+ }
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");
15631
15610
  }
15632
- function resolveSessionId(properties) {
15633
- if (typeof properties?.sessionID === "string") {
15634
- return properties.sessionID;
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}`);
15635
15614
  }
15636
- const info = properties?.info;
15637
- if (info === null || typeof info !== "object") {
15615
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all();
15616
+ if (rows.some((row) => row.name === column)) {
15638
15617
  return;
15639
15618
  }
15640
- const record2 = info;
15641
- if (typeof record2.sessionID === "string") {
15642
- return record2.sessionID;
15643
- }
15644
- if (typeof record2.id === "string") {
15645
- return record2.id;
15646
- }
15647
- return;
15619
+ db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
15648
15620
  }
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);
15662
- }
15663
- const match = normalizedTtl.match(TTL_PATTERN);
15664
- if (!match) {
15665
- throw new Error(`Invalid cache TTL format: ${ttl}`);
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)}`);
15666
15628
  }
15667
- const value = Number(match[1]);
15668
- const unit = match[2];
15669
- return value * UNIT_TO_MS[unit];
15670
15629
  }
15671
- function createScheduler(config2) {
15672
- 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";
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);
15692
15637
  }
15693
- return "defer";
15694
- }
15695
- };
15696
- }
15697
-
15698
- // src/features/magic-context/tagger.ts
15699
- var GET_COUNTER_SQL = `SELECT counter FROM session_meta WHERE session_id = ?`;
15700
- var GET_ASSIGNMENTS_SQL = "SELECT message_id, tag_number FROM tags WHERE session_id = ? ORDER BY tag_number ASC";
15701
- function isAssignmentRow(row) {
15702
- if (row === null || typeof row !== "object") {
15703
- return false;
15704
- }
15705
- const candidate = row;
15706
- return typeof candidate.message_id === "string" && typeof candidate.tag_number === "number";
15707
- }
15708
- var UPSERT_COUNTER_SQL = `
15709
- INSERT INTO session_meta (session_id, counter)
15710
- VALUES (?, ?)
15711
- ON CONFLICT(session_id) DO UPDATE SET counter = excluded.counter
15712
- `;
15713
- var upsertCounterStatements = new WeakMap;
15714
- function getUpsertCounterStatement(db) {
15715
- let stmt = upsertCounterStatements.get(db);
15716
- if (!stmt) {
15717
- stmt = db.prepare(UPSERT_COUNTER_SQL);
15718
- upsertCounterStatements.set(db, stmt);
15719
- }
15720
- return stmt;
15721
- }
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
15638
  return existing;
15738
15639
  }
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;
15765
- }
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;
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);
15779
15655
  }
15656
+ return existingFallback;
15780
15657
  }
15781
- const counter = Math.max(row?.counter ?? 0, maxTagNumber);
15782
- counters.set(sessionId, counter);
15783
- }
15784
- function cleanup(sessionId) {
15785
- counters.delete(sessionId);
15786
- assignments.delete(sessionId);
15658
+ const fallback = createFallbackDatabase();
15659
+ databases.set(FALLBACK_DATABASE_KEY, fallback);
15660
+ persistenceByDatabase.set(fallback, false);
15661
+ persistenceErrorByDatabase.set(fallback, errorMessage);
15662
+ return fallback;
15787
15663
  }
15664
+ }
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) {
15688
+ if (row === null || typeof row !== "object")
15689
+ return false;
15690
+ const r = row;
15691
+ return typeof r.session_id === "string" && typeof r.last_response_time === "number" && typeof r.cache_ttl === "string" && typeof r.counter === "number" && typeof r.last_nudge_tokens === "number" && typeof r.last_nudge_band === "string" && typeof r.last_transform_error === "string" && typeof r.is_subagent === "number" && typeof r.last_context_percentage === "number" && typeof r.last_input_tokens === "number" && typeof r.times_execute_threshold_reached === "number" && typeof r.compartment_in_progress === "number" && (typeof r.system_prompt_hash === "string" || typeof r.system_prompt_hash === "number");
15692
+ }
15693
+ function getDefaultSessionMeta(sessionId) {
15788
15694
  return {
15789
- assignTag,
15790
- getTag,
15791
- bindTag,
15792
- getAssignments,
15793
- resetCounter,
15794
- getCounter,
15795
- initFromDb,
15796
- cleanup
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: ""
15708
+ };
15709
+ }
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 ?? "");
15713
+ }
15714
+ function toSessionMeta(row) {
15715
+ return {
15716
+ sessionId: row.session_id,
15717
+ lastResponseTime: row.last_response_time,
15718
+ cacheTtl: row.cache_ttl,
15719
+ counter: row.counter,
15720
+ lastNudgeTokens: row.last_nudge_tokens,
15721
+ lastNudgeBand: row.last_nudge_band.length > 0 ? row.last_nudge_band : null,
15722
+ lastTransformError: row.last_transform_error.length > 0 ? row.last_transform_error : null,
15723
+ isSubagent: row.is_subagent === 1,
15724
+ lastContextPercentage: row.last_context_percentage,
15725
+ lastInputTokens: row.last_input_tokens,
15726
+ timesExecuteThresholdReached: row.times_execute_threshold_reached,
15727
+ compartmentInProgress: row.compartment_in_progress === 1,
15728
+ systemPromptHash: String(row.system_prompt_hash)
15797
15729
  };
15798
15730
  }
15799
15731
 
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);
15809
- }
15810
- return stmt;
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";
15811
15738
  }
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);
15817
- }
15818
- return stmt;
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";
15819
15744
  }
15820
- function getDeleteDreamStateStatement(db) {
15821
- let stmt = deleteDreamStateStatements.get(db);
15822
- if (!stmt) {
15823
- stmt = db.prepare("DELETE FROM dream_state WHERE key = ?");
15824
- deleteDreamStateStatements.set(db, stmt);
15745
+ function isPersistedStickyTurnReminderRow(row) {
15746
+ if (row === null || typeof row !== "object")
15747
+ return false;
15748
+ const r = row;
15749
+ return typeof r.sticky_turn_reminder_text === "string" && typeof r.sticky_turn_reminder_message_id === "string";
15750
+ }
15751
+ function loadPersistedUsage(db, sessionId) {
15752
+ const result = db.prepare("SELECT last_context_percentage, last_input_tokens, last_response_time FROM session_meta WHERE session_id = ?").get(sessionId);
15753
+ if (!isPersistedUsageRow(result) || result.last_context_percentage === 0 && result.last_input_tokens === 0) {
15754
+ return null;
15825
15755
  }
15826
- return stmt;
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
+ };
15827
15763
  }
15828
- function getDreamState(db, key) {
15829
- const row = getGetDreamStateStatement(db).get(key);
15830
- return typeof row?.value === "string" ? row.value : null;
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;
15768
+ }
15769
+ if (result.nudge_anchor_message_id.length === 0 || result.nudge_anchor_text.length === 0) {
15770
+ return null;
15771
+ }
15772
+ return {
15773
+ messageId: result.nudge_anchor_message_id,
15774
+ nudgeText: result.nudge_anchor_text
15775
+ };
15831
15776
  }
15832
- function setDreamState(db, key, value) {
15833
- getSetDreamStateStatement(db).run(key, value);
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
+ })();
15834
15782
  }
15835
- function deleteDreamState(db, key) {
15836
- getDeleteDreamStateStatement(db).run(key);
15783
+ function clearPersistedNudgePlacement(db, sessionId) {
15784
+ db.prepare("UPDATE session_meta SET nudge_anchor_message_id = '', nudge_anchor_text = '' WHERE session_id = ?").run(sessionId);
15837
15785
  }
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) {
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)) {
15847
15789
  return null;
15848
15790
  }
15849
- const expiry = Number(value);
15850
- return Number.isFinite(expiry) ? expiry : null;
15791
+ if (result.sticky_turn_reminder_text.length === 0) {
15792
+ return null;
15793
+ }
15794
+ return {
15795
+ text: result.sticky_turn_reminder_text,
15796
+ messageId: result.sticky_turn_reminder_message_id.length > 0 ? result.sticky_turn_reminder_message_id : null
15797
+ };
15851
15798
  }
15852
- function isLeaseActive(db) {
15853
- const expiry = getLeaseExpiry(db);
15854
- return expiry !== null && expiry > Date.now();
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
+ })();
15855
15804
  }
15856
- function getLeaseHolder(db) {
15857
- return getDreamState(db, LEASE_HOLDER_KEY);
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);
15858
15807
  }
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
- })();
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);
15813
+ }
15814
+ const defaults = getDefaultSessionMeta(sessionId);
15815
+ ensureSessionMetaRow(db, sessionId);
15816
+ return defaults;
15873
15817
  }
15874
- function renewLease(db, holderId) {
15875
- return db.transaction(() => {
15876
- if (getLeaseHolder(db) !== holderId || !isLeaseActive(db)) {
15877
- return false;
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);
15878
15834
  }
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;
15835
+ }
15836
+ if (setClauses.length === 0) {
15837
+ return;
15838
+ }
15839
+ db.transaction(() => {
15840
+ ensureSessionMetaRow(db, sessionId);
15841
+ db.prepare(`UPDATE session_meta SET ${setClauses.join(", ")} WHERE session_id = ?`).run(...values, sessionId);
15883
15842
  })();
15884
15843
  }
15885
- function releaseLease(db, holderId) {
15844
+ function clearSession(db, sessionId) {
15886
15845
  db.transaction(() => {
15887
- if (getLeaseHolder(db) !== holderId) {
15888
- return;
15889
- }
15890
- deleteDreamState(db, LEASE_HOLDER_KEY);
15891
- deleteDreamState(db, LEASE_HEARTBEAT_KEY);
15892
- deleteDreamState(db, LEASE_EXPIRY_KEY);
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);
15893
15855
  })();
15894
15856
  }
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
- })();
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";
15912
15863
  }
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)
15864
+ function toSessionNote(row) {
15865
+ return {
15866
+ id: row.id,
15867
+ sessionId: row.session_id,
15868
+ content: row.content,
15869
+ createdAt: row.created_at
15870
+ };
15871
+ }
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`);
15916
15892
  return null;
15893
+ }
15917
15894
  return {
15918
15895
  id: row.id,
15919
- projectIdentity: row.project_path,
15920
- reason: row.reason,
15921
- enqueuedAt: row.enqueued_at,
15922
- startedAt: null
15896
+ sessionId: row.session_id,
15897
+ tagId: row.tag_id,
15898
+ operation: row.operation,
15899
+ queuedAt: row.queued_at
15923
15900
  };
15924
15901
  }
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
- })();
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);
15936
15904
  }
15937
- function removeDreamEntry(db, id) {
15938
- db.prepare("DELETE FROM dream_queue WHERE id = ?").run(id);
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);
15939
15908
  }
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);
15909
+ function removePendingOp(db, sessionId, tagId) {
15910
+ db.prepare("DELETE FROM pending_ops WHERE session_id = ? AND tag_id = ?").run(sessionId, tagId);
15942
15911
  }
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;
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";
15946
15918
  }
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;
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());
15951
15921
  }
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);
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());
15958
15927
  }
15959
- function resolveDreamSessionDirectory(projectIdentity) {
15960
- return dreamProjectDirectories.get(projectIdentity) ?? projectIdentity;
15928
+ function getSourceContents(db, sessionId, tagIds) {
15929
+ if (tagIds.length === 0) {
15930
+ return new Map;
15931
+ }
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;
15961
15939
  }
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: []
15940
+ // src/features/magic-context/storage-tags.ts
15941
+ var insertTagStatements = new WeakMap;
15942
+ function getInsertTagStatement(db) {
15943
+ let stmt = insertTagStatements.get(db);
15944
+ if (!stmt) {
15945
+ stmt = db.prepare("INSERT INTO tags (session_id, message_id, type, byte_size, tag_number) VALUES (?, ?, ?, ?, ?)");
15946
+ insertTagStatements.set(db, stmt);
15947
+ }
15948
+ return stmt;
15949
+ }
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";
15955
+ }
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
15970
15966
  };
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;
15967
+ }
15968
+ function insertTag(db, sessionId, messageId, type, byteSize, tagNumber) {
15969
+ getInsertTagStatement(db).run(sessionId, messageId, type, byteSize, tagNumber);
15970
+ return tagNumber;
15971
+ }
15972
+ function updateTagStatus(db, sessionId, tagId, status) {
15973
+ db.prepare("UPDATE tags SET status = ? WHERE session_id = ? AND tag_number = ?").run(status, sessionId, tagId);
15974
+ }
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);
15977
+ }
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);
15981
+ }
15982
+ function getTopNBySize(db, sessionId, n) {
15983
+ if (n <= 0) {
15984
+ return [];
15983
15985
  }
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
- }
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);
15988
+ }
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;
16002
15995
  }
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
- });
16081
- } catch (error48) {
16082
- const durationMs = Date.now() - taskStartedAt;
16083
- const errorMsg = getErrorMessage(error48);
16084
- log(`[dreamer] task ${taskName}: failed after ${(durationMs / 1000).toFixed(1)}s \u2014 ${errorMsg}`);
16085
- result.tasks.push({
16086
- name: taskName,
16087
- durationMs,
16088
- result: null,
16089
- error: errorMsg
16090
- });
16091
- } finally {
16092
- clearInterval(leaseRenewalInterval);
16093
- if (agentSessionId) {
16094
- await args.client.session.delete({
16095
- path: { id: agentSessionId },
16096
- query: { directory: args.sessionDirectory ?? args.projectIdentity }
16097
- }).catch((error48) => {
16098
- log("[dreamer] failed to delete child session:", error48);
16099
- });
16100
- }
16101
- }
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);
16102
16011
  }
16103
- } finally {
16104
- releaseLease(args.db, holderId);
16105
- log(`[dreamer] lease released: ${holderId}`);
16012
+ }, DREAM_TIMER_INTERVAL_MS);
16013
+ if (typeof timer === "object" && "unref" in timer) {
16014
+ timer.unref();
16106
16015
  }
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));
16016
+ log(`[dreamer] started independent schedule timer (every ${DREAM_TIMER_INTERVAL_MS / 60000}m)`);
16017
+ }
16018
+
16019
+ // src/plugin/event.ts
16020
+ function createEventHandler(args) {
16021
+ return async (input) => {
16022
+ await args.magicContext?.event?.(input);
16023
+ };
16024
+ }
16025
+
16026
+ // src/features/magic-context/compaction.ts
16027
+ function createCompactionHandler() {
16028
+ return {
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
+ }
16036
+ };
16037
+ }
16038
+
16039
+ // src/hooks/is-anthropic-provider.ts
16040
+ function isAnthropicProvider(providerID) {
16041
+ return providerID === "anthropic" || providerID === "google-vertex-anthropic";
16042
+ }
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;
16112
16049
  }
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;
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;
16118
16060
  }
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;
16061
+ function resolveCacheTtl(cacheTtl, modelKey) {
16062
+ if (typeof cacheTtl === "string") {
16063
+ return cacheTtl;
16125
16064
  }
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;
16065
+ if (modelKey && typeof cacheTtl[modelKey] === "string") {
16066
+ return cacheTtl[modelKey];
16143
16067
  }
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);
16068
+ if (modelKey) {
16069
+ const bareModelId = modelKey.split("/").slice(1).join("/");
16070
+ if (bareModelId && typeof cacheTtl[bareModelId] === "string") {
16071
+ return cacheTtl[bareModelId];
16153
16072
  }
16154
- } else {
16155
- removeDreamEntry(args.db, entry.id);
16156
16073
  }
16157
- return result;
16074
+ return cacheTtl.default ?? "5m";
16158
16075
  }
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;
16076
+ function resolveExecuteThreshold(config2, modelKey, fallback) {
16077
+ if (typeof config2 === "number") {
16078
+ return config2;
16170
16079
  }
16171
- const startMinutes = startHour * 60 + startMin;
16172
- const endMinutes = endHour * 60 + endMin;
16173
- return { startMinutes, endMinutes };
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;
16174
16090
  }
16175
- function isInScheduleWindow(schedule, now = new Date) {
16176
- const window = parseScheduleWindow(schedule);
16177
- if (!window)
16091
+ function resolveModelKey(providerID, modelID) {
16092
+ if (!providerID || !modelID) {
16093
+ return;
16094
+ }
16095
+ return `${providerID}/${modelID}`;
16096
+ }
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;
16113
+ }
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);
16127
+ }
16128
+ const match = normalizedTtl.match(TTL_PATTERN);
16129
+ if (!match) {
16130
+ throw new Error(`Invalid cache TTL format: ${ttl}`);
16131
+ }
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);
16146
+ } catch (error48) {
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);
16151
+ }
16152
+ ttlMs = parseCacheTtl("5m");
16153
+ }
16154
+ const elapsedTime = currentTime - sessionMeta.lastResponseTime;
16155
+ if (elapsedTime > ttlMs) {
16156
+ return "execute";
16157
+ }
16158
+ return "defer";
16159
+ }
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") {
16178
16168
  return false;
16179
- const currentMinutes = now.getHours() * 60 + now.getMinutes();
16180
- if (window.startMinutes <= window.endMinutes) {
16181
- return currentMinutes >= window.startMinutes && currentMinutes < window.endMinutes;
16182
16169
  }
16183
- return currentMinutes >= window.startMinutes || currentMinutes < window.endMinutes;
16170
+ const candidate = row;
16171
+ return typeof candidate.message_id === "string" && typeof candidate.tag_number === "number";
16184
16172
  }
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);
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);
16184
+ }
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;
16197
+ }
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;
16196
16203
  }
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;
16197
16213
  }
16198
- return projects;
16199
- }
16200
- function checkScheduleAndEnqueue(db, schedule) {
16201
- if (!isInScheduleWindow(schedule)) {
16202
- return 0;
16214
+ function getTag(sessionId, messageId) {
16215
+ return assignments.get(sessionId)?.get(messageId);
16203
16216
  }
16204
- const projects = findProjectsNeedingDream(db);
16205
- if (projects.length === 0) {
16206
- return 0;
16217
+ function bindTag(sessionId, messageId, tagNumber) {
16218
+ getSessionAssignments(sessionId).set(messageId, tagNumber);
16207
16219
  }
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++;
16220
+ function getAssignments(sessionId) {
16221
+ return getSessionAssignments(sessionId);
16222
+ }
16223
+ function resetCounter(sessionId, db) {
16224
+ counters.set(sessionId, 0);
16225
+ assignments.delete(sessionId);
16226
+ getUpsertCounterStatement(db).run(sessionId, 0);
16227
+ }
16228
+ function getCounter(sessionId) {
16229
+ return counters.get(sessionId) ?? 0;
16230
+ }
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";
@@ -17280,191 +17327,10 @@ function createEventHandler2(deps) {
17280
17327
  sessionLog(sessionId, "event session.deleted persistence failed:", error48);
17281
17328
  }
17282
17329
  deps.onSessionCacheInvalidated?.(sessionId);
17283
- deps.contextUsageMap.delete(sessionId);
17284
- deps.tagger.cleanup(sessionId);
17285
- return;
17286
- }
17287
- };
17288
- }
17289
-
17290
- // src/hooks/magic-context/nudger.ts
17291
- var RECENT_CTX_REDUCE_WINDOW_MS = 2 * 60 * 1000;
17292
- function formatLargestTags(tags) {
17293
- if (tags.length === 0) {
17294
- return "none";
17295
- }
17296
- return tags.map((tag) => `\xA7${tag.tagNumber}\xA7`).join(", ");
17297
- }
17298
- function formatOldToolTags(activeTags, protectedCount, count) {
17299
- const sortedByNumber = [...activeTags].sort((a, b) => a.tagNumber - b.tagNumber);
17300
- const protectedThreshold = protectedCount > 0 && sortedByNumber.length > protectedCount ? sortedByNumber[sortedByNumber.length - protectedCount].tagNumber : Infinity;
17301
- const midpoint = Math.floor(sortedByNumber.length / 2);
17302
- const earlyHalf = sortedByNumber.slice(0, midpoint);
17303
- const earlyToolTags = earlyHalf.filter((t) => t.type === "tool" && t.tagNumber < protectedThreshold);
17304
- if (earlyToolTags.length === 0)
17305
- return "";
17306
- const selected = earlyToolTags.sort((a, b) => b.byteSize - a.byteSize).slice(0, count);
17307
- const formatted = selected.sort((a, b) => a.tagNumber - b.tagNumber).map((t) => `\xA7${t.tagNumber}\xA7(${formatBytes(t.byteSize)})`).join(", ");
17308
- return ` Old tool outputs worth dropping: ${formatted}`;
17309
- }
17310
- function createNudger(config2) {
17311
- const lastReduceAtBySession = config2.recentReduceBySession ?? new Map;
17312
- return (sessionId, contextUsage, db, topNFn, preloadedTags, messagesSinceLastUser, preloadedSessionMeta) => {
17313
- const sessionMeta = preloadedSessionMeta ?? getOrCreateSessionMeta(db, sessionId);
17314
- const now = config2.now?.() ?? Date.now();
17315
- const lastReduceAt = lastReduceAtBySession.get(sessionId);
17316
- if (lastReduceAt !== undefined && now - lastReduceAt > RECENT_CTX_REDUCE_WINDOW_MS) {
17317
- lastReduceAtBySession.delete(sessionId);
17318
- }
17319
- if (contextUsage.inputTokens < sessionMeta.lastNudgeTokens) {
17320
- sessionMeta.lastNudgeTokens = contextUsage.inputTokens;
17321
- updateSessionMeta(db, sessionId, { lastNudgeTokens: contextUsage.inputTokens });
17322
- }
17323
- if (lastReduceAt !== undefined && now - lastReduceAt <= RECENT_CTX_REDUCE_WINDOW_MS) {
17324
- sessionLog(sessionId, `nudge: suppressed at ${contextUsage.percentage.toFixed(1)}% because ctx_reduce ran recently (${now - lastReduceAt}ms ago)`);
17325
- return null;
17326
- }
17327
- const projectedPercentage = estimateProjectedPercentage(db, sessionId, contextUsage, preloadedTags);
17328
- const executeThreshold = resolveExecuteThreshold(config2.execute_threshold_percentage, undefined, 65);
17329
- const currentBand = getRollingNudgeBand(contextUsage.percentage, executeThreshold);
17330
- const currentInterval = getRollingNudgeIntervalTokens(config2.nudge_interval_tokens, currentBand);
17331
- const lastBand = sessionMeta.lastNudgeBand;
17332
- if (getRollingNudgeBandPriority(currentBand) < getRollingNudgeBandPriority(lastBand)) {
17333
- sessionMeta.lastNudgeBand = currentBand;
17334
- updateSessionMeta(db, sessionId, { lastNudgeBand: currentBand });
17335
- }
17336
- const largest = formatLargestTags(topNFn(db, sessionId, 3));
17337
- const protectedCount = config2.protected_tags;
17338
- const activeTags = (preloadedTags ?? getTagsBySession(db, sessionId)).filter((t) => t.status === "active");
17339
- const highestProtected = activeTags.map((t) => t.tagNumber).sort((a, b) => b - a).slice(0, protectedCount)[0];
17340
- const protectedHint = highestProtected ? ` Tags \xA7${highestProtected}\xA7 and above are protected (last ${protectedCount}) \u2014 You MUST NOT try to reduce those.` : "";
17341
- const oldToolHint = formatOldToolTags(activeTags, protectedCount, 5);
17342
- const iterationThreshold = config2.iteration_nudge_threshold;
17343
- if (messagesSinceLastUser !== undefined && messagesSinceLastUser >= iterationThreshold && contextUsage.percentage >= 35 && contextUsage.percentage < executeThreshold && contextUsage.inputTokens - sessionMeta.lastNudgeTokens >= currentInterval) {
17344
- sessionLog(sessionId, `nudge fired: iteration_nudge at ${contextUsage.percentage.toFixed(1)}% (${messagesSinceLastUser} messages since user, interval: ${contextUsage.inputTokens - sessionMeta.lastNudgeTokens}/${currentInterval} tokens)`);
17345
- updateSessionMeta(db, sessionId, { lastNudgeTokens: contextUsage.inputTokens });
17346
- return {
17347
- type: "assistant",
17348
- text: [
17349
- `
17350
-
17351
- <instruction name="context_iteration">`,
17352
- `CONTEXT ITERATION NOTICE \u2014 ~${Math.round(contextUsage.percentage)}%`,
17353
- `You have been executing ${messagesSinceLastUser}+ tool calls without clearing old context.`,
17354
- `Consider using \`ctx_reduce\` to drop old tool outputs you have already processed.`,
17355
- ``,
17356
- `Largest: ${largest}.${oldToolHint}${protectedHint}`,
17357
- `Tags are marked with \xA7N\xA7 identifiers (e.g., \xA71\xA7, \xA742\xA7).`,
17358
- ``,
17359
- `Actions:`,
17360
- `- drop: Remove content entirely. Best for old tool outputs you already acted on.`,
17361
- `- Syntax: "3-5", "1,2,9", or "1-5,8,12-15" (bare integers).`,
17362
- `- Only drop what you have already processed. NEVER drop large ranges blindly.`,
17363
- `</instruction>`
17364
- ].join(`
17365
- `)
17366
- };
17367
- }
17368
- const intervalReached = contextUsage.inputTokens - sessionMeta.lastNudgeTokens >= currentInterval;
17369
- const bandEscalated = lastBand !== null && getRollingNudgeBandPriority(currentBand) > getRollingNudgeBandPriority(lastBand);
17370
- if (bandEscalated || intervalReached) {
17371
- const reason = bandEscalated ? `band escalation (${formatRollingNudgeBand(lastBand)} -> ${currentBand})` : `interval ${contextUsage.inputTokens - sessionMeta.lastNudgeTokens}/${currentInterval} tokens`;
17372
- sessionLog(sessionId, `nudge fired: rolling_${currentBand} at ${contextUsage.percentage.toFixed(1)}% (${reason})`);
17373
- updateSessionMeta(db, sessionId, {
17374
- lastNudgeTokens: contextUsage.inputTokens,
17375
- lastNudgeBand: currentBand
17376
- });
17377
- return {
17378
- type: "assistant",
17379
- text: buildRollingNudgeText(currentBand, contextUsage.percentage, largest, oldToolHint, protectedHint)
17380
- };
17381
- }
17382
- sessionLog(sessionId, `nudge: none fired at ${contextUsage.percentage.toFixed(1)}% (band=${currentBand} lastBand=${formatRollingNudgeBand(lastBand)} lastNudge=${sessionMeta.lastNudgeTokens} current=${contextUsage.inputTokens} interval=${currentInterval} projected=${projectedPercentage?.toFixed(1) ?? "none"})`);
17383
- return null;
17384
- };
17385
- }
17386
- function buildRollingNudgeText(band, percentage, largest, oldToolHint, protectedHint) {
17387
- const titleByBand = {
17388
- far: "CONTEXT REMINDER",
17389
- near: "CONTEXT WARNING",
17390
- urgent: "CONTEXT URGENT",
17391
- critical: "CONTEXT CRITICAL"
17392
- };
17393
- const instructionByBand = {
17394
- far: "You should use `ctx_reduce` to drop old tool outputs before continuing.",
17395
- near: "You should call `ctx_reduce` soon to free space before more heavy reads or tool output.",
17396
- urgent: "You should call `ctx_reduce` before doing more reads or tool-heavy work.",
17397
- critical: "You MUST call `ctx_reduce` RIGHT NOW before doing ANYTHING else."
17398
- };
17399
- const cautionByBand = {
17400
- far: "- Only drop what you have already processed. NEVER drop large ranges blindly.",
17401
- near: "- Review what each tag contains. Drop processed outputs, keep anything you might need soon.",
17402
- urgent: "- Review each tag before deciding. Avoid broad drops that could remove active context.",
17403
- critical: '- NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.'
17404
- };
17405
- return [
17406
- `
17407
-
17408
- <instruction name="context_${band}">`,
17409
- `${titleByBand[band]} \u2014 ~${Math.round(percentage)}%`,
17410
- instructionByBand[band],
17411
- ``,
17412
- `Largest: ${largest}.${oldToolHint}${protectedHint}`,
17413
- `Tags are marked with \xA7N\xA7 identifiers (e.g., \xA71\xA7, \xA742\xA7).`,
17414
- ``,
17415
- `Actions:`,
17416
- `- drop: Remove content entirely. Best for old tool outputs you already acted on.`,
17417
- `- Syntax: "3-5", "1,2,9", or "1-5,8,12-15" (bare integers).`,
17418
- cautionByBand[band],
17419
- `</instruction>`
17420
- ].join(`
17421
- `);
17422
- }
17423
- function estimateProjectedPercentage(db, sessionId, contextUsage, preloadedTags) {
17424
- const pendingOps = getPendingOps(db, sessionId);
17425
- const pendingDrops = pendingOps.filter((op) => op.operation === "drop");
17426
- if (pendingDrops.length === 0) {
17427
- return null;
17428
- }
17429
- const activeTags = (preloadedTags ?? getTagsBySession(db, sessionId)).filter((t) => t.status === "active");
17430
- const totalActiveBytes = activeTags.reduce((sum, t) => sum + t.byteSize, 0);
17431
- if (totalActiveBytes === 0) {
17432
- return null;
17433
- }
17434
- const pendingDropTagIds = new Set(pendingDrops.map((op) => op.tagId));
17435
- const pendingDropBytes = activeTags.filter((t) => pendingDropTagIds.has(t.tagNumber)).reduce((sum, t) => sum + t.byteSize, 0);
17436
- const dropRatio = pendingDropBytes / totalActiveBytes;
17437
- return contextUsage.percentage * (1 - dropRatio);
17438
- }
17439
- function generateEmergencyNudgeText(db, sessionId, contextUsage, config2) {
17440
- const largest = formatLargestTags(getTopNBySize(db, sessionId, 3));
17441
- const protectedCount = config2.protected_tags;
17442
- const activeTags = getTagsBySession(db, sessionId).filter((t) => t.status === "active");
17443
- const highestProtected = activeTags.map((t) => t.tagNumber).sort((a, b) => b - a).slice(0, protectedCount)[0];
17444
- const protectedHint = highestProtected ? ` Tags \xA7${highestProtected}\xA7 and above are protected (last ${protectedCount}) \u2014 You MUST NOT try to reduce those.` : "";
17445
- const oldToolHint = formatOldToolTags(activeTags, protectedCount, 5);
17446
- return [
17447
- `<instruction name="context_emergency">`,
17448
- `CONTEXT EMERGENCY \u2014 ~${Math.round(contextUsage.percentage)}%. STOP all current work immediately.`,
17449
- ``,
17450
- `You MUST use \`ctx_reduce\` RIGHT NOW to free space. If context overflows, you lose all work.`,
17451
- ``,
17452
- `Steps:`,
17453
- `1. Find OLD tool outputs (grep results, file reads, build logs) you already processed \u2014 look at \xA7N\xA7 tags`,
17454
- `2. Drop those specifically: e.g. drop="3,7,12" \u2014 NEVER drop large ranges like "1-50"`,
17455
- `3. KEEP anything related to current task, recent errors, or decisions`,
17456
- ``,
17457
- `Largest tags: ${largest}.${oldToolHint}${protectedHint}`,
17458
- `</instruction>`
17459
- ].join(`
17460
- `);
17461
- }
17462
-
17463
- // src/hooks/magic-context/text-complete.ts
17464
- var TAG_PREFIX_REGEX2 = /^(\u00a7\d+\u00a7\s*)+/;
17465
- function createTextCompleteHandler() {
17466
- return async (_input, output) => {
17467
- output.text = output.text.replace(TAG_PREFIX_REGEX2, "");
17330
+ deps.contextUsageMap.delete(sessionId);
17331
+ deps.tagger.cleanup(sessionId);
17332
+ return;
17333
+ }
17468
17334
  };
17469
17335
  }
17470
17336
 
@@ -17809,6 +17675,10 @@ function archiveMemory(db, id, reason) {
17809
17675
  }
17810
17676
 
17811
17677
  // src/hooks/magic-context/inject-compartments.ts
17678
+ var injectionCache = new Map;
17679
+ function clearInjectionCache(sessionId) {
17680
+ injectionCache.delete(sessionId);
17681
+ }
17812
17682
  function renderMemoryBlock(memories) {
17813
17683
  const byCategory = new Map;
17814
17684
  for (const m of memories) {
@@ -17859,19 +17729,31 @@ function trimMemoriesToBudget(sessionId, memories, budgetTokens) {
17859
17729
  }
17860
17730
  return result;
17861
17731
  }
17862
- function prepareCompartmentInjection(db, sessionId, messages, projectPath, injectionBudgetTokens) {
17732
+ function prepareCompartmentInjection(db, sessionId, messages, isCacheBusting, projectPath, injectionBudgetTokens) {
17733
+ const cached2 = injectionCache.get(sessionId);
17734
+ if (!isCacheBusting && cached2) {
17735
+ if (cached2.compartmentEndMessageId.length > 0) {
17736
+ const cutoffIndex2 = messages.findIndex((message) => message.info.id === cached2.compartmentEndMessageId);
17737
+ if (cutoffIndex2 >= 0) {
17738
+ const remaining = messages.slice(cutoffIndex2 + 1);
17739
+ messages.splice(0, messages.length, ...remaining);
17740
+ }
17741
+ }
17742
+ return cached2;
17743
+ }
17863
17744
  const compartments = getCompartments(db, sessionId);
17864
17745
  if (compartments.length === 0) {
17746
+ injectionCache.delete(sessionId);
17865
17747
  return null;
17866
17748
  }
17867
17749
  const facts = getSessionFacts(db, sessionId);
17868
17750
  let memoryBlock;
17869
17751
  let memoryCount = 0;
17870
17752
  if (projectPath) {
17871
- const cached2 = db.prepare("SELECT memory_block_cache, memory_block_count FROM session_meta WHERE session_id = ?").get(sessionId);
17872
- if (cached2?.memory_block_cache) {
17873
- memoryBlock = cached2.memory_block_cache;
17874
- memoryCount = cached2.memory_block_count;
17753
+ const cachedMemory = db.prepare("SELECT memory_block_cache, memory_block_count FROM session_meta WHERE session_id = ?").get(sessionId);
17754
+ if (cachedMemory?.memory_block_cache) {
17755
+ memoryBlock = cachedMemory.memory_block_cache;
17756
+ memoryCount = cachedMemory.memory_block_count;
17875
17757
  } else {
17876
17758
  let memories = getMemoriesByProject(db, projectPath, ["active", "permanent"]);
17877
17759
  if (injectionBudgetTokens && memories.length > 0) {
@@ -17891,14 +17773,17 @@ function prepareCompartmentInjection(db, sessionId, messages, projectPath, injec
17891
17773
  compartmentCount: compartments.length,
17892
17774
  compartmentEndMessage: lastEnd
17893
17775
  });
17894
- return {
17776
+ const result2 = {
17895
17777
  block,
17896
17778
  compartmentEndMessage: lastEnd,
17779
+ compartmentEndMessageId: "",
17897
17780
  compartmentCount: compartments.length,
17898
17781
  skippedVisibleMessages: 0,
17899
17782
  factCount: facts.length,
17900
17783
  memoryCount
17901
17784
  };
17785
+ injectionCache.set(sessionId, result2);
17786
+ return result2;
17902
17787
  }
17903
17788
  let skippedVisibleMessages = 0;
17904
17789
  const cutoffIndex = messages.findIndex((message) => message.info.id === lastEndMessageId);
@@ -17907,14 +17792,17 @@ function prepareCompartmentInjection(db, sessionId, messages, projectPath, injec
17907
17792
  const remaining = messages.slice(cutoffIndex + 1);
17908
17793
  messages.splice(0, messages.length, ...remaining);
17909
17794
  }
17910
- return {
17795
+ const result = {
17911
17796
  block,
17912
17797
  compartmentEndMessage: lastEnd,
17798
+ compartmentEndMessageId: lastEndMessageId,
17913
17799
  compartmentCount: compartments.length,
17914
17800
  skippedVisibleMessages,
17915
17801
  factCount: facts.length,
17916
17802
  memoryCount
17917
17803
  };
17804
+ injectionCache.set(sessionId, result);
17805
+ return result;
17918
17806
  }
17919
17807
  function renderCompartmentInjection(sessionId, messages, prepared) {
17920
17808
  const historyBlock = `<session-history>
@@ -17956,6 +17844,187 @@ function isDroppedPlaceholder(text) {
17956
17844
  return /^\[dropped \u00A7\d+\u00A7\]$/.test(text.trim());
17957
17845
  }
17958
17846
 
17847
+ // src/hooks/magic-context/nudger.ts
17848
+ var RECENT_CTX_REDUCE_WINDOW_MS = 2 * 60 * 1000;
17849
+ function formatLargestTags(tags) {
17850
+ if (tags.length === 0) {
17851
+ return "none";
17852
+ }
17853
+ return tags.map((tag) => `\xA7${tag.tagNumber}\xA7`).join(", ");
17854
+ }
17855
+ function formatOldToolTags(activeTags, protectedCount, count) {
17856
+ const sortedByNumber = [...activeTags].sort((a, b) => a.tagNumber - b.tagNumber);
17857
+ const protectedThreshold = protectedCount > 0 && sortedByNumber.length > protectedCount ? sortedByNumber[sortedByNumber.length - protectedCount].tagNumber : Infinity;
17858
+ const midpoint = Math.floor(sortedByNumber.length / 2);
17859
+ const earlyHalf = sortedByNumber.slice(0, midpoint);
17860
+ const earlyToolTags = earlyHalf.filter((t) => t.type === "tool" && t.tagNumber < protectedThreshold);
17861
+ if (earlyToolTags.length === 0)
17862
+ return "";
17863
+ const selected = earlyToolTags.sort((a, b) => b.byteSize - a.byteSize).slice(0, count);
17864
+ const formatted = selected.sort((a, b) => a.tagNumber - b.tagNumber).map((t) => `\xA7${t.tagNumber}\xA7(${formatBytes(t.byteSize)})`).join(", ");
17865
+ return ` Old tool outputs worth dropping: ${formatted}`;
17866
+ }
17867
+ function createNudger(config2) {
17868
+ const lastReduceAtBySession = config2.recentReduceBySession ?? new Map;
17869
+ return (sessionId, contextUsage, db, topNFn, preloadedTags, messagesSinceLastUser, preloadedSessionMeta) => {
17870
+ const sessionMeta = preloadedSessionMeta ?? getOrCreateSessionMeta(db, sessionId);
17871
+ const now = config2.now?.() ?? Date.now();
17872
+ const lastReduceAt = lastReduceAtBySession.get(sessionId);
17873
+ if (lastReduceAt !== undefined && now - lastReduceAt > RECENT_CTX_REDUCE_WINDOW_MS) {
17874
+ lastReduceAtBySession.delete(sessionId);
17875
+ }
17876
+ if (contextUsage.inputTokens < sessionMeta.lastNudgeTokens) {
17877
+ sessionMeta.lastNudgeTokens = contextUsage.inputTokens;
17878
+ updateSessionMeta(db, sessionId, { lastNudgeTokens: contextUsage.inputTokens });
17879
+ }
17880
+ if (lastReduceAt !== undefined && now - lastReduceAt <= RECENT_CTX_REDUCE_WINDOW_MS) {
17881
+ sessionLog(sessionId, `nudge: suppressed at ${contextUsage.percentage.toFixed(1)}% because ctx_reduce ran recently (${now - lastReduceAt}ms ago)`);
17882
+ return null;
17883
+ }
17884
+ const projectedPercentage = estimateProjectedPercentage(db, sessionId, contextUsage, preloadedTags);
17885
+ const executeThreshold = resolveExecuteThreshold(config2.execute_threshold_percentage, undefined, 65);
17886
+ const currentBand = getRollingNudgeBand(contextUsage.percentage, executeThreshold);
17887
+ const currentInterval = getRollingNudgeIntervalTokens(config2.nudge_interval_tokens, currentBand);
17888
+ const lastBand = sessionMeta.lastNudgeBand;
17889
+ if (getRollingNudgeBandPriority(currentBand) < getRollingNudgeBandPriority(lastBand)) {
17890
+ sessionMeta.lastNudgeBand = currentBand;
17891
+ updateSessionMeta(db, sessionId, { lastNudgeBand: currentBand });
17892
+ }
17893
+ const largest = formatLargestTags(topNFn(db, sessionId, 3));
17894
+ const protectedCount = config2.protected_tags;
17895
+ const activeTags = (preloadedTags ?? getTagsBySession(db, sessionId)).filter((t) => t.status === "active");
17896
+ const highestProtected = activeTags.map((t) => t.tagNumber).sort((a, b) => b - a).slice(0, protectedCount)[0];
17897
+ const protectedHint = highestProtected ? ` Tags \xA7${highestProtected}\xA7 and above are protected (last ${protectedCount}) \u2014 You MUST NOT try to reduce those.` : "";
17898
+ const oldToolHint = formatOldToolTags(activeTags, protectedCount, 5);
17899
+ const iterationThreshold = config2.iteration_nudge_threshold;
17900
+ if (messagesSinceLastUser !== undefined && messagesSinceLastUser >= iterationThreshold && contextUsage.percentage >= 35 && contextUsage.percentage < executeThreshold && contextUsage.inputTokens - sessionMeta.lastNudgeTokens >= currentInterval) {
17901
+ sessionLog(sessionId, `nudge fired: iteration_nudge at ${contextUsage.percentage.toFixed(1)}% (${messagesSinceLastUser} messages since user, interval: ${contextUsage.inputTokens - sessionMeta.lastNudgeTokens}/${currentInterval} tokens)`);
17902
+ updateSessionMeta(db, sessionId, { lastNudgeTokens: contextUsage.inputTokens });
17903
+ return {
17904
+ type: "assistant",
17905
+ text: [
17906
+ `
17907
+
17908
+ <instruction name="context_iteration">`,
17909
+ `CONTEXT ITERATION NOTICE \u2014 ~${Math.round(contextUsage.percentage)}%`,
17910
+ `You have been executing ${messagesSinceLastUser}+ tool calls without clearing old context.`,
17911
+ `Consider using \`ctx_reduce\` to drop old tool outputs you have already processed.`,
17912
+ ``,
17913
+ `Largest: ${largest}.${oldToolHint}${protectedHint}`,
17914
+ `Tags are marked with \xA7N\xA7 identifiers (e.g., \xA71\xA7, \xA742\xA7).`,
17915
+ ``,
17916
+ `Actions:`,
17917
+ `- drop: Remove content entirely. Best for old tool outputs you already acted on.`,
17918
+ `- Syntax: "3-5", "1,2,9", or "1-5,8,12-15" (bare integers).`,
17919
+ `- Only drop what you have already processed. NEVER drop large ranges blindly.`,
17920
+ `</instruction>`
17921
+ ].join(`
17922
+ `)
17923
+ };
17924
+ }
17925
+ const intervalReached = contextUsage.inputTokens - sessionMeta.lastNudgeTokens >= currentInterval;
17926
+ const bandEscalated = lastBand !== null && getRollingNudgeBandPriority(currentBand) > getRollingNudgeBandPriority(lastBand);
17927
+ if (bandEscalated || intervalReached) {
17928
+ const reason = bandEscalated ? `band escalation (${formatRollingNudgeBand(lastBand)} -> ${currentBand})` : `interval ${contextUsage.inputTokens - sessionMeta.lastNudgeTokens}/${currentInterval} tokens`;
17929
+ sessionLog(sessionId, `nudge fired: rolling_${currentBand} at ${contextUsage.percentage.toFixed(1)}% (${reason})`);
17930
+ updateSessionMeta(db, sessionId, {
17931
+ lastNudgeTokens: contextUsage.inputTokens,
17932
+ lastNudgeBand: currentBand
17933
+ });
17934
+ return {
17935
+ type: "assistant",
17936
+ text: buildRollingNudgeText(currentBand, contextUsage.percentage, largest, oldToolHint, protectedHint)
17937
+ };
17938
+ }
17939
+ sessionLog(sessionId, `nudge: none fired at ${contextUsage.percentage.toFixed(1)}% (band=${currentBand} lastBand=${formatRollingNudgeBand(lastBand)} lastNudge=${sessionMeta.lastNudgeTokens} current=${contextUsage.inputTokens} interval=${currentInterval} projected=${projectedPercentage?.toFixed(1) ?? "none"})`);
17940
+ return null;
17941
+ };
17942
+ }
17943
+ function buildRollingNudgeText(band, percentage, largest, oldToolHint, protectedHint) {
17944
+ const titleByBand = {
17945
+ far: "CONTEXT REMINDER",
17946
+ near: "CONTEXT WARNING",
17947
+ urgent: "CONTEXT URGENT",
17948
+ critical: "CONTEXT CRITICAL"
17949
+ };
17950
+ const instructionByBand = {
17951
+ far: "You should use `ctx_reduce` to drop old tool outputs before continuing.",
17952
+ near: "You should call `ctx_reduce` soon to free space before more heavy reads or tool output.",
17953
+ urgent: "You should call `ctx_reduce` before doing more reads or tool-heavy work.",
17954
+ critical: "You MUST call `ctx_reduce` RIGHT NOW before doing ANYTHING else."
17955
+ };
17956
+ const cautionByBand = {
17957
+ far: "- Only drop what you have already processed. NEVER drop large ranges blindly.",
17958
+ near: "- Review what each tag contains. Drop processed outputs, keep anything you might need soon.",
17959
+ urgent: "- Review each tag before deciding. Avoid broad drops that could remove active context.",
17960
+ critical: '- NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.'
17961
+ };
17962
+ return [
17963
+ `
17964
+
17965
+ <instruction name="context_${band}">`,
17966
+ `${titleByBand[band]} \u2014 ~${Math.round(percentage)}%`,
17967
+ instructionByBand[band],
17968
+ ``,
17969
+ `Largest: ${largest}.${oldToolHint}${protectedHint}`,
17970
+ `Tags are marked with \xA7N\xA7 identifiers (e.g., \xA71\xA7, \xA742\xA7).`,
17971
+ ``,
17972
+ `Actions:`,
17973
+ `- drop: Remove content entirely. Best for old tool outputs you already acted on.`,
17974
+ `- Syntax: "3-5", "1,2,9", or "1-5,8,12-15" (bare integers).`,
17975
+ cautionByBand[band],
17976
+ `</instruction>`
17977
+ ].join(`
17978
+ `);
17979
+ }
17980
+ function estimateProjectedPercentage(db, sessionId, contextUsage, preloadedTags) {
17981
+ const pendingOps = getPendingOps(db, sessionId);
17982
+ const pendingDrops = pendingOps.filter((op) => op.operation === "drop");
17983
+ if (pendingDrops.length === 0) {
17984
+ return null;
17985
+ }
17986
+ const activeTags = (preloadedTags ?? getTagsBySession(db, sessionId)).filter((t) => t.status === "active");
17987
+ const totalActiveBytes = activeTags.reduce((sum, t) => sum + t.byteSize, 0);
17988
+ if (totalActiveBytes === 0) {
17989
+ return null;
17990
+ }
17991
+ const pendingDropTagIds = new Set(pendingDrops.map((op) => op.tagId));
17992
+ const pendingDropBytes = activeTags.filter((t) => pendingDropTagIds.has(t.tagNumber)).reduce((sum, t) => sum + t.byteSize, 0);
17993
+ const dropRatio = pendingDropBytes / totalActiveBytes;
17994
+ return contextUsage.percentage * (1 - dropRatio);
17995
+ }
17996
+ function generateEmergencyNudgeText(db, sessionId, contextUsage, config2) {
17997
+ const largest = formatLargestTags(getTopNBySize(db, sessionId, 3));
17998
+ const protectedCount = config2.protected_tags;
17999
+ const activeTags = getTagsBySession(db, sessionId).filter((t) => t.status === "active");
18000
+ const highestProtected = activeTags.map((t) => t.tagNumber).sort((a, b) => b - a).slice(0, protectedCount)[0];
18001
+ const protectedHint = highestProtected ? ` Tags \xA7${highestProtected}\xA7 and above are protected (last ${protectedCount}) \u2014 You MUST NOT try to reduce those.` : "";
18002
+ const oldToolHint = formatOldToolTags(activeTags, protectedCount, 5);
18003
+ return [
18004
+ `<instruction name="context_emergency">`,
18005
+ `CONTEXT EMERGENCY \u2014 ~${Math.round(contextUsage.percentage)}%. STOP all current work immediately.`,
18006
+ ``,
18007
+ `You MUST use \`ctx_reduce\` RIGHT NOW to free space. If context overflows, you lose all work.`,
18008
+ ``,
18009
+ `Steps:`,
18010
+ `1. Find OLD tool outputs (grep results, file reads, build logs) you already processed \u2014 look at \xA7N\xA7 tags`,
18011
+ `2. Drop those specifically: e.g. drop="3,7,12" \u2014 NEVER drop large ranges like "1-50"`,
18012
+ `3. KEEP anything related to current task, recent errors, or decisions`,
18013
+ ``,
18014
+ `Largest tags: ${largest}.${oldToolHint}${protectedHint}`,
18015
+ `</instruction>`
18016
+ ].join(`
18017
+ `);
18018
+ }
18019
+
18020
+ // src/hooks/magic-context/text-complete.ts
18021
+ var TAG_PREFIX_REGEX2 = /^(\u00a7\d+\u00a7\s*)+/;
18022
+ function createTextCompleteHandler() {
18023
+ return async (_input, output) => {
18024
+ output.text = output.text.replace(TAG_PREFIX_REGEX2, "");
18025
+ };
18026
+ }
18027
+
17959
18028
  // src/hooks/magic-context/note-nudger.ts
17960
18029
  var stateBySession = new Map;
17961
18030
  function getState(sessionId) {
@@ -19724,7 +19793,7 @@ async function runCompartmentPhase(args) {
19724
19793
  sessionLog(args.sessionId, reason);
19725
19794
  await activeRun;
19726
19795
  sessionLog(args.sessionId, "transform: compartment agent completed, refreshing compartment coverage");
19727
- pendingCompartmentInjection = prepareCompartmentInjection(args.db, args.resolvedSessionId, args.messages, args.projectPath, args.injectionBudgetTokens);
19796
+ pendingCompartmentInjection = prepareCompartmentInjection(args.db, args.resolvedSessionId, args.messages, args.cacheAlreadyBusting ?? false, args.projectPath, args.injectionBudgetTokens);
19728
19797
  }
19729
19798
  if (args.canRunCompartments && args.sessionMeta.compartmentInProgress && !getActiveCompartmentRun(args.sessionId)) {
19730
19799
  if (!hasEligibleHistoryForCompartment()) {
@@ -20406,7 +20475,6 @@ function tagMessages(sessionId, messages, tagger, db) {
20406
20475
  }
20407
20476
  // src/hooks/magic-context/nudge-injection.ts
20408
20477
  var TRAILING_CONTEXT_NUDGE_PATTERN = /(?:\s*<instruction name="(?:context_[^"]+|deferred_notes)">[\s\S]*?<\/instruction>\s*)+$/;
20409
- var TRAILING_DEFERRED_NOTES_PATTERN = /(?:\s*<instruction name="deferred_notes">[\s\S]*?<\/instruction>\s*)+$/;
20410
20478
  function isToolProtocolPart(part) {
20411
20479
  if (part === null || typeof part !== "object")
20412
20480
  return false;
@@ -20428,12 +20496,6 @@ function stripTrailingExactNudge(text, nudgeText) {
20428
20496
  function stripTrailingContextNudges(text) {
20429
20497
  return text.replace(TRAILING_CONTEXT_NUDGE_PATTERN, "");
20430
20498
  }
20431
- function stripTrailingDeferredNotes(text) {
20432
- return text.replace(TRAILING_DEFERRED_NOTES_PATTERN, "");
20433
- }
20434
- function isAppendableAssistantMessage(message) {
20435
- return message.info.role === "assistant" && !hasToolProtocolParts(message) && !isMessageDropped(message);
20436
- }
20437
20499
  function mergeNudgeText(text, currentNudgeText, nextNudgeText) {
20438
20500
  const withoutCurrentNudge = stripTrailingExactNudge(text, currentNudgeText);
20439
20501
  const withoutManagedNudges = stripTrailingContextNudges(withoutCurrentNudge);
@@ -20511,40 +20573,6 @@ function appendNudgeToAssistant(messages, nudge, nudgePlacements, sessionId) {
20511
20573
  }
20512
20574
  sessionLog(sessionId, `nudge placement failed: no suitable assistant message found (${messages.length} messages)`);
20513
20575
  }
20514
- function appendSupplementalNudgeToAssistant(messages, nudge, nudgePlacements, sessionId) {
20515
- const appendToMessage = (message) => {
20516
- if (!isAppendableAssistantMessage(message))
20517
- return false;
20518
- for (let j = message.parts.length - 1;j >= 0; j--) {
20519
- const part = message.parts[j];
20520
- if (isTextPart(part)) {
20521
- const nextText = `${stripTrailingDeferredNotes(part.text)}${nudge}`;
20522
- if (nextText !== part.text) {
20523
- part.text = nextText;
20524
- }
20525
- return true;
20526
- }
20527
- }
20528
- message.parts.push({ type: "text", text: nudge });
20529
- return true;
20530
- };
20531
- const placement = nudgePlacements.get(sessionId);
20532
- if (!placement)
20533
- return false;
20534
- for (const message of messages) {
20535
- if (message.info.id !== placement.messageId)
20536
- continue;
20537
- return appendToMessage(message);
20538
- }
20539
- return false;
20540
- }
20541
- function canAppendSupplementalNudgeToAssistant(messages, nudgePlacements, sessionId) {
20542
- const placement = nudgePlacements.get(sessionId);
20543
- if (!placement)
20544
- return false;
20545
- const anchoredMessage = messages.find((message) => message.info.id === placement.messageId);
20546
- return anchoredMessage ? isAppendableAssistantMessage(anchoredMessage) : false;
20547
- }
20548
20576
 
20549
20577
  // src/hooks/magic-context/apply-context-nudge.ts
20550
20578
  function applyContextNudge(messages, nudge, nudgePlacements, sessionId) {
@@ -20789,6 +20817,7 @@ function runPostTransformPhase(args) {
20789
20817
  const pendingOps = shouldReadPendingOps ? getPendingOps(args.db, args.sessionId) : [];
20790
20818
  const hasPendingUserOps = pendingOps.length > 0;
20791
20819
  const shouldApplyPendingOps = (args.schedulerDecision === "execute" || isExplicitFlush) && !compartmentRunning;
20820
+ const isCacheBustingPass = isExplicitFlush || shouldApplyPendingOps;
20792
20821
  const shouldRunHeuristics = args.fullFeatureMode && !compartmentRunning && (isExplicitFlush || forceMaterialization || hasPendingUserOps && args.schedulerDecision === "execute" && !alreadyRanThisTurn);
20793
20822
  if (shouldRunHeuristics) {
20794
20823
  const reason = isExplicitFlush ? "explicit_flush" : forceMaterialization ? `force_materialization (${args.contextUsage.percentage.toFixed(1)}% >= ${args.forceMaterializationPercentage}%)` : `pending_ops_execute (pendingOps=${pendingOps.length}, scheduler=${args.schedulerDecision})`;
@@ -20854,9 +20883,10 @@ function runPostTransformPhase(args) {
20854
20883
  } catch (error48) {
20855
20884
  sessionLog(args.sessionId, "transform failed applying pending operations:", error48);
20856
20885
  updateSessionMeta(args.db, args.sessionId, { lastTransformError: getErrorMessage(error48) });
20857
- args.nudgePlacements.clear(args.sessionId);
20886
+ if (isCacheBustingPass)
20887
+ args.nudgePlacements.clear(args.sessionId);
20858
20888
  }
20859
- if (didMutateFromPendingOperations) {
20889
+ if (didMutateFromPendingOperations && isCacheBustingPass) {
20860
20890
  args.nudgePlacements.clear(args.sessionId);
20861
20891
  }
20862
20892
  if (shouldRunHeuristics && (args.didMutateFromFlushedStatuses || didMutateFromPendingOperations)) {
@@ -20878,16 +20908,18 @@ function runPostTransformPhase(args) {
20878
20908
  if (strippedDropped > 0) {
20879
20909
  sessionLog(args.sessionId, `stripped ${strippedDropped} empty dropped-placeholder messages`);
20880
20910
  }
20881
- const protectedTailStart = Math.max(0, args.messages.length - args.protectedTags * 2);
20882
- const strippedSystemInjected = stripSystemInjectedMessages(args.messages, protectedTailStart);
20883
- if (strippedSystemInjected > 0) {
20884
- sessionLog(args.sessionId, `stripped ${strippedSystemInjected} system-injected messages (notifications/reminders)`);
20911
+ if (isCacheBustingPass) {
20912
+ const protectedTailStart = Math.max(0, args.messages.length - args.protectedTags * 2);
20913
+ const strippedSystemInjected = stripSystemInjectedMessages(args.messages, protectedTailStart);
20914
+ if (strippedSystemInjected > 0) {
20915
+ sessionLog(args.sessionId, `stripped ${strippedSystemInjected} system-injected messages (notifications/reminders)`);
20916
+ }
20885
20917
  }
20886
20918
  const pendingUserTurnReminder = getPersistedStickyTurnReminder(args.db, args.sessionId);
20887
20919
  if (pendingUserTurnReminder) {
20888
- if (args.hasRecentReduceCall) {
20920
+ if (args.hasRecentReduceCall && isCacheBustingPass) {
20889
20921
  clearPersistedStickyTurnReminder(args.db, args.sessionId);
20890
- sessionLog(args.sessionId, "sticky turn reminder cleared \u2014 ctx_reduce found in recent messages");
20922
+ sessionLog(args.sessionId, "sticky turn reminder cleared \u2014 ctx_reduce found in recent messages (cache-busting pass)");
20891
20923
  } else {
20892
20924
  if (pendingUserTurnReminder.messageId) {
20893
20925
  const reinjected = appendReminderToUserMessageById(args.messages, pendingUserTurnReminder.messageId, pendingUserTurnReminder.text);
@@ -20914,15 +20946,20 @@ function runPostTransformPhase(args) {
20914
20946
  const t9 = performance.now();
20915
20947
  applyContextNudge(args.messages, nudge, args.nudgePlacements, args.sessionId);
20916
20948
  logTransformTiming(args.sessionId, "applyContextNudge", t9);
20917
- } else {
20949
+ } else if (isCacheBustingPass) {
20918
20950
  args.nudgePlacements.clear(args.sessionId);
20951
+ } else {
20952
+ const existing = args.nudgePlacements.get(args.sessionId);
20953
+ if (existing) {
20954
+ reinjectNudgeAtAnchor(args.messages, existing.nudgeText, args.nudgePlacements, args.sessionId);
20955
+ }
20919
20956
  }
20920
- const canInjectDeferredNoteNudge = canAppendSupplementalNudgeToAssistant(args.messages, args.nudgePlacements, args.sessionId);
20921
20957
  const deferredNoteText = getNoteNudgeText(args.db, args.sessionId);
20922
- if (deferredNoteText && canInjectDeferredNoteNudge) {
20923
- appendSupplementalNudgeToAssistant(args.messages, `
20958
+ if (deferredNoteText) {
20959
+ const noteInstruction = `
20924
20960
 
20925
- <instruction name="deferred_notes">${deferredNoteText}</instruction>`, args.nudgePlacements, args.sessionId);
20961
+ <instruction name="deferred_notes">${deferredNoteText}</instruction>`;
20962
+ appendReminderToLatestUserMessage(args.messages, noteInstruction);
20926
20963
  }
20927
20964
  } else {
20928
20965
  args.nudgePlacements.clear(args.sessionId);
@@ -20989,10 +21026,13 @@ function createTransform(deps) {
20989
21026
  const fullFeatureMode = !reducedMode;
20990
21027
  const compartmentDirectory = deps.directory ?? "";
20991
21028
  const canRunCompartments = fullFeatureMode && deps.client !== undefined && compartmentDirectory.length > 0;
21029
+ const contextUsageEarly = loadContextUsage(deps.contextUsageMap, db, sessionId);
21030
+ const schedulerDecisionEarly = resolveSchedulerDecision(deps.scheduler, sessionMeta, contextUsageEarly, sessionId);
21031
+ const isCacheBusting = deps.flushedSessions.has(sessionId) || schedulerDecisionEarly === "execute";
20992
21032
  let pendingCompartmentInjection = null;
20993
21033
  if (fullFeatureMode) {
20994
21034
  const projectPath = deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined;
20995
- pendingCompartmentInjection = prepareCompartmentInjection(db, sessionId, messages, projectPath, deps.memoryConfig?.injectionBudgetTokens);
21035
+ pendingCompartmentInjection = prepareCompartmentInjection(db, sessionId, messages, isCacheBusting, projectPath, deps.memoryConfig?.injectionBudgetTokens);
20996
21036
  }
20997
21037
  let targets = new Map;
20998
21038
  let reasoningByMessage = new Map;
@@ -21029,9 +21069,10 @@ function createTransform(deps) {
21029
21069
  logTransformTiming(sessionId, "batchFinalize:flushed", t2);
21030
21070
  } catch (error48) {
21031
21071
  sessionLog(sessionId, "transform failed applying flushed statuses:", error48);
21032
- deps.nudgePlacements.clear(sessionId);
21072
+ if (isCacheBusting)
21073
+ deps.nudgePlacements.clear(sessionId);
21033
21074
  }
21034
- if (didMutateFromFlushedStatuses) {
21075
+ if (didMutateFromFlushedStatuses && isCacheBusting) {
21035
21076
  deps.nudgePlacements.clear(sessionId);
21036
21077
  }
21037
21078
  const t3 = performance.now();
@@ -21046,9 +21087,8 @@ function createTransform(deps) {
21046
21087
  watermark = tag.tagNumber;
21047
21088
  }
21048
21089
  }
21049
- const contextUsage = loadContextUsage(deps.contextUsageMap, db, sessionId);
21050
- const schedulerDecision = resolveSchedulerDecision(deps.scheduler, sessionMeta, contextUsage, sessionId);
21051
- const isCacheBusting = deps.flushedSessions.has(sessionId) || schedulerDecision === "execute";
21090
+ const contextUsage = contextUsageEarly;
21091
+ const schedulerDecision = schedulerDecisionEarly;
21052
21092
  const rawGetNotifParams = deps.getNotificationParams;
21053
21093
  const compartmentPhase = await runCompartmentPhase({
21054
21094
  canRunCompartments,
@@ -21127,11 +21167,13 @@ function createChatMessageHook(args) {
21127
21167
  const sessionId = input.sessionID;
21128
21168
  if (!sessionId)
21129
21169
  return;
21130
- const sessionMeta = getOrCreateSessionMeta(args.db, sessionId);
21131
- const turnUsage = args.toolUsageSinceUserTurn.get(sessionId);
21132
- const agentAlreadyReduced = args.recentReduceBySession.has(sessionId);
21133
- if (!sessionMeta.isSubagent && !agentAlreadyReduced && getPersistedStickyTurnReminder(args.db, sessionId) === null && turnUsage !== undefined && turnUsage >= TOOL_HEAVY_TURN_REMINDER_THRESHOLD) {
21134
- 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
+ }
21135
21177
  }
21136
21178
  args.toolUsageSinceUserTurn.set(sessionId, 0);
21137
21179
  const previousVariant = args.variantBySession.get(sessionId);
@@ -21177,6 +21219,8 @@ function createEventHook(args) {
21177
21219
  args.emergencyNudgeFired.delete(sessionId);
21178
21220
  return;
21179
21221
  }
21222
+ if (args.ctxReduceEnabled === false)
21223
+ return;
21180
21224
  if (args.emergencyNudgeFired.has(sessionId))
21181
21225
  return;
21182
21226
  const meta3 = getOrCreateSessionMeta(args.db, sessionId);
@@ -21249,12 +21293,18 @@ Use \`ctx_reduce\` to manage context size. It supports one operation:
21249
21293
  - \`drop\`: Remove entirely (best for tool outputs you already acted on).
21250
21294
  Syntax: "3-5", "1,2,9", or "1-5,8,12-15". Last ${protectedTags} tags are protected.
21251
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).
21252
- 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.
21253
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.
21254
21299
  NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
21255
21300
  NEVER drop user messages \u2014 they are short and will be summarized by compartmentalization automatically. Dropping them loses context the historian needs.
21256
21301
  NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
21257
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.`;
21258
21308
  var SISYPHUS_SECTION = `
21259
21309
  ### Reduction Triggers
21260
21310
  - After collecting background agent results (explore/librarian) \u2014 drop raw outputs once you extracted what you need.
@@ -21391,7 +21441,12 @@ function detectAgentFromSystemPrompt(systemPrompt) {
21391
21441
  }
21392
21442
  return null;
21393
21443
  }
21394
- 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
+ }
21395
21450
  const section = agent ? AGENT_SECTIONS[agent] : GENERIC_SECTION;
21396
21451
  return `## Magic Context
21397
21452
 
@@ -21412,7 +21467,7 @@ function createSystemPromptHashHandler(deps) {
21412
21467
  `);
21413
21468
  if (fullPrompt.length > 0 && !fullPrompt.includes(MAGIC_CONTEXT_MARKER)) {
21414
21469
  const detectedAgent = detectAgentFromSystemPrompt(fullPrompt);
21415
- const guidance = buildMagicContextSection(detectedAgent, deps.protectedTags);
21470
+ const guidance = buildMagicContextSection(detectedAgent, deps.protectedTags, deps.ctxReduceEnabled);
21416
21471
  output.system.push(guidance);
21417
21472
  sessionLog(sessionId, `injected ${detectedAgent ?? "generic"} guidance into system prompt`);
21418
21473
  }
@@ -21487,13 +21542,14 @@ function createMagicContextHook(deps) {
21487
21542
  const liveModelBySession = new Map;
21488
21543
  const recentReduceBySession = new Map;
21489
21544
  const toolUsageSinceUserTurn = new Map;
21490
- const nudgerWithRecentReduce = createNudger({
21545
+ const ctxReduceEnabled = deps.config.ctx_reduce_enabled !== false;
21546
+ const nudgerWithRecentReduce = ctxReduceEnabled ? createNudger({
21491
21547
  protected_tags: deps.config.protected_tags,
21492
21548
  nudge_interval_tokens: deps.config.nudge_interval_tokens ?? DEFAULT_NUDGE_INTERVAL_TOKENS,
21493
21549
  iteration_nudge_threshold: deps.config.iteration_nudge_threshold ?? 15,
21494
21550
  execute_threshold_percentage: deps.config.execute_threshold_percentage ?? 65,
21495
21551
  recentReduceBySession
21496
- });
21552
+ }) : () => null;
21497
21553
  const transform2 = createTransform({
21498
21554
  tagger: deps.tagger,
21499
21555
  scheduler: deps.scheduler,
@@ -21525,7 +21581,10 @@ function createMagicContextHook(deps) {
21525
21581
  tagger: deps.tagger,
21526
21582
  db,
21527
21583
  nudgePlacements,
21528
- onSessionCacheInvalidated: deps.onSessionCacheInvalidated
21584
+ onSessionCacheInvalidated: (sessionId) => {
21585
+ clearInjectionCache(sessionId);
21586
+ deps.onSessionCacheInvalidated?.(sessionId);
21587
+ }
21529
21588
  });
21530
21589
  const runDreamQueueInBackground = () => {
21531
21590
  const dreaming = deps.config.dreamer;
@@ -21596,6 +21655,7 @@ function createMagicContextHook(deps) {
21596
21655
  const systemPromptHashHandler = createSystemPromptHashHandler({
21597
21656
  db,
21598
21657
  protectedTags: deps.config.protected_tags,
21658
+ ctxReduceEnabled,
21599
21659
  flushedSessions,
21600
21660
  lastHeuristicsTurnId
21601
21661
  });
@@ -21612,7 +21672,8 @@ function createMagicContextHook(deps) {
21612
21672
  lastHeuristicsTurnId,
21613
21673
  commitSeenLastPass,
21614
21674
  client: deps.client,
21615
- protectedTags: deps.config.protected_tags
21675
+ protectedTags: deps.config.protected_tags,
21676
+ ctxReduceEnabled
21616
21677
  });
21617
21678
  return {
21618
21679
  "experimental.chat.messages.transform": transform2,
@@ -21624,7 +21685,8 @@ function createMagicContextHook(deps) {
21624
21685
  recentReduceBySession,
21625
21686
  variantBySession,
21626
21687
  flushedSessions,
21627
- lastHeuristicsTurnId
21688
+ lastHeuristicsTurnId,
21689
+ ctxReduceEnabled
21628
21690
  }),
21629
21691
  event: async (input) => {
21630
21692
  await eventHook(input);
@@ -21727,18 +21789,17 @@ function createCtxExpandTools() {
21727
21789
  }
21728
21790
  // src/tools/ctx-memory/constants.ts
21729
21791
  var CTX_MEMORY_TOOL_NAME = "ctx_memory";
21730
- 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.
21731
21793
 
21732
- Supported actions: write, delete, search, list, update, merge, archive.`;
21794
+ Supported actions: write, delete, list, update, merge, archive.`;
21733
21795
  var DEFAULT_SEARCH_LIMIT2 = 10;
21734
21796
  // src/tools/ctx-memory/tools.ts
21735
21797
  import { tool as tool2 } from "@opencode-ai/plugin";
21736
21798
 
21737
21799
  // src/tools/ctx-memory/types.ts
21738
- var CTX_MEMORY_ACTIONS = [
21739
- "write",
21740
- "delete",
21741
- "search",
21800
+ var CTX_MEMORY_ACTIONS = ["write", "delete"];
21801
+ var CTX_MEMORY_DREAMER_ACTIONS = [
21802
+ ...CTX_MEMORY_ACTIONS,
21742
21803
  "list",
21743
21804
  "update",
21744
21805
  "merge",
@@ -21746,9 +21807,6 @@ var CTX_MEMORY_ACTIONS = [
21746
21807
  ];
21747
21808
 
21748
21809
  // src/tools/ctx-memory/tools.ts
21749
- var SEMANTIC_WEIGHT = 0.7;
21750
- var FTS_WEIGHT = 0.3;
21751
- var SINGLE_SOURCE_PENALTY = 0.8;
21752
21810
  var MEMORY_CATEGORIES = new Set(CATEGORY_PRIORITY);
21753
21811
  function isMemoryCategory2(value) {
21754
21812
  return MEMORY_CATEGORIES.has(value);
@@ -21760,31 +21818,12 @@ function normalizeLimit(limit) {
21760
21818
  return Math.max(1, Math.floor(limit));
21761
21819
  }
21762
21820
  function getAllowedActions(deps) {
21763
- const allowed = deps.allowedActions?.length ? deps.allowedActions : [...CTX_MEMORY_ACTIONS];
21821
+ const allowed = deps.allowedActions?.length ? deps.allowedActions : [...CTX_MEMORY_DREAMER_ACTIONS];
21764
21822
  return [...allowed];
21765
21823
  }
21766
21824
  function normalizeCategory(category) {
21767
21825
  const trimmed = category?.trim();
21768
- return trimmed ? trimmed : undefined;
21769
- }
21770
- function normalizeCosineScore(score) {
21771
- if (!Number.isFinite(score)) {
21772
- return 0;
21773
- }
21774
- return Math.min(1, Math.max(0, score));
21775
- }
21776
- function formatSearchResults(query, results) {
21777
- if (results.length === 0) {
21778
- return `No memories found matching "${query}".`;
21779
- }
21780
- const noun = results.length === 1 ? "memory" : "memories";
21781
- const body = results.map((result, index) => `[${index + 1}] (score: ${result.score.toFixed(2)}) [${result.category}]
21782
- ${result.content}`).join(`
21783
-
21784
- `);
21785
- return `Found ${results.length} ${noun} matching "${query}":
21786
-
21787
- ${body}`;
21826
+ return trimmed ? trimmed : undefined;
21788
21827
  }
21789
21828
  function formatMemoryList(memories) {
21790
21829
  if (memories.length === 0) {
@@ -21843,77 +21882,6 @@ function filterByCategory(memories, category) {
21843
21882
  }
21844
21883
  return memories.filter((memory) => memory.category === category);
21845
21884
  }
21846
- async function getSemanticScores(deps, query, memories) {
21847
- const semanticScores = new Map;
21848
- if (!deps.embeddingEnabled || !isEmbeddingEnabled() || memories.length === 0) {
21849
- return semanticScores;
21850
- }
21851
- const queryEmbedding = await embedText(query);
21852
- if (!queryEmbedding) {
21853
- return semanticScores;
21854
- }
21855
- const embeddings = await ensureMemoryEmbeddings({
21856
- db: deps.db,
21857
- memories,
21858
- existingEmbeddings: loadAllEmbeddings(deps.db, deps.projectPath)
21859
- });
21860
- for (const memory of memories) {
21861
- const memoryEmbedding = embeddings.get(memory.id);
21862
- if (!memoryEmbedding) {
21863
- continue;
21864
- }
21865
- semanticScores.set(memory.id, normalizeCosineScore(cosineSimilarity(queryEmbedding, memoryEmbedding)));
21866
- }
21867
- return semanticScores;
21868
- }
21869
- function getFtsScores(deps, query, category, limit = DEFAULT_SEARCH_LIMIT2) {
21870
- try {
21871
- const matches = filterByCategory(searchMemoriesFTS(deps.db, deps.projectPath, query, limit), category);
21872
- return new Map(matches.map((memory, rank) => [memory.id, 1 / (rank + 1)]));
21873
- } catch {
21874
- return new Map;
21875
- }
21876
- }
21877
- function mergeResults(memories, semanticScores, ftsScores, limit) {
21878
- const memoryById = new Map(memories.map((memory) => [memory.id, memory]));
21879
- const candidateIds = new Set([...semanticScores.keys(), ...ftsScores.keys()]);
21880
- const results = [];
21881
- for (const id of candidateIds) {
21882
- const memory = memoryById.get(id);
21883
- if (!memory) {
21884
- continue;
21885
- }
21886
- const semanticScore = semanticScores.get(id);
21887
- const ftsScore = ftsScores.get(id);
21888
- let score = 0;
21889
- let source = "fts";
21890
- if (semanticScore !== undefined && ftsScore !== undefined) {
21891
- score = SEMANTIC_WEIGHT * semanticScore + FTS_WEIGHT * ftsScore;
21892
- source = "hybrid";
21893
- } else if (semanticScore !== undefined) {
21894
- score = semanticScore * SINGLE_SOURCE_PENALTY;
21895
- source = "semantic";
21896
- } else if (ftsScore !== undefined) {
21897
- score = ftsScore * SINGLE_SOURCE_PENALTY;
21898
- source = "fts";
21899
- }
21900
- if (score > 0) {
21901
- results.push({
21902
- id,
21903
- category: memory.category,
21904
- content: memory.content,
21905
- score,
21906
- source
21907
- });
21908
- }
21909
- }
21910
- return results.sort((left, right) => {
21911
- if (right.score !== left.score) {
21912
- return right.score - left.score;
21913
- }
21914
- return left.id - right.id;
21915
- }).slice(0, limit);
21916
- }
21917
21885
  function queueMemoryEmbedding(deps, memoryId, content) {
21918
21886
  (async () => {
21919
21887
  const embedding2 = await embedText(content);
@@ -21946,13 +21914,12 @@ function createCtxMemoryTool(deps) {
21946
21914
  return tool2({
21947
21915
  description: CTX_MEMORY_DESCRIPTION,
21948
21916
  args: {
21949
- 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"),
21950
21918
  content: tool2.schema.string().optional().describe("Memory content (required for write, update, merge)"),
21951
- 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)"),
21952
21920
  id: tool2.schema.number().optional().describe("Memory ID (required for delete, update, archive)"),
21953
21921
  ids: tool2.schema.array(tool2.schema.number()).optional().describe("Memory IDs to merge (required for merge)"),
21954
- query: tool2.schema.string().optional().describe("Natural language search query for project memories (required for search)"),
21955
- 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)"),
21956
21923
  reason: tool2.schema.string().optional().describe("Archive reason (optional for archive)")
21957
21924
  },
21958
21925
  async execute(args, toolContext) {
@@ -22107,30 +22074,6 @@ function createCtxMemoryTool(deps) {
22107
22074
  archiveMemory(deps.db, args.id, args.reason);
22108
22075
  return args.reason?.trim() ? `Archived memory [ID: ${args.id}] (${args.reason.trim()}).` : `Archived memory [ID: ${args.id}].`;
22109
22076
  }
22110
- if (args.action === "search") {
22111
- if (typeof args.query !== "string") {
22112
- return "Error: 'query' must be provided when action is 'search'.";
22113
- }
22114
- const query = args.query.trim();
22115
- if (!query) {
22116
- return "Error: 'query' must be provided when action is 'search'.";
22117
- }
22118
- const limit = normalizeLimit(args.limit);
22119
- const category = normalizeCategory(args.category);
22120
- const projectMemories = filterByCategory(getMemoriesByProject(deps.db, deps.projectPath), category);
22121
- const ftsLimit = Math.max(limit * 5, projectMemories.length, DEFAULT_SEARCH_LIMIT2);
22122
- const semanticScores = await getSemanticScores(deps, query, projectMemories);
22123
- const ftsScores = getFtsScores(deps, query, category, ftsLimit);
22124
- const results = mergeResults(projectMemories, semanticScores, ftsScores, limit);
22125
- if (results.length > 0) {
22126
- deps.db.transaction(() => {
22127
- for (const result of results) {
22128
- updateMemoryRetrievalCount(deps.db, result.id);
22129
- }
22130
- })();
22131
- }
22132
- return formatSearchResults(query, results);
22133
- }
22134
22077
  return "Error: Unknown action.";
22135
22078
  }
22136
22079
  });
@@ -22335,8 +22278,458 @@ function createCtxReduceTools(deps) {
22335
22278
  ctx_reduce: createCtxReduceTool(deps)
22336
22279
  };
22337
22280
  }
22338
- // 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
22339
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";
22340
22733
  function stripRootJsonSchemaFields(jsonSchema) {
22341
22734
  const { $schema: _schema, ...rest } = jsonSchema;
22342
22735
  return rest;
@@ -22349,7 +22742,7 @@ function attachJsonSchemaOverride(schema) {
22349
22742
  const originalOverride = schema._zod.toJSONSchema;
22350
22743
  delete schema._zod.toJSONSchema;
22351
22744
  try {
22352
- return stripRootJsonSchemaFields(tool5.schema.toJSONSchema(schema));
22745
+ return stripRootJsonSchemaFields(tool6.schema.toJSONSchema(schema));
22353
22746
  } finally {
22354
22747
  schema._zod.toJSONSchema = originalOverride;
22355
22748
  }
@@ -22390,20 +22783,27 @@ function createToolRegistry(args) {
22390
22783
  console.warn(`[magic-context] embedding model changed from ${storedModelId} to ${currentModelId}; cleared embeddings for project ${projectPath}`);
22391
22784
  }
22392
22785
  }
22786
+ const ctxReduceEnabled = pluginConfig.ctx_reduce_enabled !== false;
22393
22787
  const allTools = {
22394
- ...createCtxReduceTools({
22788
+ ...ctxReduceEnabled ? createCtxReduceTools({
22395
22789
  db,
22396
22790
  protectedTags: pluginConfig.protected_tags ?? DEFAULT_PROTECTED_TAGS
22397
- }),
22791
+ }) : {},
22398
22792
  ...createCtxExpandTools(),
22399
22793
  ...createCtxNoteTools({ db }),
22794
+ ...createCtxSearchTools({
22795
+ db,
22796
+ projectPath,
22797
+ memoryEnabled,
22798
+ embeddingEnabled: embeddingConfig2.provider !== "off"
22799
+ }),
22400
22800
  ...memoryEnabled ? {
22401
22801
  ...createCtxMemoryTools({
22402
22802
  db,
22403
22803
  projectPath,
22404
22804
  memoryEnabled: true,
22405
22805
  embeddingEnabled: embeddingConfig2.provider !== "off",
22406
- allowedActions: ["write", "delete", "search"]
22806
+ allowedActions: ["write", "delete"]
22407
22807
  })
22408
22808
  } : {}
22409
22809
  };
@@ -22503,12 +22903,18 @@ var plugin = async (ctx) => {
22503
22903
  ctx,
22504
22904
  pluginConfig
22505
22905
  });
22506
- const tools4 = createToolRegistry({
22906
+ const tools5 = createToolRegistry({
22507
22907
  ctx,
22508
22908
  pluginConfig
22509
22909
  });
22910
+ if (pluginConfig.dreamer) {
22911
+ startDreamScheduleTimer({
22912
+ client: ctx.client,
22913
+ dreamerConfig: pluginConfig.dreamer
22914
+ });
22915
+ }
22510
22916
  return {
22511
- tool: tools4,
22917
+ tool: tools5,
22512
22918
  event: createEventHandler({ magicContext: hooks.magicContext }),
22513
22919
  "experimental.chat.messages.transform": createMessagesTransformHandler({
22514
22920
  magicContext: hooks.magicContext