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