@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.
- package/README.md +14 -3
- package/dist/agents/magic-context-prompt.d.ts +1 -1
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +6 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
- package/dist/features/magic-context/index.d.ts +1 -0
- package/dist/features/magic-context/index.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts +8 -0
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +4 -0
- package/dist/features/magic-context/message-index.d.ts.map +1 -0
- package/dist/features/magic-context/search.d.ts +36 -0
- package/dist/features/magic-context/search.d.ts.map +1 -0
- package/dist/features/magic-context/sidekick/agent.d.ts +1 -1
- package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -1
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts +2 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +1 -0
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +3 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +5 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +1 -0
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1961 -1555
- package/dist/plugin/dream-timer.d.ts +14 -0
- package/dist/plugin/dream-timer.d.ts.map +1 -0
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/tools/ctx-memory/constants.d.ts +1 -1
- package/dist/tools/ctx-memory/constants.d.ts.map +1 -1
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tools/ctx-memory/types.d.ts +3 -10
- package/dist/tools/ctx-memory/types.d.ts.map +1 -1
- package/dist/tools/ctx-search/constants.d.ts +4 -0
- package/dist/tools/ctx-search/constants.d.ts.map +1 -0
- package/dist/tools/ctx-search/index.d.ts +4 -0
- package/dist/tools/ctx-search/index.d.ts.map +1 -0
- package/dist/tools/ctx-search/tools.d.ts +4 -0
- package/dist/tools/ctx-search/tools.d.ts.map +1 -0
- package/dist/tools/ctx-search/types.d.ts +19 -0
- package/dist/tools/ctx-search/types.d.ts.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- 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: "
|
|
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
|
|
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
|
|
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/
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
|
|
14952
|
-
|
|
14953
|
-
|
|
14954
|
-
|
|
14955
|
-
|
|
14956
|
-
|
|
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
|
-
|
|
14958
|
+
return stmt;
|
|
15192
14959
|
}
|
|
15193
|
-
function
|
|
15194
|
-
|
|
15195
|
-
|
|
15196
|
-
|
|
15197
|
-
|
|
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
|
|
15203
|
-
|
|
15204
|
-
|
|
15205
|
-
|
|
15206
|
-
|
|
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
|
|
15238
|
-
|
|
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
|
|
15283
|
-
|
|
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
|
|
15287
|
-
|
|
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/
|
|
15305
|
-
|
|
15306
|
-
|
|
15307
|
-
|
|
15308
|
-
|
|
15309
|
-
|
|
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
|
|
15312
|
-
|
|
15313
|
-
|
|
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
|
|
15318
|
-
|
|
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
|
|
15324
|
-
|
|
15325
|
-
|
|
15326
|
-
|
|
15327
|
-
|
|
15328
|
-
|
|
15329
|
-
|
|
15330
|
-
|
|
15331
|
-
|
|
15332
|
-
|
|
15333
|
-
|
|
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
|
|
15337
|
-
|
|
15338
|
-
|
|
15339
|
-
|
|
15340
|
-
|
|
15341
|
-
|
|
15342
|
-
|
|
15343
|
-
|
|
15344
|
-
|
|
15345
|
-
|
|
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
|
|
15033
|
+
function releaseLease(db, holderId) {
|
|
15350
15034
|
db.transaction(() => {
|
|
15351
|
-
|
|
15352
|
-
|
|
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
|
-
|
|
15356
|
-
|
|
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
|
|
15359
|
-
const
|
|
15360
|
-
if (!
|
|
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
|
-
|
|
15368
|
-
|
|
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
|
|
15372
|
-
|
|
15373
|
-
|
|
15374
|
-
|
|
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
|
|
15378
|
-
db.prepare("
|
|
15085
|
+
function removeDreamEntry(db, id) {
|
|
15086
|
+
db.prepare("DELETE FROM dream_queue WHERE id = ?").run(id);
|
|
15379
15087
|
}
|
|
15380
|
-
|
|
15381
|
-
|
|
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
|
|
15391
|
-
const
|
|
15392
|
-
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
|
|
15396
|
-
|
|
15397
|
-
|
|
15398
|
-
|
|
15399
|
-
|
|
15400
|
-
|
|
15401
|
-
|
|
15402
|
-
|
|
15403
|
-
|
|
15404
|
-
|
|
15405
|
-
|
|
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
|
-
|
|
15409
|
-
|
|
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
|
-
|
|
15412
|
-
|
|
15413
|
-
|
|
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
|
-
|
|
15462
|
-
|
|
15463
|
-
|
|
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
|
-
|
|
15467
|
-
|
|
15468
|
-
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
|
|
15472
|
-
|
|
15473
|
-
|
|
15474
|
-
|
|
15475
|
-
|
|
15476
|
-
|
|
15477
|
-
|
|
15478
|
-
|
|
15479
|
-
|
|
15480
|
-
}
|
|
15481
|
-
|
|
15482
|
-
|
|
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
|
|
15505
|
-
|
|
15506
|
-
|
|
15507
|
-
|
|
15508
|
-
|
|
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
|
|
15305
|
+
return result;
|
|
15511
15306
|
}
|
|
15512
|
-
// src/features/magic-context/
|
|
15513
|
-
|
|
15514
|
-
|
|
15515
|
-
|
|
15516
|
-
|
|
15517
|
-
|
|
15518
|
-
|
|
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
|
-
|
|
15319
|
+
const startMinutes = startHour * 60 + startMin;
|
|
15320
|
+
const endMinutes = endHour * 60 + endMin;
|
|
15321
|
+
return { startMinutes, endMinutes };
|
|
15521
15322
|
}
|
|
15522
|
-
function
|
|
15523
|
-
|
|
15323
|
+
function isInScheduleWindow(schedule, now = new Date) {
|
|
15324
|
+
const window = parseScheduleWindow(schedule);
|
|
15325
|
+
if (!window)
|
|
15524
15326
|
return false;
|
|
15525
|
-
const
|
|
15526
|
-
|
|
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
|
|
15331
|
+
return currentMinutes >= window.startMinutes || currentMinutes < window.endMinutes;
|
|
15595
15332
|
}
|
|
15596
|
-
function
|
|
15597
|
-
|
|
15598
|
-
|
|
15599
|
-
|
|
15600
|
-
|
|
15601
|
-
|
|
15602
|
-
|
|
15603
|
-
|
|
15604
|
-
|
|
15605
|
-
if (
|
|
15606
|
-
|
|
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
|
|
15346
|
+
return projects;
|
|
15610
15347
|
}
|
|
15611
|
-
function
|
|
15612
|
-
if (
|
|
15613
|
-
return
|
|
15348
|
+
function checkScheduleAndEnqueue(db, schedule) {
|
|
15349
|
+
if (!isInScheduleWindow(schedule)) {
|
|
15350
|
+
return 0;
|
|
15614
15351
|
}
|
|
15615
|
-
|
|
15616
|
-
|
|
15352
|
+
const projects = findProjectsNeedingDream(db);
|
|
15353
|
+
if (projects.length === 0) {
|
|
15354
|
+
return 0;
|
|
15617
15355
|
}
|
|
15618
|
-
|
|
15619
|
-
|
|
15620
|
-
|
|
15621
|
-
|
|
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
|
|
15364
|
+
return enqueued;
|
|
15625
15365
|
}
|
|
15626
|
-
|
|
15627
|
-
|
|
15628
|
-
|
|
15629
|
-
|
|
15630
|
-
|
|
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
|
|
15633
|
-
if (
|
|
15634
|
-
|
|
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
|
|
15637
|
-
if (
|
|
15615
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
15616
|
+
if (rows.some((row) => row.name === column)) {
|
|
15638
15617
|
return;
|
|
15639
15618
|
}
|
|
15640
|
-
|
|
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
|
-
|
|
15651
|
-
|
|
15652
|
-
|
|
15653
|
-
|
|
15654
|
-
|
|
15655
|
-
|
|
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
|
|
15672
|
-
|
|
15673
|
-
|
|
15674
|
-
|
|
15675
|
-
|
|
15676
|
-
|
|
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
|
-
|
|
15740
|
-
const
|
|
15741
|
-
db
|
|
15742
|
-
|
|
15743
|
-
|
|
15744
|
-
|
|
15745
|
-
|
|
15746
|
-
|
|
15747
|
-
|
|
15748
|
-
|
|
15749
|
-
|
|
15750
|
-
|
|
15751
|
-
|
|
15752
|
-
|
|
15753
|
-
|
|
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
|
|
15782
|
-
|
|
15783
|
-
|
|
15784
|
-
|
|
15785
|
-
|
|
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
|
-
|
|
15790
|
-
|
|
15791
|
-
|
|
15792
|
-
|
|
15793
|
-
|
|
15794
|
-
|
|
15795
|
-
|
|
15796
|
-
|
|
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/
|
|
15801
|
-
|
|
15802
|
-
|
|
15803
|
-
|
|
15804
|
-
|
|
15805
|
-
|
|
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
|
|
15813
|
-
|
|
15814
|
-
|
|
15815
|
-
|
|
15816
|
-
|
|
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
|
|
15821
|
-
|
|
15822
|
-
|
|
15823
|
-
|
|
15824
|
-
|
|
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
|
|
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
|
|
15829
|
-
const
|
|
15830
|
-
|
|
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
|
|
15833
|
-
|
|
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
|
|
15836
|
-
|
|
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
|
-
|
|
15840
|
-
|
|
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
|
-
|
|
15850
|
-
|
|
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
|
|
15853
|
-
|
|
15854
|
-
|
|
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
|
|
15857
|
-
|
|
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
|
-
|
|
15860
|
-
|
|
15861
|
-
|
|
15862
|
-
|
|
15863
|
-
|
|
15864
|
-
|
|
15865
|
-
|
|
15866
|
-
|
|
15867
|
-
|
|
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
|
|
15875
|
-
|
|
15876
|
-
|
|
15877
|
-
|
|
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
|
-
|
|
15880
|
-
|
|
15881
|
-
|
|
15882
|
-
|
|
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
|
|
15844
|
+
function clearSession(db, sessionId) {
|
|
15886
15845
|
db.transaction(() => {
|
|
15887
|
-
|
|
15888
|
-
|
|
15889
|
-
|
|
15890
|
-
|
|
15891
|
-
|
|
15892
|
-
|
|
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/
|
|
15896
|
-
function
|
|
15897
|
-
|
|
15898
|
-
|
|
15899
|
-
|
|
15900
|
-
|
|
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
|
|
15914
|
-
|
|
15915
|
-
|
|
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
|
-
|
|
15920
|
-
|
|
15921
|
-
|
|
15922
|
-
|
|
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
|
|
15926
|
-
|
|
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
|
|
15938
|
-
db.prepare("
|
|
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
|
|
15941
|
-
db.prepare("
|
|
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
|
-
|
|
15944
|
-
|
|
15945
|
-
|
|
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
|
|
15948
|
-
|
|
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
|
-
|
|
15953
|
-
|
|
15954
|
-
|
|
15955
|
-
|
|
15956
|
-
|
|
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
|
|
15960
|
-
|
|
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
|
-
|
|
15963
|
-
|
|
15964
|
-
|
|
15965
|
-
|
|
15966
|
-
|
|
15967
|
-
|
|
15968
|
-
|
|
15969
|
-
|
|
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
|
-
|
|
15972
|
-
|
|
15973
|
-
|
|
15974
|
-
|
|
15975
|
-
|
|
15976
|
-
|
|
15977
|
-
|
|
15978
|
-
|
|
15979
|
-
|
|
15980
|
-
|
|
15981
|
-
|
|
15982
|
-
|
|
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
|
-
|
|
15985
|
-
|
|
15986
|
-
|
|
15987
|
-
|
|
15988
|
-
|
|
15989
|
-
|
|
15990
|
-
|
|
15991
|
-
|
|
15992
|
-
|
|
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
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16013
|
-
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
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
|
-
}
|
|
16104
|
-
|
|
16105
|
-
|
|
16012
|
+
}, DREAM_TIMER_INTERVAL_MS);
|
|
16013
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
16014
|
+
timer.unref();
|
|
16106
16015
|
}
|
|
16107
|
-
|
|
16108
|
-
|
|
16109
|
-
|
|
16110
|
-
|
|
16111
|
-
|
|
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
|
-
|
|
16114
|
-
|
|
16115
|
-
|
|
16116
|
-
|
|
16117
|
-
|
|
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
|
-
|
|
16120
|
-
|
|
16121
|
-
|
|
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
|
-
|
|
16127
|
-
|
|
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
|
-
|
|
16145
|
-
|
|
16146
|
-
|
|
16147
|
-
|
|
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
|
|
16074
|
+
return cacheTtl.default ?? "5m";
|
|
16158
16075
|
}
|
|
16159
|
-
|
|
16160
|
-
|
|
16161
|
-
|
|
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
|
-
|
|
16172
|
-
|
|
16173
|
-
|
|
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
|
|
16176
|
-
|
|
16177
|
-
|
|
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
|
-
|
|
16170
|
+
const candidate = row;
|
|
16171
|
+
return typeof candidate.message_id === "string" && typeof candidate.tag_number === "number";
|
|
16184
16172
|
}
|
|
16185
|
-
|
|
16186
|
-
|
|
16187
|
-
|
|
16188
|
-
|
|
16189
|
-
|
|
16190
|
-
|
|
16191
|
-
|
|
16192
|
-
|
|
16193
|
-
|
|
16194
|
-
|
|
16195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16205
|
-
|
|
16206
|
-
return 0;
|
|
16217
|
+
function bindTag(sessionId, messageId, tagNumber) {
|
|
16218
|
+
getSessionAssignments(sessionId).set(messageId, tagNumber);
|
|
16207
16219
|
}
|
|
16208
|
-
|
|
16209
|
-
|
|
16210
|
-
|
|
16211
|
-
|
|
16212
|
-
|
|
16213
|
-
|
|
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
|
-
|
|
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
|
|
17872
|
-
if (
|
|
17873
|
-
memoryBlock =
|
|
17874
|
-
memoryCount =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20882
|
-
|
|
20883
|
-
|
|
20884
|
-
|
|
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
|
|
20923
|
-
|
|
20958
|
+
if (deferredNoteText) {
|
|
20959
|
+
const noteInstruction = `
|
|
20924
20960
|
|
|
20925
|
-
<instruction name="deferred_notes">${deferredNoteText}</instruction
|
|
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
|
-
|
|
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 =
|
|
21050
|
-
const schedulerDecision =
|
|
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
|
-
|
|
21131
|
-
|
|
21132
|
-
|
|
21133
|
-
|
|
21134
|
-
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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,
|
|
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
|
-
|
|
21740
|
-
|
|
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 : [...
|
|
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(
|
|
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
|
|
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
|
-
|
|
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/
|
|
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(
|
|
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"
|
|
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
|
|
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:
|
|
22917
|
+
tool: tools5,
|
|
22512
22918
|
event: createEventHandler({ magicContext: hooks.magicContext }),
|
|
22513
22919
|
"experimental.chat.messages.transform": createMessagesTransformHandler({
|
|
22514
22920
|
magicContext: hooks.magicContext
|