@cortexkit/opencode-magic-context 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli.js +14 -0
- package/dist/config/schema/magic-context.d.ts +21 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/magic-context/compaction-marker.d.ts +72 -0
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/runner.d.ts +8 -0
- package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +8 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -0
- package/dist/features/magic-context/plugin-messages.d.ts +75 -0
- package/dist/features/magic-context/plugin-messages.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +10 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-notes.d.ts +51 -5
- package/dist/features/magic-context/storage-notes.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -2
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts +20 -0
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -0
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +33 -0
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -0
- package/dist/hooks/magic-context/command-handler.d.ts +4 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts +27 -0
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-prompt.d.ts +4 -1
- package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +5 -0
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +7 -0
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/note-nudger.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +2 -0
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +4 -0
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +2 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1239 -159
- package/dist/plugin/dream-timer.d.ts +4 -0
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/tui-action-consumer.d.ts +13 -0
- package/dist/plugin/tui-action-consumer.d.ts.map +1 -0
- package/dist/tools/ctx-note/constants.d.ts +1 -1
- package/dist/tools/ctx-note/constants.d.ts.map +1 -1
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tools/ctx-note/types.d.ts +3 -1
- package/dist/tools/ctx-note/types.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts +20 -0
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/tui/data/context-db.ts +114 -6
- package/src/tui/index.tsx +77 -2
- package/src/tui/slots/sidebar-content.tsx +3 -2
- package/dist/features/magic-context/storage-smart-notes.d.ts +0 -24
- package/dist/features/magic-context/storage-smart-notes.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -8323,10 +8323,10 @@ var require_stringify = __commonJS((exports, module) => {
|
|
|
8323
8323
|
replacer = null;
|
|
8324
8324
|
indent = EMPTY;
|
|
8325
8325
|
};
|
|
8326
|
-
var
|
|
8326
|
+
var join13 = (one, two, gap) => one ? two ? one + two.trim() + LF + gap : one.trimRight() + repeat_line_breaks(Math.max(1, count_trailing_line_breaks(one, gap)), gap) : two ? two.trimRight() + repeat_line_breaks(Math.max(1, count_trailing_line_breaks(two, gap)), gap) : EMPTY;
|
|
8327
8327
|
var join_content = (inside, value, gap) => {
|
|
8328
8328
|
const comment = process_comments(value, PREFIX_BEFORE, gap + indent, true);
|
|
8329
|
-
return
|
|
8329
|
+
return join13(comment, inside, gap);
|
|
8330
8330
|
};
|
|
8331
8331
|
var stringify_string = (holder, key, value) => {
|
|
8332
8332
|
const raw = get_raw_string_literal(holder, key);
|
|
@@ -8348,13 +8348,13 @@ var require_stringify = __commonJS((exports, module) => {
|
|
|
8348
8348
|
if (i !== 0) {
|
|
8349
8349
|
inside += COMMA;
|
|
8350
8350
|
}
|
|
8351
|
-
const before =
|
|
8351
|
+
const before = join13(after_comma, process_comments(value, BEFORE(i), deeper_gap), deeper_gap);
|
|
8352
8352
|
inside += before || LF + deeper_gap;
|
|
8353
8353
|
inside += stringify(i, value, deeper_gap) || STR_NULL;
|
|
8354
8354
|
inside += process_comments(value, AFTER_VALUE(i), deeper_gap);
|
|
8355
8355
|
after_comma = process_comments(value, AFTER(i), deeper_gap);
|
|
8356
8356
|
}
|
|
8357
|
-
inside +=
|
|
8357
|
+
inside += join13(after_comma, process_comments(value, PREFIX_AFTER, deeper_gap), deeper_gap);
|
|
8358
8358
|
return BRACKET_OPEN + join_content(inside, value, gap) + BRACKET_CLOSE;
|
|
8359
8359
|
};
|
|
8360
8360
|
var object_stringify = (value, gap) => {
|
|
@@ -8375,13 +8375,13 @@ var require_stringify = __commonJS((exports, module) => {
|
|
|
8375
8375
|
inside += COMMA;
|
|
8376
8376
|
}
|
|
8377
8377
|
first = false;
|
|
8378
|
-
const before =
|
|
8378
|
+
const before = join13(after_comma, process_comments(value, BEFORE(key), deeper_gap), deeper_gap);
|
|
8379
8379
|
inside += before || LF + deeper_gap;
|
|
8380
8380
|
inside += quote(key) + process_comments(value, AFTER_PROP(key), deeper_gap) + COLON + process_comments(value, AFTER_COLON(key), deeper_gap) + SPACE + sv + process_comments(value, AFTER_VALUE(key), deeper_gap);
|
|
8381
8381
|
after_comma = process_comments(value, AFTER(key), deeper_gap);
|
|
8382
8382
|
};
|
|
8383
8383
|
keys.forEach(iteratee);
|
|
8384
|
-
inside +=
|
|
8384
|
+
inside += join13(after_comma, process_comments(value, PREFIX_AFTER, deeper_gap), deeper_gap);
|
|
8385
8385
|
return CURLY_BRACKET_OPEN + join_content(inside, value, gap) + CURLY_BRACKET_CLOSE;
|
|
8386
8386
|
};
|
|
8387
8387
|
function stringify(key, holder, gap) {
|
|
@@ -8479,11 +8479,11 @@ __export(exports_tui_config, {
|
|
|
8479
8479
|
ensureTuiPluginEntry: () => ensureTuiPluginEntry
|
|
8480
8480
|
});
|
|
8481
8481
|
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
8482
|
-
import { dirname, join as
|
|
8482
|
+
import { dirname, join as join13 } from "path";
|
|
8483
8483
|
function resolveTuiConfigPath() {
|
|
8484
8484
|
const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
|
|
8485
|
-
const jsoncPath =
|
|
8486
|
-
const jsonPath =
|
|
8485
|
+
const jsoncPath = join13(configDir, "tui.jsonc");
|
|
8486
|
+
const jsonPath = join13(configDir, "tui.json");
|
|
8487
8487
|
if (existsSync6(jsoncPath))
|
|
8488
8488
|
return jsoncPath;
|
|
8489
8489
|
if (existsSync6(jsonPath))
|
|
@@ -22197,6 +22197,16 @@ var MagicContextConfigSchema = exports_external.object({
|
|
|
22197
22197
|
provider: "local",
|
|
22198
22198
|
model: DEFAULT_LOCAL_EMBEDDING_MODEL
|
|
22199
22199
|
}),
|
|
22200
|
+
experimental: exports_external.object({
|
|
22201
|
+
compaction_markers: exports_external.boolean().default(false),
|
|
22202
|
+
user_memories: exports_external.object({
|
|
22203
|
+
enabled: exports_external.boolean().default(false),
|
|
22204
|
+
promotion_threshold: exports_external.number().min(2).max(20).default(3)
|
|
22205
|
+
}).default({ enabled: false, promotion_threshold: 3 })
|
|
22206
|
+
}).default({
|
|
22207
|
+
compaction_markers: false,
|
|
22208
|
+
user_memories: { enabled: false, promotion_threshold: 3 }
|
|
22209
|
+
}),
|
|
22200
22210
|
memory: exports_external.object({
|
|
22201
22211
|
enabled: exports_external.boolean().default(true),
|
|
22202
22212
|
injection_budget_tokens: exports_external.number().min(500).max(20000).default(4000),
|
|
@@ -23262,6 +23272,22 @@ More summary text.
|
|
|
23262
23272
|
</output>
|
|
23263
23273
|
|
|
23264
23274
|
Omit empty fact categories. Compartments must be ordered, contiguous for the ranges they cover, and non-overlapping.`;
|
|
23275
|
+
var USER_OBSERVATIONS_APPENDIX = `
|
|
23276
|
+
|
|
23277
|
+
User observation rules (EXPERIMENTAL):
|
|
23278
|
+
- After outputting compartments and facts, also output a <user_observations> section.
|
|
23279
|
+
- User observations capture UNIVERSAL behavioral patterns about the human user \u2014 not project-specific or technical.
|
|
23280
|
+
- Good observations: communication preferences, review focus areas, expertise level, decision-making patterns, frustration triggers, working style.
|
|
23281
|
+
- Bad observations (DO NOT emit): project-specific preferences, framework choices, coding language preferences, one-off moods, task-local frustration.
|
|
23282
|
+
- Each observation must be a single concise sentence in present tense.
|
|
23283
|
+
- Only emit observations you have strong evidence for from the conversation. Do not speculate.
|
|
23284
|
+
- Emit 0-5 observations per run. Zero is fine if nothing stands out.
|
|
23285
|
+
- The output shape inside <output> gains an additional section:
|
|
23286
|
+
<user_observations>
|
|
23287
|
+
* User prefers terse communication and dislikes verbose explanations.
|
|
23288
|
+
* User is technically deep \u2014 understands cache invalidation, SQLite internals, and prompt engineering.
|
|
23289
|
+
</user_observations>
|
|
23290
|
+
If no observations, omit the <user_observations> section entirely.`;
|
|
23265
23291
|
function buildCompressorPrompt(compartments, currentTokens, targetTokens, averageDepth = 0) {
|
|
23266
23292
|
const lines = [];
|
|
23267
23293
|
lines.push(`These ${compartments.length} compartments use approximately ${currentTokens} tokens. Compress them to approximately ${targetTokens} tokens.`);
|
|
@@ -23290,7 +23316,7 @@ function buildCompressorPrompt(compartments, currentTokens, targetTokens, averag
|
|
|
23290
23316
|
return lines.join(`
|
|
23291
23317
|
`);
|
|
23292
23318
|
}
|
|
23293
|
-
function buildCompartmentAgentPrompt(existingState, inputSource) {
|
|
23319
|
+
function buildCompartmentAgentPrompt(existingState, inputSource, options) {
|
|
23294
23320
|
return [
|
|
23295
23321
|
"Existing state (read-only context for continuity and fact normalization \u2014 do NOT re-emit these compartments):",
|
|
23296
23322
|
existingState,
|
|
@@ -23305,7 +23331,10 @@ function buildCompartmentAgentPrompt(existingState, inputSource) {
|
|
|
23305
23331
|
"- Rewrite every fact into terse, present-tense operational form. Merge semantic duplicates within each category.",
|
|
23306
23332
|
"- Drop any session fact already covered by a project memory in the existing state.",
|
|
23307
23333
|
"- Do not preserve prior narrative wording verbatim; if a fact is already canonical and still correct, keep or lightly normalize it.",
|
|
23308
|
-
"- Drop obsolete or task-local facts."
|
|
23334
|
+
"- Drop obsolete or task-local facts.",
|
|
23335
|
+
...options?.userMemoriesEnabled ? [
|
|
23336
|
+
"- Also emit <user_observations> with universal behavioral observations about the user (see system prompt for rules)."
|
|
23337
|
+
] : []
|
|
23309
23338
|
].join(`
|
|
23310
23339
|
`);
|
|
23311
23340
|
}
|
|
@@ -23936,7 +23965,10 @@ function getMemoryCountsByStatus(db, projectPath) {
|
|
|
23936
23965
|
};
|
|
23937
23966
|
for (const row of rows) {
|
|
23938
23967
|
counts.ids.push(row.id);
|
|
23939
|
-
if (row.
|
|
23968
|
+
if (typeof row.superseded_by_memory_id === "number") {
|
|
23969
|
+
counts.merged += 1;
|
|
23970
|
+
counts.mergedIds.push(row.id);
|
|
23971
|
+
} else if (row.status === "active") {
|
|
23940
23972
|
counts.active += 1;
|
|
23941
23973
|
} else if (row.status === "permanent") {
|
|
23942
23974
|
counts.permanent += 1;
|
|
@@ -23944,10 +23976,6 @@ function getMemoryCountsByStatus(db, projectPath) {
|
|
|
23944
23976
|
counts.archived += 1;
|
|
23945
23977
|
counts.archivedIds.push(row.id);
|
|
23946
23978
|
}
|
|
23947
|
-
if (typeof row.superseded_by_memory_id === "number") {
|
|
23948
|
-
counts.merged += 1;
|
|
23949
|
-
counts.mergedIds.push(row.id);
|
|
23950
|
-
}
|
|
23951
23979
|
}
|
|
23952
23980
|
counts.ids.sort((left, right) => left - right);
|
|
23953
23981
|
counts.archivedIds.sort((left, right) => left - right);
|
|
@@ -23955,40 +23983,96 @@ function getMemoryCountsByStatus(db, projectPath) {
|
|
|
23955
23983
|
return counts;
|
|
23956
23984
|
}
|
|
23957
23985
|
|
|
23958
|
-
// src/features/magic-context/storage-
|
|
23959
|
-
|
|
23986
|
+
// src/features/magic-context/storage-notes.ts
|
|
23987
|
+
var NOTE_TYPES = new Set(["session", "smart"]);
|
|
23988
|
+
var NOTE_STATUSES = new Set(["active", "pending", "ready", "dismissed"]);
|
|
23989
|
+
var DEFAULT_SMART_STATUSES = ["pending", "ready"];
|
|
23990
|
+
function toNullableString(value) {
|
|
23991
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
23992
|
+
}
|
|
23993
|
+
function toNullableNumber(value) {
|
|
23994
|
+
return typeof value === "number" ? value : null;
|
|
23995
|
+
}
|
|
23996
|
+
function isNoteRow(row) {
|
|
23960
23997
|
if (row === null || typeof row !== "object")
|
|
23961
23998
|
return false;
|
|
23962
|
-
const
|
|
23963
|
-
return typeof
|
|
23999
|
+
const candidate = row;
|
|
24000
|
+
return typeof candidate.id === "number" && typeof candidate.type === "string" && NOTE_TYPES.has(candidate.type) && typeof candidate.status === "string" && NOTE_STATUSES.has(candidate.status) && typeof candidate.content === "string" && (candidate.session_id === null || typeof candidate.session_id === "string") && (candidate.project_path === null || typeof candidate.project_path === "string") && (candidate.surface_condition === null || typeof candidate.surface_condition === "string") && typeof candidate.created_at === "number" && typeof candidate.updated_at === "number" && (candidate.last_checked_at === null || typeof candidate.last_checked_at === "number") && (candidate.ready_at === null || typeof candidate.ready_at === "number") && (candidate.ready_reason === null || typeof candidate.ready_reason === "string");
|
|
23964
24001
|
}
|
|
23965
|
-
function
|
|
24002
|
+
function toNote(row) {
|
|
23966
24003
|
return {
|
|
23967
24004
|
id: row.id,
|
|
23968
|
-
|
|
23969
|
-
content: row.content,
|
|
23970
|
-
surfaceCondition: row.surface_condition,
|
|
24005
|
+
type: row.type,
|
|
23971
24006
|
status: row.status,
|
|
23972
|
-
|
|
24007
|
+
content: row.content,
|
|
24008
|
+
sessionId: toNullableString(row.session_id),
|
|
24009
|
+
projectPath: toNullableString(row.project_path),
|
|
24010
|
+
surfaceCondition: toNullableString(row.surface_condition),
|
|
23973
24011
|
createdAt: row.created_at,
|
|
23974
24012
|
updatedAt: row.updated_at,
|
|
23975
|
-
lastCheckedAt: row.last_checked_at,
|
|
23976
|
-
readyAt: row.ready_at,
|
|
23977
|
-
readyReason: row.ready_reason
|
|
24013
|
+
lastCheckedAt: toNullableNumber(row.last_checked_at),
|
|
24014
|
+
readyAt: toNullableNumber(row.ready_at),
|
|
24015
|
+
readyReason: toNullableString(row.ready_reason)
|
|
24016
|
+
};
|
|
24017
|
+
}
|
|
24018
|
+
function getNoteById(db, noteId) {
|
|
24019
|
+
const row = db.prepare("SELECT * FROM notes WHERE id = ?").get(noteId);
|
|
24020
|
+
return isNoteRow(row) ? toNote(row) : null;
|
|
24021
|
+
}
|
|
24022
|
+
function buildStatusClause(status) {
|
|
24023
|
+
if (status === undefined) {
|
|
24024
|
+
return null;
|
|
24025
|
+
}
|
|
24026
|
+
const statuses = Array.isArray(status) ? status : [status];
|
|
24027
|
+
if (statuses.length === 0) {
|
|
24028
|
+
return null;
|
|
24029
|
+
}
|
|
24030
|
+
const placeholders = statuses.map(() => "?").join(", ");
|
|
24031
|
+
return {
|
|
24032
|
+
sql: `status IN (${placeholders})`,
|
|
24033
|
+
params: statuses
|
|
23978
24034
|
};
|
|
23979
24035
|
}
|
|
23980
|
-
function
|
|
24036
|
+
function getNotes(db, options = {}) {
|
|
24037
|
+
const clauses = [];
|
|
24038
|
+
const params = [];
|
|
24039
|
+
if (options.sessionId !== undefined) {
|
|
24040
|
+
clauses.push("session_id = ?");
|
|
24041
|
+
params.push(options.sessionId);
|
|
24042
|
+
}
|
|
24043
|
+
if (options.projectPath !== undefined) {
|
|
24044
|
+
clauses.push("project_path = ?");
|
|
24045
|
+
params.push(options.projectPath);
|
|
24046
|
+
}
|
|
24047
|
+
if (options.type !== undefined) {
|
|
24048
|
+
clauses.push("type = ?");
|
|
24049
|
+
params.push(options.type);
|
|
24050
|
+
}
|
|
24051
|
+
const statusClause = buildStatusClause(options.status);
|
|
24052
|
+
if (statusClause) {
|
|
24053
|
+
clauses.push(statusClause.sql);
|
|
24054
|
+
params.push(...statusClause.params);
|
|
24055
|
+
}
|
|
24056
|
+
const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
24057
|
+
return db.prepare(`SELECT * FROM notes${where} ORDER BY created_at ASC, id ASC`).all(...params).filter(isNoteRow).map(toNote);
|
|
24058
|
+
}
|
|
24059
|
+
function addNote(db, type, options) {
|
|
23981
24060
|
const now = Date.now();
|
|
23982
|
-
const result = db.prepare("INSERT INTO
|
|
23983
|
-
if (!
|
|
23984
|
-
throw new Error("[
|
|
24061
|
+
const result = type === "session" ? db.prepare("INSERT INTO notes (type, status, content, session_id, created_at, updated_at) VALUES ('session', 'active', ?, ?, ?, ?) RETURNING *").get(options.content, options.sessionId, now, now) : db.prepare("INSERT INTO notes (type, status, content, session_id, project_path, surface_condition, created_at, updated_at) VALUES ('smart', 'pending', ?, ?, ?, ?, ?, ?) RETURNING *").get(options.content, options.sessionId ?? null, options.projectPath, options.surfaceCondition, now, now);
|
|
24062
|
+
if (!isNoteRow(result)) {
|
|
24063
|
+
throw new Error("[notes] failed to insert note");
|
|
23985
24064
|
}
|
|
23986
|
-
return
|
|
24065
|
+
return toNote(result);
|
|
24066
|
+
}
|
|
24067
|
+
function getSessionNotes(db, sessionId) {
|
|
24068
|
+
return getNotes(db, { sessionId, type: "session", status: "active" });
|
|
23987
24069
|
}
|
|
23988
24070
|
function getSmartNotes(db, projectPath, status) {
|
|
23989
|
-
|
|
23990
|
-
|
|
23991
|
-
|
|
24071
|
+
return getNotes(db, {
|
|
24072
|
+
projectPath,
|
|
24073
|
+
type: "smart",
|
|
24074
|
+
status: status ?? DEFAULT_SMART_STATUSES
|
|
24075
|
+
});
|
|
23992
24076
|
}
|
|
23993
24077
|
function getPendingSmartNotes(db, projectPath) {
|
|
23994
24078
|
return getSmartNotes(db, projectPath, "pending");
|
|
@@ -23996,17 +24080,294 @@ function getPendingSmartNotes(db, projectPath) {
|
|
|
23996
24080
|
function getReadySmartNotes(db, projectPath) {
|
|
23997
24081
|
return getSmartNotes(db, projectPath, "ready");
|
|
23998
24082
|
}
|
|
23999
|
-
function
|
|
24083
|
+
function updateNote(db, noteId, updates) {
|
|
24084
|
+
const existing = getNoteById(db, noteId);
|
|
24085
|
+
if (!existing) {
|
|
24086
|
+
return null;
|
|
24087
|
+
}
|
|
24000
24088
|
const now = Date.now();
|
|
24001
|
-
|
|
24089
|
+
const sets = ["updated_at = ?"];
|
|
24090
|
+
const params = [now];
|
|
24091
|
+
if (updates.content !== undefined) {
|
|
24092
|
+
sets.push("content = ?");
|
|
24093
|
+
params.push(updates.content);
|
|
24094
|
+
}
|
|
24095
|
+
if (updates.sessionId !== undefined) {
|
|
24096
|
+
sets.push("session_id = ?");
|
|
24097
|
+
params.push(updates.sessionId);
|
|
24098
|
+
}
|
|
24099
|
+
if (updates.status !== undefined) {
|
|
24100
|
+
sets.push("status = ?");
|
|
24101
|
+
params.push(updates.status);
|
|
24102
|
+
}
|
|
24103
|
+
if (existing.type === "smart") {
|
|
24104
|
+
if (updates.projectPath !== undefined) {
|
|
24105
|
+
sets.push("project_path = ?");
|
|
24106
|
+
params.push(updates.projectPath);
|
|
24107
|
+
}
|
|
24108
|
+
if (updates.surfaceCondition !== undefined) {
|
|
24109
|
+
sets.push("surface_condition = ?");
|
|
24110
|
+
params.push(updates.surfaceCondition);
|
|
24111
|
+
}
|
|
24112
|
+
if (updates.lastCheckedAt !== undefined) {
|
|
24113
|
+
sets.push("last_checked_at = ?");
|
|
24114
|
+
params.push(updates.lastCheckedAt);
|
|
24115
|
+
}
|
|
24116
|
+
if (updates.readyAt !== undefined) {
|
|
24117
|
+
sets.push("ready_at = ?");
|
|
24118
|
+
params.push(updates.readyAt);
|
|
24119
|
+
}
|
|
24120
|
+
if (updates.readyReason !== undefined) {
|
|
24121
|
+
sets.push("ready_reason = ?");
|
|
24122
|
+
params.push(updates.readyReason);
|
|
24123
|
+
}
|
|
24124
|
+
}
|
|
24125
|
+
if (sets.length === 1) {
|
|
24126
|
+
return null;
|
|
24127
|
+
}
|
|
24128
|
+
params.push(noteId);
|
|
24129
|
+
const result = db.prepare(`UPDATE notes SET ${sets.join(", ")} WHERE id = ? RETURNING *`).get(...params);
|
|
24130
|
+
return isNoteRow(result) ? toNote(result) : null;
|
|
24002
24131
|
}
|
|
24003
|
-
function
|
|
24132
|
+
function dismissNote(db, noteId) {
|
|
24133
|
+
const result = db.prepare("UPDATE notes SET status = 'dismissed', updated_at = ? WHERE id = ? AND status != 'dismissed'").run(Date.now(), noteId);
|
|
24134
|
+
return result.changes > 0;
|
|
24135
|
+
}
|
|
24136
|
+
function markNoteReady(db, noteId, reason) {
|
|
24004
24137
|
const now = Date.now();
|
|
24005
|
-
db.prepare("UPDATE
|
|
24138
|
+
db.prepare("UPDATE notes SET status = 'ready', ready_at = ?, ready_reason = ?, updated_at = ?, last_checked_at = ? WHERE id = ? AND type = 'smart'").run(now, reason ?? null, now, now, noteId);
|
|
24006
24139
|
}
|
|
24007
|
-
function
|
|
24008
|
-
const
|
|
24009
|
-
|
|
24140
|
+
function markNoteChecked(db, noteId) {
|
|
24141
|
+
const now = Date.now();
|
|
24142
|
+
db.prepare("UPDATE notes SET last_checked_at = ?, updated_at = ? WHERE id = ? AND type = 'smart'").run(now, now, noteId);
|
|
24143
|
+
}
|
|
24144
|
+
|
|
24145
|
+
// src/features/magic-context/user-memory/review-user-memories.ts
|
|
24146
|
+
init_logger();
|
|
24147
|
+
|
|
24148
|
+
// src/features/magic-context/user-memory/storage-user-memory.ts
|
|
24149
|
+
function insertUserMemoryCandidates(db, candidates) {
|
|
24150
|
+
if (candidates.length === 0)
|
|
24151
|
+
return;
|
|
24152
|
+
const now = Date.now();
|
|
24153
|
+
const stmt = db.prepare("INSERT INTO user_memory_candidates (content, session_id, source_compartment_start, source_compartment_end, created_at) VALUES (?, ?, ?, ?, ?)");
|
|
24154
|
+
db.transaction(() => {
|
|
24155
|
+
for (const c of candidates) {
|
|
24156
|
+
stmt.run(c.content, c.sessionId, c.sourceCompartmentStart ?? null, c.sourceCompartmentEnd ?? null, now);
|
|
24157
|
+
}
|
|
24158
|
+
})();
|
|
24159
|
+
}
|
|
24160
|
+
function getUserMemoryCandidates(db) {
|
|
24161
|
+
const rows = db.prepare("SELECT id, content, session_id, source_compartment_start, source_compartment_end, created_at FROM user_memory_candidates ORDER BY created_at ASC").all();
|
|
24162
|
+
return rows.map((r) => ({
|
|
24163
|
+
id: r.id,
|
|
24164
|
+
content: r.content,
|
|
24165
|
+
sessionId: r.session_id,
|
|
24166
|
+
sourceCompartmentStart: r.source_compartment_start,
|
|
24167
|
+
sourceCompartmentEnd: r.source_compartment_end,
|
|
24168
|
+
createdAt: r.created_at
|
|
24169
|
+
}));
|
|
24170
|
+
}
|
|
24171
|
+
function deleteUserMemoryCandidates(db, ids) {
|
|
24172
|
+
if (ids.length === 0)
|
|
24173
|
+
return;
|
|
24174
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
24175
|
+
db.prepare(`DELETE FROM user_memory_candidates WHERE id IN (${placeholders})`).run(...ids);
|
|
24176
|
+
}
|
|
24177
|
+
function insertUserMemory(db, content, sourceCandidateIds) {
|
|
24178
|
+
const now = Date.now();
|
|
24179
|
+
const result = db.prepare("INSERT INTO user_memories (content, status, promoted_at, source_candidate_ids, created_at, updated_at) VALUES (?, 'active', ?, ?, ?, ?)").run(content, now, JSON.stringify(sourceCandidateIds), now, now);
|
|
24180
|
+
return Number(result.lastInsertRowid);
|
|
24181
|
+
}
|
|
24182
|
+
function getActiveUserMemories(db) {
|
|
24183
|
+
const rows = db.prepare("SELECT id, content, status, promoted_at, source_candidate_ids, created_at, updated_at FROM user_memories WHERE status = 'active' ORDER BY promoted_at ASC").all();
|
|
24184
|
+
return rows.map(parseUserMemoryRow);
|
|
24185
|
+
}
|
|
24186
|
+
function updateUserMemoryContent(db, id, content) {
|
|
24187
|
+
db.prepare("UPDATE user_memories SET content = ?, updated_at = ? WHERE id = ?").run(content, Date.now(), id);
|
|
24188
|
+
}
|
|
24189
|
+
function dismissUserMemory(db, id) {
|
|
24190
|
+
db.prepare("UPDATE user_memories SET status = 'dismissed', updated_at = ? WHERE id = ?").run(Date.now(), id);
|
|
24191
|
+
}
|
|
24192
|
+
function parseUserMemoryRow(row) {
|
|
24193
|
+
let candidateIds = [];
|
|
24194
|
+
try {
|
|
24195
|
+
candidateIds = JSON.parse(row.source_candidate_ids);
|
|
24196
|
+
} catch {}
|
|
24197
|
+
return {
|
|
24198
|
+
id: row.id,
|
|
24199
|
+
content: row.content,
|
|
24200
|
+
status: row.status === "dismissed" ? "dismissed" : "active",
|
|
24201
|
+
promotedAt: row.promoted_at,
|
|
24202
|
+
sourceCandidateIds: candidateIds,
|
|
24203
|
+
createdAt: row.created_at,
|
|
24204
|
+
updatedAt: row.updated_at
|
|
24205
|
+
};
|
|
24206
|
+
}
|
|
24207
|
+
|
|
24208
|
+
// src/features/magic-context/user-memory/review-user-memories.ts
|
|
24209
|
+
async function reviewUserMemories(args) {
|
|
24210
|
+
const result = { promoted: 0, merged: 0, dismissed: 0, candidatesConsumed: 0 };
|
|
24211
|
+
const candidates = getUserMemoryCandidates(args.db);
|
|
24212
|
+
if (candidates.length < args.promotionThreshold) {
|
|
24213
|
+
log(`[dreamer] user-memories: ${candidates.length} candidate(s), need ${args.promotionThreshold} \u2014 skipping`);
|
|
24214
|
+
return result;
|
|
24215
|
+
}
|
|
24216
|
+
const stableMemories = getActiveUserMemories(args.db);
|
|
24217
|
+
log(`[dreamer] user-memories: reviewing ${candidates.length} candidate(s) against ${stableMemories.length} stable memorie(s)`);
|
|
24218
|
+
const candidateList = candidates.map((c) => `- Candidate #${c.id} [session ${c.sessionId.slice(0, 12)}]: "${c.content}"`).join(`
|
|
24219
|
+
`);
|
|
24220
|
+
const stableList = stableMemories.length > 0 ? stableMemories.map((m) => `- Memory #${m.id}: "${m.content}"`).join(`
|
|
24221
|
+
`) : "(none)";
|
|
24222
|
+
const prompt = `## Task: Review User Memory Candidates
|
|
24223
|
+
|
|
24224
|
+
You are reviewing behavioral observations about a human user to decide which patterns are real and persistent.
|
|
24225
|
+
|
|
24226
|
+
### Current Stable User Memories
|
|
24227
|
+
${stableList}
|
|
24228
|
+
|
|
24229
|
+
### Candidate Observations (from recent historian runs)
|
|
24230
|
+
${candidateList}
|
|
24231
|
+
|
|
24232
|
+
### Instructions
|
|
24233
|
+
|
|
24234
|
+
1. Look for **recurring patterns** across multiple candidates \u2014 observations that appear independently from different sessions or historian runs indicate a real user trait.
|
|
24235
|
+
2. A candidate must appear in at least ${args.promotionThreshold} semantically similar variants before promotion.
|
|
24236
|
+
3. Only promote **truly universal** user traits \u2014 communication style, expertise level, review focus, decision-making patterns, working habits.
|
|
24237
|
+
4. Do NOT promote: project-specific preferences, framework choices, one-off moods, task-local frustrations.
|
|
24238
|
+
5. If a candidate is semantically equivalent to an existing stable memory, mark it as already covered.
|
|
24239
|
+
6. If multiple candidates describe the same trait, merge them into one clean statement.
|
|
24240
|
+
7. If an existing stable memory should be updated based on new evidence, include the update.
|
|
24241
|
+
|
|
24242
|
+
### Output Format
|
|
24243
|
+
|
|
24244
|
+
Return valid JSON (no markdown fencing):
|
|
24245
|
+
|
|
24246
|
+
{
|
|
24247
|
+
"promote": [
|
|
24248
|
+
{ "content": "Clean universal observation text", "candidate_ids": [1, 3, 7] }
|
|
24249
|
+
],
|
|
24250
|
+
"update_existing": [
|
|
24251
|
+
{ "memory_id": 5, "content": "Updated text incorporating new evidence", "candidate_ids": [2] }
|
|
24252
|
+
],
|
|
24253
|
+
"dismiss_existing": [
|
|
24254
|
+
{ "memory_id": 3, "reason": "No longer supported by recent observations" }
|
|
24255
|
+
],
|
|
24256
|
+
"consume_candidate_ids": [1, 2, 3, 4, 5, 7, 8]
|
|
24257
|
+
}
|
|
24258
|
+
|
|
24259
|
+
- \`promote\`: new stable memories to create from candidates
|
|
24260
|
+
- \`update_existing\`: existing stable memories to rewrite with new evidence
|
|
24261
|
+
- \`dismiss_existing\`: existing stable memories that are no longer valid
|
|
24262
|
+
- \`consume_candidate_ids\`: ALL candidate IDs that were reviewed (promoted, merged, or rejected) \u2014 they will be deleted from the candidate pool
|
|
24263
|
+
|
|
24264
|
+
If no promotions are warranted, return empty arrays. Always consume reviewed candidates so they don't accumulate indefinitely.`;
|
|
24265
|
+
let agentSessionId = null;
|
|
24266
|
+
const abortController = new AbortController;
|
|
24267
|
+
const leaseInterval = setInterval(() => {
|
|
24268
|
+
try {
|
|
24269
|
+
if (!renewLease(args.db, args.holderId)) {
|
|
24270
|
+
log("[dreamer] user-memories: lease renewal failed \u2014 aborting");
|
|
24271
|
+
abortController.abort();
|
|
24272
|
+
}
|
|
24273
|
+
} catch {
|
|
24274
|
+
abortController.abort();
|
|
24275
|
+
}
|
|
24276
|
+
}, 60000);
|
|
24277
|
+
try {
|
|
24278
|
+
const createResponse = await args.client.session.create({
|
|
24279
|
+
body: {
|
|
24280
|
+
...args.parentSessionId ? { parentID: args.parentSessionId } : {},
|
|
24281
|
+
title: "magic-context-dream-user-memories"
|
|
24282
|
+
},
|
|
24283
|
+
query: { directory: args.sessionDirectory }
|
|
24284
|
+
});
|
|
24285
|
+
const created = normalizeSDKResponse(createResponse, null, { preferResponseOnMissingData: true });
|
|
24286
|
+
agentSessionId = typeof created?.id === "string" ? created.id : null;
|
|
24287
|
+
if (!agentSessionId)
|
|
24288
|
+
throw new Error("Could not create user memory review session.");
|
|
24289
|
+
log(`[dreamer] user-memories: child session created ${agentSessionId}`);
|
|
24290
|
+
const remainingMs = Math.max(0, args.deadline - Date.now());
|
|
24291
|
+
await promptSyncWithModelSuggestionRetry(args.client, {
|
|
24292
|
+
path: { id: agentSessionId },
|
|
24293
|
+
query: { directory: args.sessionDirectory },
|
|
24294
|
+
body: {
|
|
24295
|
+
agent: DREAMER_AGENT,
|
|
24296
|
+
system: DREAMER_SYSTEM_PROMPT,
|
|
24297
|
+
parts: [{ type: "text", text: prompt }]
|
|
24298
|
+
}
|
|
24299
|
+
}, { timeoutMs: Math.min(remainingMs, 5 * 60 * 1000), signal: abortController.signal });
|
|
24300
|
+
const messagesResponse = await args.client.session.messages({
|
|
24301
|
+
path: { id: agentSessionId },
|
|
24302
|
+
query: { directory: args.sessionDirectory }
|
|
24303
|
+
});
|
|
24304
|
+
const messages = normalizeSDKResponse(messagesResponse, [], {
|
|
24305
|
+
preferResponseOnMissingData: true
|
|
24306
|
+
});
|
|
24307
|
+
const responseText = extractLatestAssistantText(messages);
|
|
24308
|
+
if (!responseText) {
|
|
24309
|
+
log("[dreamer] user-memories: no response from review agent");
|
|
24310
|
+
return result;
|
|
24311
|
+
}
|
|
24312
|
+
const jsonMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)```/) ?? responseText.match(/(\{[\s\S]*\})/);
|
|
24313
|
+
if (!jsonMatch) {
|
|
24314
|
+
log("[dreamer] user-memories: could not parse JSON from response");
|
|
24315
|
+
return result;
|
|
24316
|
+
}
|
|
24317
|
+
let parsed;
|
|
24318
|
+
try {
|
|
24319
|
+
parsed = JSON.parse(jsonMatch[1]);
|
|
24320
|
+
} catch {
|
|
24321
|
+
log("[dreamer] user-memories: JSON parse failed");
|
|
24322
|
+
return result;
|
|
24323
|
+
}
|
|
24324
|
+
if (parsed.promote) {
|
|
24325
|
+
for (const p of parsed.promote) {
|
|
24326
|
+
if (p.content?.trim()) {
|
|
24327
|
+
insertUserMemory(args.db, p.content.trim(), p.candidate_ids ?? []);
|
|
24328
|
+
result.promoted++;
|
|
24329
|
+
log(`[dreamer] user-memories: promoted "${p.content.trim().slice(0, 60)}..."`);
|
|
24330
|
+
}
|
|
24331
|
+
}
|
|
24332
|
+
}
|
|
24333
|
+
if (parsed.update_existing) {
|
|
24334
|
+
for (const u of parsed.update_existing) {
|
|
24335
|
+
if (u.memory_id && u.content?.trim()) {
|
|
24336
|
+
updateUserMemoryContent(args.db, u.memory_id, u.content.trim());
|
|
24337
|
+
result.merged++;
|
|
24338
|
+
log(`[dreamer] user-memories: updated memory #${u.memory_id}`);
|
|
24339
|
+
}
|
|
24340
|
+
}
|
|
24341
|
+
}
|
|
24342
|
+
if (parsed.dismiss_existing) {
|
|
24343
|
+
for (const d of parsed.dismiss_existing) {
|
|
24344
|
+
if (d.memory_id) {
|
|
24345
|
+
dismissUserMemory(args.db, d.memory_id);
|
|
24346
|
+
result.dismissed++;
|
|
24347
|
+
log(`[dreamer] user-memories: dismissed memory #${d.memory_id} \u2014 ${d.reason ?? "no reason"}`);
|
|
24348
|
+
}
|
|
24349
|
+
}
|
|
24350
|
+
}
|
|
24351
|
+
if (parsed.consume_candidate_ids && parsed.consume_candidate_ids.length > 0) {
|
|
24352
|
+
deleteUserMemoryCandidates(args.db, parsed.consume_candidate_ids);
|
|
24353
|
+
result.candidatesConsumed = parsed.consume_candidate_ids.length;
|
|
24354
|
+
log(`[dreamer] user-memories: consumed ${result.candidatesConsumed} candidate(s)`);
|
|
24355
|
+
}
|
|
24356
|
+
return result;
|
|
24357
|
+
} catch (error48) {
|
|
24358
|
+
log(`[dreamer] user-memories: review failed: ${getErrorMessage(error48)}`);
|
|
24359
|
+
return result;
|
|
24360
|
+
} finally {
|
|
24361
|
+
clearInterval(leaseInterval);
|
|
24362
|
+
if (agentSessionId) {
|
|
24363
|
+
await args.client.session.delete({
|
|
24364
|
+
path: { id: agentSessionId },
|
|
24365
|
+
query: { directory: args.sessionDirectory }
|
|
24366
|
+
}).catch((e) => {
|
|
24367
|
+
log(`[dreamer] user-memories: session cleanup failed: ${getErrorMessage(e)}`);
|
|
24368
|
+
});
|
|
24369
|
+
}
|
|
24370
|
+
}
|
|
24010
24371
|
}
|
|
24011
24372
|
|
|
24012
24373
|
// src/features/magic-context/dreamer/storage-dream-runs.ts
|
|
@@ -24186,6 +24547,24 @@ async function runDream(args) {
|
|
|
24186
24547
|
}
|
|
24187
24548
|
}
|
|
24188
24549
|
}
|
|
24550
|
+
if (args.experimentalUserMemories?.enabled && Date.now() <= deadline) {
|
|
24551
|
+
try {
|
|
24552
|
+
const reviewResult = await reviewUserMemories({
|
|
24553
|
+
db: args.db,
|
|
24554
|
+
client: args.client,
|
|
24555
|
+
parentSessionId,
|
|
24556
|
+
sessionDirectory: args.sessionDirectory,
|
|
24557
|
+
holderId,
|
|
24558
|
+
deadline,
|
|
24559
|
+
promotionThreshold: args.experimentalUserMemories.promotionThreshold
|
|
24560
|
+
});
|
|
24561
|
+
if (reviewResult.promoted > 0 || reviewResult.merged > 0 || reviewResult.dismissed > 0) {
|
|
24562
|
+
log(`[dreamer] user-memories: promoted=${reviewResult.promoted} merged=${reviewResult.merged} dismissed=${reviewResult.dismissed} consumed=${reviewResult.candidatesConsumed}`);
|
|
24563
|
+
}
|
|
24564
|
+
} catch (error48) {
|
|
24565
|
+
log(`[dreamer] user-memory review failed: ${getErrorMessage(error48)}`);
|
|
24566
|
+
}
|
|
24567
|
+
}
|
|
24189
24568
|
if (Date.now() <= deadline) {
|
|
24190
24569
|
try {
|
|
24191
24570
|
await evaluateSmartNotes({
|
|
@@ -24326,7 +24705,7 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
|
|
|
24326
24705
|
if (!jsonMatch) {
|
|
24327
24706
|
log("[dreamer] smart notes: no JSON array found in output, skipping");
|
|
24328
24707
|
for (const note of pendingNotes)
|
|
24329
|
-
|
|
24708
|
+
markNoteChecked(args.db, note.id);
|
|
24330
24709
|
throw new Error("Smart note evaluation returned no JSON array.");
|
|
24331
24710
|
}
|
|
24332
24711
|
let evaluations;
|
|
@@ -24335,7 +24714,7 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
|
|
|
24335
24714
|
} catch {
|
|
24336
24715
|
log(`[dreamer] smart notes: failed to parse JSON from LLM output, marking all checked`);
|
|
24337
24716
|
for (const note of pendingNotes)
|
|
24338
|
-
|
|
24717
|
+
markNoteChecked(args.db, note.id);
|
|
24339
24718
|
throw new Error("Smart note evaluation returned invalid JSON.");
|
|
24340
24719
|
}
|
|
24341
24720
|
let surfaced = 0;
|
|
@@ -24346,16 +24725,16 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
|
|
|
24346
24725
|
if (!note)
|
|
24347
24726
|
continue;
|
|
24348
24727
|
if (evaluation.met) {
|
|
24349
|
-
|
|
24728
|
+
markNoteReady(args.db, note.id, evaluation.reason);
|
|
24350
24729
|
surfaced++;
|
|
24351
24730
|
log(`[dreamer] smart notes: #${note.id} condition MET \u2014 "${evaluation.reason ?? "condition satisfied"}"`);
|
|
24352
24731
|
} else {
|
|
24353
|
-
|
|
24732
|
+
markNoteChecked(args.db, note.id);
|
|
24354
24733
|
}
|
|
24355
24734
|
}
|
|
24356
24735
|
for (const note of pendingNotes) {
|
|
24357
24736
|
if (!evaluations.some((e) => e.id === note.id)) {
|
|
24358
|
-
|
|
24737
|
+
markNoteChecked(args.db, note.id);
|
|
24359
24738
|
}
|
|
24360
24739
|
}
|
|
24361
24740
|
const durationMs = Date.now() - taskStartedAt;
|
|
@@ -24409,7 +24788,8 @@ async function processDreamQueue(args) {
|
|
|
24409
24788
|
tasks: args.tasks,
|
|
24410
24789
|
taskTimeoutMinutes: args.taskTimeoutMinutes,
|
|
24411
24790
|
maxRuntimeMinutes: args.maxRuntimeMinutes,
|
|
24412
|
-
sessionDirectory: projectDirectory
|
|
24791
|
+
sessionDirectory: projectDirectory,
|
|
24792
|
+
experimentalUserMemories: args.experimentalUserMemories
|
|
24413
24793
|
});
|
|
24414
24794
|
} catch (error48) {
|
|
24415
24795
|
log(`[dreamer] runDream threw for ${entry.projectIdentity}: ${getErrorMessage(error48)}`);
|
|
@@ -24461,7 +24841,8 @@ function isInScheduleWindow(schedule, now = new Date) {
|
|
|
24461
24841
|
function findProjectsNeedingDream(db) {
|
|
24462
24842
|
const projectRows = db.query(`SELECT DISTINCT project_path FROM memories WHERE status = 'active'
|
|
24463
24843
|
UNION
|
|
24464
|
-
SELECT DISTINCT project_path FROM
|
|
24844
|
+
SELECT DISTINCT project_path FROM notes
|
|
24845
|
+
WHERE type = 'smart' AND status = 'pending' AND project_path IS NOT NULL
|
|
24465
24846
|
ORDER BY project_path`).all();
|
|
24466
24847
|
const projects = [];
|
|
24467
24848
|
for (const row of projectRows) {
|
|
@@ -24470,8 +24851,8 @@ function findProjectsNeedingDream(db) {
|
|
|
24470
24851
|
const lastDreamAt = Number(lastDreamAtStr ?? fallbackStr ?? "0") || 0;
|
|
24471
24852
|
const updatedMemories = db.query(`SELECT COUNT(*) as cnt FROM memories
|
|
24472
24853
|
WHERE project_path = ? AND status = 'active' AND updated_at > ?`).get(row.project_path, lastDreamAt);
|
|
24473
|
-
const pendingSmartNotes = db.query(`SELECT COUNT(*) as cnt FROM
|
|
24474
|
-
WHERE project_path = ? AND status = 'pending'`).get(row.project_path);
|
|
24854
|
+
const pendingSmartNotes = db.query(`SELECT COUNT(*) as cnt FROM notes
|
|
24855
|
+
WHERE project_path = ? AND type = 'smart' AND status = 'pending'`).get(row.project_path);
|
|
24475
24856
|
if (updatedMemories && updatedMemories.cnt > 0 || pendingSmartNotes && pendingSmartNotes.cnt > 0) {
|
|
24476
24857
|
projects.push(row.project_path);
|
|
24477
24858
|
}
|
|
@@ -24518,6 +24899,22 @@ function cosineSimilarity(a, b) {
|
|
|
24518
24899
|
|
|
24519
24900
|
// src/features/magic-context/memory/embedding-local.ts
|
|
24520
24901
|
init_logger();
|
|
24902
|
+
async function withQuietConsole(fn) {
|
|
24903
|
+
const origWarn = console.warn;
|
|
24904
|
+
const origError = console.error;
|
|
24905
|
+
const redirect = (...args) => {
|
|
24906
|
+
const message = args.map((a) => typeof a === "string" ? a : String(a)).join(" ");
|
|
24907
|
+
log(`[transformers] ${message}`);
|
|
24908
|
+
};
|
|
24909
|
+
console.warn = redirect;
|
|
24910
|
+
console.error = redirect;
|
|
24911
|
+
try {
|
|
24912
|
+
return await fn();
|
|
24913
|
+
} finally {
|
|
24914
|
+
console.warn = origWarn;
|
|
24915
|
+
console.error = origError;
|
|
24916
|
+
}
|
|
24917
|
+
}
|
|
24521
24918
|
function isArrayLikeNumber(value) {
|
|
24522
24919
|
if (typeof value !== "object" || value === null || !("length" in value)) {
|
|
24523
24920
|
return false;
|
|
@@ -24573,10 +24970,16 @@ class LocalEmbeddingProvider {
|
|
|
24573
24970
|
this.initPromise = (async () => {
|
|
24574
24971
|
try {
|
|
24575
24972
|
const transformersModule = await import("@huggingface/transformers");
|
|
24973
|
+
const env = transformersModule.env;
|
|
24974
|
+
const LogLevel = transformersModule.LogLevel;
|
|
24975
|
+
if (LogLevel && "ERROR" in LogLevel) {
|
|
24976
|
+
env.logLevel = LogLevel.ERROR;
|
|
24977
|
+
}
|
|
24576
24978
|
const createPipeline = transformersModule.pipeline;
|
|
24577
|
-
this.pipeline = await createPipeline("feature-extraction", this.model, {
|
|
24578
|
-
quantized: true
|
|
24579
|
-
|
|
24979
|
+
this.pipeline = await withQuietConsole(() => createPipeline("feature-extraction", this.model, {
|
|
24980
|
+
quantized: true,
|
|
24981
|
+
dtype: "fp32"
|
|
24982
|
+
}));
|
|
24580
24983
|
log(`[magic-context] embedding model loaded: ${this.model}`);
|
|
24581
24984
|
} catch (error48) {
|
|
24582
24985
|
log("[magic-context] embedding model failed to load:", error48);
|
|
@@ -24593,13 +24996,14 @@ class LocalEmbeddingProvider {
|
|
|
24593
24996
|
return null;
|
|
24594
24997
|
}
|
|
24595
24998
|
try {
|
|
24596
|
-
|
|
24999
|
+
const pipeline = this.pipeline;
|
|
25000
|
+
if (!pipeline) {
|
|
24597
25001
|
return null;
|
|
24598
25002
|
}
|
|
24599
|
-
const result = await
|
|
25003
|
+
const result = await withQuietConsole(() => pipeline(text, {
|
|
24600
25004
|
pooling: "mean",
|
|
24601
25005
|
normalize: true
|
|
24602
|
-
});
|
|
25006
|
+
}));
|
|
24603
25007
|
return extractBatchEmbeddings(result, 1)[0] ?? null;
|
|
24604
25008
|
} catch (error48) {
|
|
24605
25009
|
log("[magic-context] embedding failed:", error48);
|
|
@@ -24614,13 +25018,14 @@ class LocalEmbeddingProvider {
|
|
|
24614
25018
|
return Array.from({ length: texts.length }, () => null);
|
|
24615
25019
|
}
|
|
24616
25020
|
try {
|
|
24617
|
-
|
|
25021
|
+
const pipeline = this.pipeline;
|
|
25022
|
+
if (!pipeline) {
|
|
24618
25023
|
return Array.from({ length: texts.length }, () => null);
|
|
24619
25024
|
}
|
|
24620
|
-
const result = await
|
|
25025
|
+
const result = await withQuietConsole(() => pipeline(texts, {
|
|
24621
25026
|
pooling: "mean",
|
|
24622
25027
|
normalize: true
|
|
24623
|
-
});
|
|
25028
|
+
}));
|
|
24624
25029
|
return extractBatchEmbeddings(result, texts.length);
|
|
24625
25030
|
} catch (error48) {
|
|
24626
25031
|
log("[magic-context] embedding batch failed:", error48);
|
|
@@ -25220,7 +25625,11 @@ function readRawSessionMessagesFromDb(db, sessionId) {
|
|
|
25220
25625
|
list.push(parseJsonUnknown(part.data));
|
|
25221
25626
|
partsByMessageId.set(part.message_id, list);
|
|
25222
25627
|
}
|
|
25223
|
-
|
|
25628
|
+
const filtered = messageRows.filter((row) => {
|
|
25629
|
+
const info = parseJsonRecord(row.data);
|
|
25630
|
+
return !(info?.summary === true && info?.finish === "stop");
|
|
25631
|
+
});
|
|
25632
|
+
return filtered.flatMap((row, index) => {
|
|
25224
25633
|
const info = parseJsonRecord(row.data);
|
|
25225
25634
|
if (!info)
|
|
25226
25635
|
return [];
|
|
@@ -25594,6 +26003,158 @@ import { Database as Database2 } from "bun:sqlite";
|
|
|
25594
26003
|
import { mkdirSync } from "fs";
|
|
25595
26004
|
import { join as join9 } from "path";
|
|
25596
26005
|
init_logger();
|
|
26006
|
+
|
|
26007
|
+
// src/features/magic-context/migrations.ts
|
|
26008
|
+
init_logger();
|
|
26009
|
+
var MIGRATIONS = [
|
|
26010
|
+
{
|
|
26011
|
+
version: 1,
|
|
26012
|
+
description: "Merge session_notes + smart_notes into unified notes table",
|
|
26013
|
+
up: (db) => {
|
|
26014
|
+
db.exec(`
|
|
26015
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
26016
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26017
|
+
type TEXT NOT NULL DEFAULT 'session',
|
|
26018
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
26019
|
+
content TEXT NOT NULL,
|
|
26020
|
+
session_id TEXT,
|
|
26021
|
+
project_path TEXT,
|
|
26022
|
+
surface_condition TEXT,
|
|
26023
|
+
created_at INTEGER NOT NULL,
|
|
26024
|
+
updated_at INTEGER NOT NULL,
|
|
26025
|
+
last_checked_at INTEGER,
|
|
26026
|
+
ready_at INTEGER,
|
|
26027
|
+
ready_reason TEXT
|
|
26028
|
+
);
|
|
26029
|
+
CREATE INDEX IF NOT EXISTS idx_notes_session_status ON notes(session_id, status);
|
|
26030
|
+
CREATE INDEX IF NOT EXISTS idx_notes_project_status ON notes(project_path, status);
|
|
26031
|
+
CREATE INDEX IF NOT EXISTS idx_notes_type_status ON notes(type, status);
|
|
26032
|
+
`);
|
|
26033
|
+
const hasSessionNotes = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='session_notes'").get();
|
|
26034
|
+
if (hasSessionNotes) {
|
|
26035
|
+
db.exec(`
|
|
26036
|
+
INSERT INTO notes (type, status, content, session_id, created_at, updated_at)
|
|
26037
|
+
SELECT 'session', 'active', content, session_id, created_at, created_at
|
|
26038
|
+
FROM session_notes
|
|
26039
|
+
`);
|
|
26040
|
+
}
|
|
26041
|
+
const hasSmartNotes = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='smart_notes'").get();
|
|
26042
|
+
if (hasSmartNotes) {
|
|
26043
|
+
db.exec(`
|
|
26044
|
+
INSERT INTO notes (type, status, content, session_id, project_path, surface_condition,
|
|
26045
|
+
created_at, updated_at, last_checked_at, ready_at, ready_reason)
|
|
26046
|
+
SELECT 'smart', status, content, created_session_id, project_path, surface_condition,
|
|
26047
|
+
created_at, updated_at, last_checked_at, ready_at, ready_reason
|
|
26048
|
+
FROM smart_notes
|
|
26049
|
+
`);
|
|
26050
|
+
}
|
|
26051
|
+
if (hasSessionNotes) {
|
|
26052
|
+
const sourceCount = db.prepare("SELECT COUNT(*) as c FROM session_notes").get().c;
|
|
26053
|
+
const migratedCount = db.prepare("SELECT COUNT(*) as c FROM notes WHERE type = 'session'").get().c;
|
|
26054
|
+
if (migratedCount >= sourceCount) {
|
|
26055
|
+
db.exec("DROP TABLE session_notes");
|
|
26056
|
+
} else {
|
|
26057
|
+
throw new Error(`session_notes migration verification failed: expected ${sourceCount} rows, got ${migratedCount}`);
|
|
26058
|
+
}
|
|
26059
|
+
}
|
|
26060
|
+
if (hasSmartNotes) {
|
|
26061
|
+
const sourceCount = db.prepare("SELECT COUNT(*) as c FROM smart_notes").get().c;
|
|
26062
|
+
const migratedCount = db.prepare("SELECT COUNT(*) as c FROM notes WHERE type = 'smart'").get().c;
|
|
26063
|
+
if (migratedCount >= sourceCount) {
|
|
26064
|
+
db.exec("DROP TABLE smart_notes");
|
|
26065
|
+
} else {
|
|
26066
|
+
throw new Error(`smart_notes migration verification failed: expected ${sourceCount} rows, got ${migratedCount}`);
|
|
26067
|
+
}
|
|
26068
|
+
}
|
|
26069
|
+
}
|
|
26070
|
+
},
|
|
26071
|
+
{
|
|
26072
|
+
version: 2,
|
|
26073
|
+
description: "Add plugin_messages table for TUI \u2194 server communication",
|
|
26074
|
+
up: (db) => {
|
|
26075
|
+
db.exec(`
|
|
26076
|
+
CREATE TABLE IF NOT EXISTS plugin_messages (
|
|
26077
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26078
|
+
direction TEXT NOT NULL,
|
|
26079
|
+
type TEXT NOT NULL,
|
|
26080
|
+
payload TEXT NOT NULL DEFAULT '{}',
|
|
26081
|
+
session_id TEXT,
|
|
26082
|
+
created_at INTEGER NOT NULL,
|
|
26083
|
+
consumed_at INTEGER
|
|
26084
|
+
);
|
|
26085
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_messages_direction_consumed
|
|
26086
|
+
ON plugin_messages(direction, consumed_at);
|
|
26087
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_messages_created
|
|
26088
|
+
ON plugin_messages(created_at);
|
|
26089
|
+
`);
|
|
26090
|
+
}
|
|
26091
|
+
},
|
|
26092
|
+
{
|
|
26093
|
+
version: 3,
|
|
26094
|
+
description: "Add user_memory_candidates and user_memories tables",
|
|
26095
|
+
up: (db) => {
|
|
26096
|
+
db.exec(`
|
|
26097
|
+
CREATE TABLE IF NOT EXISTS user_memory_candidates (
|
|
26098
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26099
|
+
content TEXT NOT NULL,
|
|
26100
|
+
session_id TEXT NOT NULL,
|
|
26101
|
+
source_compartment_start INTEGER,
|
|
26102
|
+
source_compartment_end INTEGER,
|
|
26103
|
+
created_at INTEGER NOT NULL
|
|
26104
|
+
);
|
|
26105
|
+
CREATE INDEX IF NOT EXISTS idx_umc_created ON user_memory_candidates(created_at);
|
|
26106
|
+
|
|
26107
|
+
CREATE TABLE IF NOT EXISTS user_memories (
|
|
26108
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26109
|
+
content TEXT NOT NULL,
|
|
26110
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
26111
|
+
promoted_at INTEGER NOT NULL,
|
|
26112
|
+
source_candidate_ids TEXT DEFAULT '[]',
|
|
26113
|
+
created_at INTEGER NOT NULL,
|
|
26114
|
+
updated_at INTEGER NOT NULL
|
|
26115
|
+
);
|
|
26116
|
+
CREATE INDEX IF NOT EXISTS idx_um_status ON user_memories(status);
|
|
26117
|
+
`);
|
|
26118
|
+
}
|
|
26119
|
+
}
|
|
26120
|
+
];
|
|
26121
|
+
function ensureMigrationsTable(db) {
|
|
26122
|
+
db.exec(`
|
|
26123
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
26124
|
+
version INTEGER PRIMARY KEY,
|
|
26125
|
+
description TEXT NOT NULL,
|
|
26126
|
+
applied_at INTEGER NOT NULL
|
|
26127
|
+
)
|
|
26128
|
+
`);
|
|
26129
|
+
}
|
|
26130
|
+
function getCurrentVersion(db) {
|
|
26131
|
+
const row = db.prepare("SELECT MAX(version) as version FROM schema_migrations").get();
|
|
26132
|
+
return row?.version ?? 0;
|
|
26133
|
+
}
|
|
26134
|
+
function runMigrations(db) {
|
|
26135
|
+
ensureMigrationsTable(db);
|
|
26136
|
+
const currentVersion = getCurrentVersion(db);
|
|
26137
|
+
const pendingMigrations = MIGRATIONS.filter((m) => m.version > currentVersion);
|
|
26138
|
+
if (pendingMigrations.length === 0) {
|
|
26139
|
+
return;
|
|
26140
|
+
}
|
|
26141
|
+
log(`[migrations] current schema version: ${currentVersion}, applying ${pendingMigrations.length} migration(s)`);
|
|
26142
|
+
for (const migration of pendingMigrations) {
|
|
26143
|
+
try {
|
|
26144
|
+
db.transaction(() => {
|
|
26145
|
+
migration.up(db);
|
|
26146
|
+
db.prepare("INSERT INTO schema_migrations (version, description, applied_at) VALUES (?, ?, ?)").run(migration.version, migration.description, Date.now());
|
|
26147
|
+
})();
|
|
26148
|
+
log(`[migrations] applied v${migration.version}: ${migration.description}`);
|
|
26149
|
+
} catch (error48) {
|
|
26150
|
+
log(`[migrations] FAILED v${migration.version}: ${migration.description} \u2014 ${error48 instanceof Error ? error48.message : String(error48)}`);
|
|
26151
|
+
throw new Error(`Migration v${migration.version} failed: ${error48 instanceof Error ? error48.message : String(error48)}. Database may need manual repair.`);
|
|
26152
|
+
}
|
|
26153
|
+
}
|
|
26154
|
+
log(`[migrations] schema version now: ${MIGRATIONS[MIGRATIONS.length - 1].version}`);
|
|
26155
|
+
}
|
|
26156
|
+
|
|
26157
|
+
// src/features/magic-context/storage-db.ts
|
|
25597
26158
|
var databases = new Map;
|
|
25598
26159
|
var FALLBACK_DATABASE_KEY = "__fallback__:memory:";
|
|
25599
26160
|
var persistenceByDatabase = new WeakMap;
|
|
@@ -25873,6 +26434,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
|
|
|
25873
26434
|
ensureColumn(db, "dream_queue", "retry_count", "INTEGER DEFAULT 0");
|
|
25874
26435
|
ensureColumn(db, "tags", "reasoning_byte_size", "INTEGER DEFAULT 0");
|
|
25875
26436
|
ensureColumn(db, "session_meta", "system_prompt_tokens", "INTEGER DEFAULT 0");
|
|
26437
|
+
ensureColumn(db, "session_meta", "compaction_marker_state", "TEXT DEFAULT ''");
|
|
25876
26438
|
}
|
|
25877
26439
|
function ensureColumn(db, table, column, definition) {
|
|
25878
26440
|
if (!/^[a-z_]+$/.test(table) || !/^[a-z_]+$/.test(column) || !/^[A-Z0-9_'(),\s]+$/i.test(definition)) {
|
|
@@ -25888,6 +26450,7 @@ function createFallbackDatabase() {
|
|
|
25888
26450
|
try {
|
|
25889
26451
|
const fallback = new Database2(":memory:");
|
|
25890
26452
|
initializeDatabase(fallback);
|
|
26453
|
+
runMigrations(fallback);
|
|
25891
26454
|
return fallback;
|
|
25892
26455
|
} catch (error48) {
|
|
25893
26456
|
throw new Error(`[magic-context] storage fatal: failed to initialize fallback database: ${getErrorMessage(error48)}`);
|
|
@@ -25906,6 +26469,7 @@ function openDatabase() {
|
|
|
25906
26469
|
mkdirSync(dbDir, { recursive: true });
|
|
25907
26470
|
const db = new Database2(dbPath);
|
|
25908
26471
|
initializeDatabase(db);
|
|
26472
|
+
runMigrations(db);
|
|
25909
26473
|
databases.set(dbPath, db);
|
|
25910
26474
|
persistenceByDatabase.set(db, true);
|
|
25911
26475
|
persistenceErrorByDatabase.delete(db);
|
|
@@ -26138,6 +26702,24 @@ function setPersistedDeliveredNoteNudge(db, sessionId, text, messageId = "") {
|
|
|
26138
26702
|
function clearPersistedNoteNudge(db, sessionId) {
|
|
26139
26703
|
db.prepare("UPDATE session_meta SET note_nudge_trigger_pending = 0, note_nudge_trigger_message_id = '', note_nudge_sticky_text = '', note_nudge_sticky_message_id = '' WHERE session_id = ?").run(sessionId);
|
|
26140
26704
|
}
|
|
26705
|
+
function getPersistedCompactionMarkerState(db, sessionId) {
|
|
26706
|
+
const row = db.prepare("SELECT compaction_marker_state FROM session_meta WHERE session_id = ?").get(sessionId);
|
|
26707
|
+
const raw = row?.compaction_marker_state;
|
|
26708
|
+
if (!raw || raw.length === 0)
|
|
26709
|
+
return null;
|
|
26710
|
+
try {
|
|
26711
|
+
const parsed = JSON.parse(raw);
|
|
26712
|
+
if (parsed && typeof parsed === "object" && typeof parsed.boundaryMessageId === "string" && typeof parsed.summaryMessageId === "string" && typeof parsed.compactionPartId === "string" && typeof parsed.summaryPartId === "string" && typeof parsed.boundaryOrdinal === "number") {
|
|
26713
|
+
return parsed;
|
|
26714
|
+
}
|
|
26715
|
+
} catch {}
|
|
26716
|
+
return null;
|
|
26717
|
+
}
|
|
26718
|
+
function setPersistedCompactionMarkerState(db, sessionId, state) {
|
|
26719
|
+
ensureSessionMetaRow(db, sessionId);
|
|
26720
|
+
const json2 = state ? JSON.stringify(state) : "";
|
|
26721
|
+
db.prepare("UPDATE session_meta SET compaction_marker_state = ? WHERE session_id = ?").run(json2, sessionId);
|
|
26722
|
+
}
|
|
26141
26723
|
function getStrippedPlaceholderIds(db, sessionId) {
|
|
26142
26724
|
const row = db.prepare("SELECT stripped_placeholder_ids FROM session_meta WHERE session_id = ?").get(sessionId);
|
|
26143
26725
|
const raw = row?.stripped_placeholder_ids;
|
|
@@ -26208,37 +26790,13 @@ function clearSession(db, sessionId) {
|
|
|
26208
26790
|
db.prepare("DELETE FROM compartments WHERE session_id = ?").run(sessionId);
|
|
26209
26791
|
clearCompressionDepth(db, sessionId);
|
|
26210
26792
|
db.prepare("DELETE FROM session_facts WHERE session_id = ?").run(sessionId);
|
|
26211
|
-
db.prepare("DELETE FROM
|
|
26793
|
+
db.prepare("DELETE FROM notes WHERE session_id = ? AND type = 'session'").run(sessionId);
|
|
26212
26794
|
db.prepare("DELETE FROM recomp_compartments WHERE session_id = ?").run(sessionId);
|
|
26213
26795
|
db.prepare("DELETE FROM recomp_facts WHERE session_id = ?").run(sessionId);
|
|
26796
|
+
db.prepare("DELETE FROM user_memory_candidates WHERE session_id = ?").run(sessionId);
|
|
26214
26797
|
clearIndexedMessages(db, sessionId);
|
|
26215
26798
|
})();
|
|
26216
26799
|
}
|
|
26217
|
-
// src/features/magic-context/storage-notes.ts
|
|
26218
|
-
function isSessionNoteRow(row) {
|
|
26219
|
-
if (row === null || typeof row !== "object")
|
|
26220
|
-
return false;
|
|
26221
|
-
const candidate = row;
|
|
26222
|
-
return typeof candidate.id === "number" && typeof candidate.session_id === "string" && typeof candidate.content === "string" && typeof candidate.created_at === "number";
|
|
26223
|
-
}
|
|
26224
|
-
function toSessionNote(row) {
|
|
26225
|
-
return {
|
|
26226
|
-
id: row.id,
|
|
26227
|
-
sessionId: row.session_id,
|
|
26228
|
-
content: row.content,
|
|
26229
|
-
createdAt: row.created_at
|
|
26230
|
-
};
|
|
26231
|
-
}
|
|
26232
|
-
function getSessionNotes(db, sessionId) {
|
|
26233
|
-
const rows = db.prepare("SELECT * FROM session_notes WHERE session_id = ? ORDER BY id ASC").all(sessionId).filter(isSessionNoteRow);
|
|
26234
|
-
return rows.map(toSessionNote);
|
|
26235
|
-
}
|
|
26236
|
-
function addSessionNote(db, sessionId, content) {
|
|
26237
|
-
db.prepare("INSERT INTO session_notes (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, Date.now());
|
|
26238
|
-
}
|
|
26239
|
-
function clearSessionNotes(db, sessionId) {
|
|
26240
|
-
db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
|
|
26241
|
-
}
|
|
26242
26800
|
// src/features/magic-context/storage-ops.ts
|
|
26243
26801
|
init_logger();
|
|
26244
26802
|
var queuePendingOpStatements = new WeakMap;
|
|
@@ -26455,7 +27013,14 @@ function getTopNBySize(db, sessionId, n) {
|
|
|
26455
27013
|
init_logger();
|
|
26456
27014
|
var DREAM_TIMER_INTERVAL_MS = 15 * 60 * 1000;
|
|
26457
27015
|
function startDreamScheduleTimer(args) {
|
|
26458
|
-
const {
|
|
27016
|
+
const {
|
|
27017
|
+
client,
|
|
27018
|
+
directory,
|
|
27019
|
+
dreamerConfig,
|
|
27020
|
+
embeddingConfig: embeddingConfig2,
|
|
27021
|
+
memoryEnabled,
|
|
27022
|
+
experimentalUserMemories
|
|
27023
|
+
} = args;
|
|
26459
27024
|
const dreamingEnabled = Boolean(dreamerConfig?.enabled && dreamerConfig.schedule?.trim());
|
|
26460
27025
|
const embeddingSweepEnabled = memoryEnabled && embeddingConfig2.provider !== "off";
|
|
26461
27026
|
if (!dreamingEnabled && !embeddingSweepEnabled) {
|
|
@@ -26483,7 +27048,8 @@ function startDreamScheduleTimer(args) {
|
|
|
26483
27048
|
client,
|
|
26484
27049
|
tasks: dreamerConfig.tasks,
|
|
26485
27050
|
taskTimeoutMinutes: dreamerConfig.task_timeout_minutes,
|
|
26486
|
-
maxRuntimeMinutes: dreamerConfig.max_runtime_minutes
|
|
27051
|
+
maxRuntimeMinutes: dreamerConfig.max_runtime_minutes,
|
|
27052
|
+
experimentalUserMemories
|
|
26487
27053
|
}).catch((error48) => {
|
|
26488
27054
|
log("[dreamer] timer-triggered queue processing failed:", error48);
|
|
26489
27055
|
});
|
|
@@ -27204,6 +27770,8 @@ async function sendUserPrompt(client, sessionId, text) {
|
|
|
27204
27770
|
}
|
|
27205
27771
|
|
|
27206
27772
|
// src/hooks/magic-context/command-handler.ts
|
|
27773
|
+
var recompConfirmationBySession = new Map;
|
|
27774
|
+
var RECOMP_CONFIRMATION_WINDOW_MS = 60000;
|
|
27207
27775
|
var SENTINEL_PREFIX = "__CONTEXT_MANAGEMENT_";
|
|
27208
27776
|
async function executeAugmentation(deps, sessionId, userPrompt) {
|
|
27209
27777
|
if (!deps.sidekick?.config) {
|
|
@@ -27279,7 +27847,8 @@ Dreaming is not configured for this project.`, {});
|
|
|
27279
27847
|
client: deps.dreamer.client,
|
|
27280
27848
|
tasks: deps.dreamer.config.tasks,
|
|
27281
27849
|
taskTimeoutMinutes: deps.dreamer.config.task_timeout_minutes,
|
|
27282
|
-
maxRuntimeMinutes: deps.dreamer.config.max_runtime_minutes
|
|
27850
|
+
maxRuntimeMinutes: deps.dreamer.config.max_runtime_minutes,
|
|
27851
|
+
experimentalUserMemories: deps.dreamer.experimentalUserMemories
|
|
27283
27852
|
});
|
|
27284
27853
|
await deps.sendNotification(sessionId, result ? summarizeDreamResult(result) : "Dream queued, but another worker is already processing the queue.", {});
|
|
27285
27854
|
throw new Error(`${SENTINEL_PREFIX}CTX-DREAM_HANDLED__`);
|
|
@@ -27322,12 +27891,40 @@ function createMagicContextCommandHandler(deps) {
|
|
|
27322
27891
|
${statusOutput}` : statusOutput;
|
|
27323
27892
|
}
|
|
27324
27893
|
if (isRecomp) {
|
|
27325
|
-
|
|
27326
|
-
|
|
27327
|
-
Historian recomp started. Rebuilding compartments and facts from raw session history now.`, {});
|
|
27328
|
-
result = deps.executeRecomp ? await deps.executeRecomp(sessionId) : `## Magic Recomp
|
|
27894
|
+
if (!deps.executeRecomp) {
|
|
27895
|
+
result = `## Magic Recomp
|
|
27329
27896
|
|
|
27330
27897
|
/ctx-recomp is unavailable because the recomp handler is not configured.`;
|
|
27898
|
+
} else {
|
|
27899
|
+
const lastConfirmation = recompConfirmationBySession.get(sessionId);
|
|
27900
|
+
const now = Date.now();
|
|
27901
|
+
if (lastConfirmation && now - lastConfirmation < RECOMP_CONFIRMATION_WINDOW_MS) {
|
|
27902
|
+
recompConfirmationBySession.delete(sessionId);
|
|
27903
|
+
await deps.sendNotification(sessionId, `## Magic Recomp
|
|
27904
|
+
|
|
27905
|
+
Historian recomp started. Rebuilding compartments and facts from raw session history now.`, {});
|
|
27906
|
+
result = await deps.executeRecomp(sessionId);
|
|
27907
|
+
} else {
|
|
27908
|
+
recompConfirmationBySession.set(sessionId, now);
|
|
27909
|
+
const compartments = getCompartments(deps.db, sessionId);
|
|
27910
|
+
const compartmentCount = compartments.length;
|
|
27911
|
+
const warningLines = [
|
|
27912
|
+
"## \u26A0\uFE0F Recomp Confirmation Required",
|
|
27913
|
+
"",
|
|
27914
|
+
`You currently have **${compartmentCount}** compartments.`,
|
|
27915
|
+
"Running /ctx-recomp will **regenerate all compartments and facts** from raw session history.",
|
|
27916
|
+
"",
|
|
27917
|
+
"This operation:",
|
|
27918
|
+
"- May take a long time (minutes to hours for long sessions)",
|
|
27919
|
+
"- Will consume significant tokens on your historian model",
|
|
27920
|
+
"- Cannot be interrupted cleanly once started",
|
|
27921
|
+
"",
|
|
27922
|
+
"**To confirm, run `/ctx-recomp` again within 60 seconds.**"
|
|
27923
|
+
];
|
|
27924
|
+
result = warningLines.join(`
|
|
27925
|
+
`);
|
|
27926
|
+
}
|
|
27927
|
+
}
|
|
27331
27928
|
}
|
|
27332
27929
|
await deps.sendNotification(sessionId, result, {});
|
|
27333
27930
|
sessionLog(sessionId, `command ${input.command} handled via command.execute.before`);
|
|
@@ -27339,6 +27936,175 @@ Historian recomp started. Rebuilding compartments and facts from raw session his
|
|
|
27339
27936
|
// src/hooks/magic-context/event-handler.ts
|
|
27340
27937
|
init_logger();
|
|
27341
27938
|
|
|
27939
|
+
// src/features/magic-context/compaction-marker.ts
|
|
27940
|
+
import { Database as Database3 } from "bun:sqlite";
|
|
27941
|
+
import { join as join10 } from "path";
|
|
27942
|
+
init_logger();
|
|
27943
|
+
var BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
27944
|
+
function randomBase62(length) {
|
|
27945
|
+
const chars = [];
|
|
27946
|
+
for (let i = 0;i < length; i++) {
|
|
27947
|
+
chars.push(BASE62_CHARS[Math.floor(Math.random() * BASE62_CHARS.length)]);
|
|
27948
|
+
}
|
|
27949
|
+
return chars.join("");
|
|
27950
|
+
}
|
|
27951
|
+
function generateId(prefix, timestampMs, counter = 0n) {
|
|
27952
|
+
const encoded = BigInt(timestampMs) * 0x1000n + counter;
|
|
27953
|
+
const hex3 = encoded.toString(16).padStart(14, "0");
|
|
27954
|
+
return `${prefix}_${hex3}${randomBase62(14)}`;
|
|
27955
|
+
}
|
|
27956
|
+
function generateMessageId(timestampMs, counter = 0n) {
|
|
27957
|
+
return generateId("msg", timestampMs, counter);
|
|
27958
|
+
}
|
|
27959
|
+
function generatePartId(timestampMs, counter = 0n) {
|
|
27960
|
+
return generateId("prt", timestampMs, counter);
|
|
27961
|
+
}
|
|
27962
|
+
function getOpenCodeDbPath2() {
|
|
27963
|
+
return join10(getDataDir(), "opencode", "opencode.db");
|
|
27964
|
+
}
|
|
27965
|
+
var cachedWriteDb = null;
|
|
27966
|
+
function getWritableOpenCodeDb() {
|
|
27967
|
+
const dbPath = getOpenCodeDbPath2();
|
|
27968
|
+
if (cachedWriteDb?.path === dbPath) {
|
|
27969
|
+
return cachedWriteDb.db;
|
|
27970
|
+
}
|
|
27971
|
+
if (cachedWriteDb) {
|
|
27972
|
+
try {
|
|
27973
|
+
cachedWriteDb.db.close(false);
|
|
27974
|
+
} catch {}
|
|
27975
|
+
}
|
|
27976
|
+
const db = new Database3(dbPath);
|
|
27977
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
27978
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
27979
|
+
cachedWriteDb = { path: dbPath, db };
|
|
27980
|
+
return db;
|
|
27981
|
+
}
|
|
27982
|
+
function findBoundaryUserMessage(sessionId, endOrdinal) {
|
|
27983
|
+
const db = getWritableOpenCodeDb();
|
|
27984
|
+
const rows = db.prepare("SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId);
|
|
27985
|
+
const filtered = rows.filter((row) => {
|
|
27986
|
+
try {
|
|
27987
|
+
const info = JSON.parse(row.data);
|
|
27988
|
+
return !(info.summary === true && info.finish === "stop");
|
|
27989
|
+
} catch {
|
|
27990
|
+
return true;
|
|
27991
|
+
}
|
|
27992
|
+
});
|
|
27993
|
+
let bestMatch = null;
|
|
27994
|
+
for (let i = 0;i < filtered.length && i < endOrdinal; i++) {
|
|
27995
|
+
const row = filtered[i];
|
|
27996
|
+
try {
|
|
27997
|
+
const info = JSON.parse(row.data);
|
|
27998
|
+
if (info.role === "user") {
|
|
27999
|
+
bestMatch = { id: row.id, timeCreated: row.time_created };
|
|
28000
|
+
}
|
|
28001
|
+
} catch {}
|
|
28002
|
+
}
|
|
28003
|
+
return bestMatch;
|
|
28004
|
+
}
|
|
28005
|
+
function injectCompactionMarker(args) {
|
|
28006
|
+
const boundary = findBoundaryUserMessage(args.sessionId, args.endOrdinal);
|
|
28007
|
+
if (!boundary) {
|
|
28008
|
+
log(`[magic-context] compaction-marker: no user message found at or before ordinal ${args.endOrdinal}`);
|
|
28009
|
+
return null;
|
|
28010
|
+
}
|
|
28011
|
+
const db = getWritableOpenCodeDb();
|
|
28012
|
+
const boundaryTime = boundary.timeCreated;
|
|
28013
|
+
const summaryMsgId = generateMessageId(boundaryTime + 1, 1n);
|
|
28014
|
+
const compactionPartId = generatePartId(boundaryTime, 1n);
|
|
28015
|
+
const summaryPartId = generatePartId(boundaryTime + 1, 2n);
|
|
28016
|
+
const summaryMsgData = JSON.stringify({
|
|
28017
|
+
role: "assistant",
|
|
28018
|
+
parentID: boundary.id,
|
|
28019
|
+
summary: true,
|
|
28020
|
+
finish: "stop",
|
|
28021
|
+
mode: "compaction",
|
|
28022
|
+
agent: "compaction",
|
|
28023
|
+
path: { cwd: args.directory, root: args.directory },
|
|
28024
|
+
cost: 0,
|
|
28025
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
28026
|
+
modelID: "magic-context",
|
|
28027
|
+
providerID: "magic-context",
|
|
28028
|
+
time: { created: boundaryTime + 1 }
|
|
28029
|
+
});
|
|
28030
|
+
try {
|
|
28031
|
+
db.transaction(() => {
|
|
28032
|
+
db.prepare("INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)").run(compactionPartId, boundary.id, args.sessionId, boundaryTime, boundaryTime, '{"type":"compaction","auto":true}');
|
|
28033
|
+
db.prepare("INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)").run(summaryMsgId, args.sessionId, boundaryTime + 1, boundaryTime + 1, summaryMsgData);
|
|
28034
|
+
db.prepare("INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)").run(summaryPartId, summaryMsgId, args.sessionId, boundaryTime + 1, boundaryTime + 1, JSON.stringify({ type: "text", text: args.summaryText }));
|
|
28035
|
+
})();
|
|
28036
|
+
log(`[magic-context] compaction-marker: injected boundary at user msg ${boundary.id} (ordinal ~${args.endOrdinal}), summary msg ${summaryMsgId}`);
|
|
28037
|
+
return {
|
|
28038
|
+
boundaryMessageId: boundary.id,
|
|
28039
|
+
summaryMessageId: summaryMsgId,
|
|
28040
|
+
compactionPartId,
|
|
28041
|
+
summaryPartId
|
|
28042
|
+
};
|
|
28043
|
+
} catch (error48) {
|
|
28044
|
+
log(`[magic-context] compaction-marker: injection failed: ${error48 instanceof Error ? error48.message : String(error48)}`);
|
|
28045
|
+
return null;
|
|
28046
|
+
}
|
|
28047
|
+
}
|
|
28048
|
+
function removeCompactionMarker(state) {
|
|
28049
|
+
try {
|
|
28050
|
+
const db = getWritableOpenCodeDb();
|
|
28051
|
+
db.transaction(() => {
|
|
28052
|
+
db.prepare("DELETE FROM part WHERE id = ?").run(state.summaryPartId);
|
|
28053
|
+
db.prepare("DELETE FROM message WHERE id = ?").run(state.summaryMessageId);
|
|
28054
|
+
db.prepare("DELETE FROM part WHERE id = ?").run(state.compactionPartId);
|
|
28055
|
+
})();
|
|
28056
|
+
return true;
|
|
28057
|
+
} catch (error48) {
|
|
28058
|
+
log(`[magic-context] compaction-marker: removal failed: ${error48 instanceof Error ? error48.message : String(error48)}`);
|
|
28059
|
+
return false;
|
|
28060
|
+
}
|
|
28061
|
+
}
|
|
28062
|
+
|
|
28063
|
+
// src/hooks/magic-context/compaction-marker-manager.ts
|
|
28064
|
+
init_logger();
|
|
28065
|
+
var MARKER_SUMMARY_TEXT = "[Compacted by magic-context \u2014 session history is managed by the plugin]";
|
|
28066
|
+
function updateCompactionMarkerAfterPublication(db, sessionId, lastCompartmentEnd, directory) {
|
|
28067
|
+
const existing = getPersistedCompactionMarkerState(db, sessionId);
|
|
28068
|
+
if (existing) {
|
|
28069
|
+
if (existing.boundaryOrdinal === lastCompartmentEnd) {
|
|
28070
|
+
return;
|
|
28071
|
+
}
|
|
28072
|
+
try {
|
|
28073
|
+
removeCompactionMarker(existing);
|
|
28074
|
+
setPersistedCompactionMarkerState(db, sessionId, null);
|
|
28075
|
+
sessionLog(sessionId, `compaction-marker: removed old boundary at ordinal ${existing.boundaryOrdinal}, moving to ${lastCompartmentEnd}`);
|
|
28076
|
+
} catch (error48) {
|
|
28077
|
+
sessionLog(sessionId, `compaction-marker: failed to remove old boundary at ordinal ${existing.boundaryOrdinal}, proceeding with new injection:`, error48);
|
|
28078
|
+
}
|
|
28079
|
+
}
|
|
28080
|
+
const result = injectCompactionMarker({
|
|
28081
|
+
sessionId,
|
|
28082
|
+
endOrdinal: lastCompartmentEnd,
|
|
28083
|
+
summaryText: MARKER_SUMMARY_TEXT,
|
|
28084
|
+
directory: directory ?? process.cwd()
|
|
28085
|
+
});
|
|
28086
|
+
if (result) {
|
|
28087
|
+
setPersistedCompactionMarkerState(db, sessionId, {
|
|
28088
|
+
...result,
|
|
28089
|
+
boundaryOrdinal: lastCompartmentEnd
|
|
28090
|
+
});
|
|
28091
|
+
sessionLog(sessionId, `compaction-marker: injected at ordinal ${lastCompartmentEnd}, boundary user msg ${result.boundaryMessageId}`);
|
|
28092
|
+
}
|
|
28093
|
+
}
|
|
28094
|
+
function removeCompactionMarkerForSession(db, sessionId) {
|
|
28095
|
+
const existing = getPersistedCompactionMarkerState(db, sessionId);
|
|
28096
|
+
if (existing) {
|
|
28097
|
+
try {
|
|
28098
|
+
removeCompactionMarker(existing);
|
|
28099
|
+
setPersistedCompactionMarkerState(db, sessionId, null);
|
|
28100
|
+
sessionLog(sessionId, "compaction-marker: removed on session cleanup");
|
|
28101
|
+
} catch (error48) {
|
|
28102
|
+
setPersistedCompactionMarkerState(db, sessionId, null);
|
|
28103
|
+
sessionLog(sessionId, "compaction-marker: removal failed during session cleanup, cleared persisted state:", error48);
|
|
28104
|
+
}
|
|
28105
|
+
}
|
|
28106
|
+
}
|
|
28107
|
+
|
|
27342
28108
|
// src/hooks/magic-context/event-payloads.ts
|
|
27343
28109
|
function getSessionProperties(properties) {
|
|
27344
28110
|
if (!isRecord(properties)) {
|
|
@@ -27635,6 +28401,11 @@ function createEventHandler2(deps) {
|
|
|
27635
28401
|
clearNoteNudgeState(deps.db, info.sessionID, { persist: false });
|
|
27636
28402
|
sessionLog(info.sessionID, "event message.removed: cleared in-memory note nudge state");
|
|
27637
28403
|
}
|
|
28404
|
+
const markerState = getPersistedCompactionMarkerState(deps.db, info.sessionID);
|
|
28405
|
+
if (markerState && (markerState.boundaryMessageId === info.messageID || markerState.summaryMessageId === info.messageID)) {
|
|
28406
|
+
removeCompactionMarkerForSession(deps.db, info.sessionID);
|
|
28407
|
+
sessionLog(info.sessionID, `event message.removed: cleared compaction marker (boundary or summary message removed)`);
|
|
28408
|
+
}
|
|
27638
28409
|
deps.onSessionCacheInvalidated?.(info.sessionID);
|
|
27639
28410
|
sessionLog(info.sessionID, "event message.removed: cleared session injection cache");
|
|
27640
28411
|
} catch (error48) {
|
|
@@ -27652,6 +28423,11 @@ function createEventHandler2(deps) {
|
|
|
27652
28423
|
} catch (error48) {
|
|
27653
28424
|
sessionLog(sessionId, "event session.compacted handling failed:", error48);
|
|
27654
28425
|
}
|
|
28426
|
+
try {
|
|
28427
|
+
removeCompactionMarkerForSession(deps.db, sessionId);
|
|
28428
|
+
} catch (error48) {
|
|
28429
|
+
sessionLog(sessionId, "event session.compacted marker cleanup failed:", error48);
|
|
28430
|
+
}
|
|
27655
28431
|
deps.onSessionCacheInvalidated?.(sessionId);
|
|
27656
28432
|
return;
|
|
27657
28433
|
}
|
|
@@ -27662,6 +28438,7 @@ function createEventHandler2(deps) {
|
|
|
27662
28438
|
}
|
|
27663
28439
|
deps.nudgePlacements.clear(sessionId);
|
|
27664
28440
|
try {
|
|
28441
|
+
removeCompactionMarkerForSession(deps.db, sessionId);
|
|
27665
28442
|
clearSession(deps.db, sessionId);
|
|
27666
28443
|
} catch (error48) {
|
|
27667
28444
|
sessionLog(sessionId, "event session.deleted persistence failed:", error48);
|
|
@@ -27741,7 +28518,10 @@ function trimMemoriesToBudget(sessionId, memories, budgetTokens) {
|
|
|
27741
28518
|
return -1;
|
|
27742
28519
|
if (b.status === "permanent" && a.status !== "permanent")
|
|
27743
28520
|
return 1;
|
|
27744
|
-
|
|
28521
|
+
const seenDiff = b.seenCount - a.seenCount;
|
|
28522
|
+
if (seenDiff !== 0)
|
|
28523
|
+
return seenDiff;
|
|
28524
|
+
return a.id - b.id;
|
|
27745
28525
|
});
|
|
27746
28526
|
const result = [];
|
|
27747
28527
|
let usedTokens = 0;
|
|
@@ -28479,6 +29259,9 @@ function searchMemoriesFTS(db, projectPath, query, limit = DEFAULT_SEARCH_LIMIT)
|
|
|
28479
29259
|
const rows = getSearchStatement(db).all(projectPath, Date.now(), sanitized, limit).filter(isMemoryRow);
|
|
28480
29260
|
return rows.map(toMemory);
|
|
28481
29261
|
}
|
|
29262
|
+
// src/hooks/magic-context/compartment-runner-incremental.ts
|
|
29263
|
+
init_logger();
|
|
29264
|
+
|
|
28482
29265
|
// src/hooks/magic-context/compartment-runner-compressor.ts
|
|
28483
29266
|
init_logger();
|
|
28484
29267
|
|
|
@@ -28487,6 +29270,8 @@ var COMPARTMENT_REGEX = /<compartment\s+(?:id="[^"]*"\s+)?start="(\d+)"\s+end="(
|
|
|
28487
29270
|
var CATEGORY_BLOCK_REGEX = /<(WORKFLOW_RULES|ARCHITECTURE_DECISIONS|CONSTRAINTS|CONFIG_DEFAULTS|KNOWN_ISSUES|ENVIRONMENT|NAMING|USER_PREFERENCES|USER_DIRECTIVES)>(.*?)<\/\1>/gs;
|
|
28488
29271
|
var FACT_ITEM_REGEX = /^\s*\*\s*(.+)$/gm;
|
|
28489
29272
|
var UNPROCESSED_REGEX = /<unprocessed_from>(\d+)<\/unprocessed_from>/;
|
|
29273
|
+
var USER_OBSERVATIONS_REGEX = /<user_observations>(.*?)<\/user_observations>/s;
|
|
29274
|
+
var USER_OBS_ITEM_REGEX = /^\s*\*\s*(.+)$/gm;
|
|
28490
29275
|
function parseCompartmentOutput(text) {
|
|
28491
29276
|
const compartments = [];
|
|
28492
29277
|
const facts = [];
|
|
@@ -28511,8 +29296,17 @@ function parseCompartmentOutput(text) {
|
|
|
28511
29296
|
}
|
|
28512
29297
|
const unprocessedMatch = text.match(UNPROCESSED_REGEX);
|
|
28513
29298
|
const unprocessedFrom = unprocessedMatch ? parseInt(unprocessedMatch[1], 10) : null;
|
|
29299
|
+
const userObservations = [];
|
|
29300
|
+
const userObsMatch = text.match(USER_OBSERVATIONS_REGEX);
|
|
29301
|
+
if (userObsMatch) {
|
|
29302
|
+
for (const itemMatch of userObsMatch[1].matchAll(USER_OBS_ITEM_REGEX)) {
|
|
29303
|
+
const obs = unescapeXml(itemMatch[1].trim());
|
|
29304
|
+
if (obs)
|
|
29305
|
+
userObservations.push(obs);
|
|
29306
|
+
}
|
|
29307
|
+
}
|
|
28514
29308
|
compartments.sort((a, b) => a.startMessage - b.startMessage);
|
|
28515
|
-
return { compartments, facts, unprocessedFrom };
|
|
29309
|
+
return { compartments, facts, unprocessedFrom, userObservations };
|
|
28516
29310
|
}
|
|
28517
29311
|
function unescapeXml(s) {
|
|
28518
29312
|
return s.replace(/&/g, "&").replace(/'/g, "'").replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -28671,6 +29465,7 @@ ${compartment.content}
|
|
|
28671
29465
|
}
|
|
28672
29466
|
}
|
|
28673
29467
|
replaceAllCompartmentState(db, sessionId, allCompartments, facts.map((f) => ({ category: f.category, content: f.content })));
|
|
29468
|
+
clearInjectionCache(sessionId);
|
|
28674
29469
|
incrementCompressionDepth(db, sessionId, originalStart, originalEnd);
|
|
28675
29470
|
sessionLog(sessionId, `compressor: replaced ${selectedCompartments.length} compartments with ${compressed.length} compressed compartments`);
|
|
28676
29471
|
sessionLog(sessionId, `compressor: incremented compression depth for messages ${originalStart}-${originalEnd}`);
|
|
@@ -28790,7 +29585,7 @@ function queueDropsForCompartmentalizedMessages(db, sessionId, upToMessageIndex)
|
|
|
28790
29585
|
// src/hooks/magic-context/compartment-runner-historian.ts
|
|
28791
29586
|
import { mkdirSync as mkdirSync2, unlinkSync, writeFileSync } from "fs";
|
|
28792
29587
|
import { tmpdir as tmpdir2 } from "os";
|
|
28793
|
-
import { join as
|
|
29588
|
+
import { join as join11 } from "path";
|
|
28794
29589
|
|
|
28795
29590
|
// src/hooks/magic-context/compartment-runner-mapping.ts
|
|
28796
29591
|
function mapParsedCompartmentsToChunk(compartments, chunk, sequenceOffset) {
|
|
@@ -28857,7 +29652,8 @@ function validateHistorianOutput(text, _sessionId, chunk, _priorCompartments, se
|
|
|
28857
29652
|
return {
|
|
28858
29653
|
ok: true,
|
|
28859
29654
|
compartments: mapped.compartments,
|
|
28860
|
-
facts: parsed.facts
|
|
29655
|
+
facts: parsed.facts,
|
|
29656
|
+
userObservations: parsed.userObservations.length > 0 ? parsed.userObservations : undefined
|
|
28861
29657
|
};
|
|
28862
29658
|
}
|
|
28863
29659
|
function buildHistorianRepairPrompt(originalPrompt, previousOutput, validationError) {
|
|
@@ -28950,7 +29746,7 @@ function getReducedRecompTokenBudget(currentBudget) {
|
|
|
28950
29746
|
}
|
|
28951
29747
|
|
|
28952
29748
|
// src/hooks/magic-context/compartment-runner-historian.ts
|
|
28953
|
-
var HISTORIAN_RESPONSE_DUMP_DIR =
|
|
29749
|
+
var HISTORIAN_RESPONSE_DUMP_DIR = join11(tmpdir2(), "magic-context-historian");
|
|
28954
29750
|
async function runValidatedHistorianPass(args) {
|
|
28955
29751
|
const firstRun = await runHistorianPrompt({
|
|
28956
29752
|
...args,
|
|
@@ -29052,7 +29848,7 @@ function dumpHistorianResponse(sessionId, label, text) {
|
|
|
29052
29848
|
mkdirSync2(HISTORIAN_RESPONSE_DUMP_DIR, { recursive: true });
|
|
29053
29849
|
const safeSessionId = sanitizeDumpName(sessionId);
|
|
29054
29850
|
const safeLabel = sanitizeDumpName(label);
|
|
29055
|
-
const dumpPath =
|
|
29851
|
+
const dumpPath = join11(HISTORIAN_RESPONSE_DUMP_DIR, `${safeSessionId}-${safeLabel}-${Date.now()}.xml`);
|
|
29056
29852
|
writeFileSync(dumpPath, text, "utf8");
|
|
29057
29853
|
sessionLog(sessionId, "compartment agent: historian response dumped", {
|
|
29058
29854
|
label,
|
|
@@ -29201,11 +29997,15 @@ No new compartments or facts were written. Check the historian model/output and
|
|
|
29201
29997
|
appendCompartments(db, sessionId, newCompartments);
|
|
29202
29998
|
replaceSessionFacts(db, sessionId, validatedPass.facts ?? []);
|
|
29203
29999
|
})();
|
|
30000
|
+
clearInjectionCache(sessionId);
|
|
29204
30001
|
if (deps.directory) {
|
|
29205
30002
|
promoteSessionFactsToMemory(db, sessionId, resolveProjectIdentity(deps.directory), validatedPass.facts ?? []);
|
|
29206
30003
|
}
|
|
29207
30004
|
const lastCompartmentEnd = lastNewEnd;
|
|
29208
30005
|
queueDropsForCompartmentalizedMessages(db, sessionId, lastCompartmentEnd);
|
|
30006
|
+
if (deps.experimentalCompactionMarkers) {
|
|
30007
|
+
updateCompactionMarkerAfterPublication(db, sessionId, lastCompartmentEnd, sessionDirectory);
|
|
30008
|
+
}
|
|
29209
30009
|
if (deps.historyBudgetTokens && deps.historyBudgetTokens > 0) {
|
|
29210
30010
|
await runCompressionPassIfNeeded({
|
|
29211
30011
|
client,
|
|
@@ -29219,6 +30019,20 @@ No new compartments or facts were written. Check the historian model/output and
|
|
|
29219
30019
|
updateSessionMeta(db, sessionId, { compartmentInProgress: false });
|
|
29220
30020
|
completedSuccessfully = true;
|
|
29221
30021
|
onNoteTrigger(db, sessionId, "historian_complete");
|
|
30022
|
+
if (validatedPass.userObservations && validatedPass.userObservations.length > 0) {
|
|
30023
|
+
try {
|
|
30024
|
+
const lastNew = newCompartments[newCompartments.length - 1];
|
|
30025
|
+
insertUserMemoryCandidates(db, validatedPass.userObservations.map((obs) => ({
|
|
30026
|
+
content: obs,
|
|
30027
|
+
sessionId,
|
|
30028
|
+
sourceCompartmentStart: newCompartments[0]?.startMessage,
|
|
30029
|
+
sourceCompartmentEnd: lastNew?.endMessage
|
|
30030
|
+
})));
|
|
30031
|
+
sessionLog(sessionId, `stored ${validatedPass.userObservations.length} user memory candidate(s)`);
|
|
30032
|
+
} catch (error48) {
|
|
30033
|
+
sessionLog(sessionId, "failed to store user memory candidates:", error48);
|
|
30034
|
+
}
|
|
30035
|
+
}
|
|
29222
30036
|
} catch (error48) {
|
|
29223
30037
|
const msg = getErrorMessage(error48);
|
|
29224
30038
|
if (!issueNotified) {
|
|
@@ -29259,6 +30073,7 @@ async function executeContextRecompInternal(deps) {
|
|
|
29259
30073
|
const promoted2 = promoteRecompStaging(db, sessionId);
|
|
29260
30074
|
if (!promoted2)
|
|
29261
30075
|
return null;
|
|
30076
|
+
clearInjectionCache(sessionId);
|
|
29262
30077
|
if (deps.directory) {
|
|
29263
30078
|
promoteSessionFactsToMemory(db, sessionId, resolveProjectIdentity(deps.directory), promoted2.facts);
|
|
29264
30079
|
}
|
|
@@ -29266,6 +30081,9 @@ async function executeContextRecompInternal(deps) {
|
|
|
29266
30081
|
if (lastCompartmentEnd2 > 0) {
|
|
29267
30082
|
queueDropsForCompartmentalizedMessages(db, sessionId, lastCompartmentEnd2);
|
|
29268
30083
|
}
|
|
30084
|
+
if (deps.experimentalCompactionMarkers && lastCompartmentEnd2 > 0) {
|
|
30085
|
+
updateCompactionMarkerAfterPublication(db, sessionId, lastCompartmentEnd2, deps.directory);
|
|
30086
|
+
}
|
|
29269
30087
|
return [
|
|
29270
30088
|
`Persisted ${promoted2.compartments.length} compartment${promoted2.compartments.length === 1 ? "" : "s"} from ${passCount} successful pass${passCount === 1 ? "" : "es"}.`,
|
|
29271
30089
|
`Covered raw history 1-${lastCompartmentEnd2} out of ${rawMessageCount} total messages.`,
|
|
@@ -29420,6 +30238,7 @@ Nothing was written.`;
|
|
|
29420
30238
|
replaceAllCompartmentState(db, sessionId, candidateCompartments, candidateFacts);
|
|
29421
30239
|
clearRecompStaging(db, sessionId);
|
|
29422
30240
|
}
|
|
30241
|
+
clearInjectionCache(sessionId);
|
|
29423
30242
|
const finalCompartments = promoted?.compartments ?? candidateCompartments;
|
|
29424
30243
|
const finalFacts = promoted?.facts ?? candidateFacts;
|
|
29425
30244
|
if (deps.directory) {
|
|
@@ -29549,7 +30368,9 @@ async function runCompartmentPhase(args) {
|
|
|
29549
30368
|
historyBudgetTokens: args.historyBudgetTokens,
|
|
29550
30369
|
historianTimeoutMs: args.historianTimeoutMs,
|
|
29551
30370
|
directory: args.compartmentDirectory,
|
|
29552
|
-
getNotificationParams: args.getNotificationParams
|
|
30371
|
+
getNotificationParams: args.getNotificationParams,
|
|
30372
|
+
experimentalCompactionMarkers: args.experimentalCompactionMarkers,
|
|
30373
|
+
experimentalUserMemories: args.experimentalUserMemories
|
|
29553
30374
|
});
|
|
29554
30375
|
compartmentInProgress = true;
|
|
29555
30376
|
}
|
|
@@ -29575,7 +30396,9 @@ async function runCompartmentPhase(args) {
|
|
|
29575
30396
|
historyBudgetTokens: args.historyBudgetTokens,
|
|
29576
30397
|
historianTimeoutMs: args.historianTimeoutMs,
|
|
29577
30398
|
directory: args.compartmentDirectory,
|
|
29578
|
-
getNotificationParams: args.getNotificationParams
|
|
30399
|
+
getNotificationParams: args.getNotificationParams,
|
|
30400
|
+
experimentalCompactionMarkers: args.experimentalCompactionMarkers,
|
|
30401
|
+
experimentalUserMemories: args.experimentalUserMemories
|
|
29579
30402
|
});
|
|
29580
30403
|
activeRun = getActiveCompartmentRun(args.sessionId);
|
|
29581
30404
|
} else if (!activeRun && hasEligibleHistoryForCompartment()) {
|
|
@@ -29589,7 +30412,7 @@ async function runCompartmentPhase(args) {
|
|
|
29589
30412
|
}
|
|
29590
30413
|
if (args.cacheAlreadyBusting && args.historyBudgetTokens && args.historyBudgetTokens > 0 && args.client && !compartmentInProgress && !awaitedCompartmentRun) {
|
|
29591
30414
|
try {
|
|
29592
|
-
await runCompressionPassIfNeeded({
|
|
30415
|
+
const compressed = await runCompressionPassIfNeeded({
|
|
29593
30416
|
client: args.client,
|
|
29594
30417
|
db: args.db,
|
|
29595
30418
|
sessionId: args.sessionId,
|
|
@@ -29597,6 +30420,9 @@ async function runCompartmentPhase(args) {
|
|
|
29597
30420
|
historyBudgetTokens: args.historyBudgetTokens,
|
|
29598
30421
|
historianTimeoutMs: args.historianTimeoutMs
|
|
29599
30422
|
});
|
|
30423
|
+
if (compressed && args.projectPath !== undefined) {
|
|
30424
|
+
pendingCompartmentInjection = prepareCompartmentInjection(args.db, args.sessionId, args.messages, true, args.projectPath, args.injectionBudgetTokens);
|
|
30425
|
+
}
|
|
29600
30426
|
} catch (error48) {
|
|
29601
30427
|
sessionLog(args.sessionId, "transform: independent compressor check failed:", getErrorMessage(error48));
|
|
29602
30428
|
}
|
|
@@ -30517,11 +31343,9 @@ function applyHeuristicCleanup(sessionId, db, targets, messageTagNumbers, config
|
|
|
30517
31343
|
return { droppedTools, deduplicatedTools, droppedInjections };
|
|
30518
31344
|
}
|
|
30519
31345
|
function extractToolInfo(part) {
|
|
30520
|
-
if (part.type === "tool" && typeof part.
|
|
30521
|
-
const state = part.state;
|
|
30522
|
-
|
|
30523
|
-
return { toolName: state.tool, args: state.input ?? {} };
|
|
30524
|
-
}
|
|
31346
|
+
if (part.type === "tool" && typeof part.tool === "string" && DEDUP_SAFE_TOOLS.has(part.tool)) {
|
|
31347
|
+
const state = typeof part.state === "object" && part.state !== null ? part.state : {};
|
|
31348
|
+
return { toolName: part.tool, args: state.input ?? {} };
|
|
30525
31349
|
}
|
|
30526
31350
|
if (part.type === "tool-invocation" && typeof part.toolName === "string" && DEDUP_SAFE_TOOLS.has(part.toolName)) {
|
|
30527
31351
|
return { toolName: part.toolName, args: part.args ?? {} };
|
|
@@ -30744,7 +31568,18 @@ function runPostTransformPhase(args) {
|
|
|
30744
31568
|
if (pendingUserTurnReminder.messageId) {
|
|
30745
31569
|
const reinjected = appendReminderToUserMessageById(args.messages, pendingUserTurnReminder.messageId, pendingUserTurnReminder.text);
|
|
30746
31570
|
if (!reinjected) {
|
|
30747
|
-
|
|
31571
|
+
if (isCacheBustingPass) {
|
|
31572
|
+
const newAnchorId = appendReminderToLatestUserMessage(args.messages, pendingUserTurnReminder.text);
|
|
31573
|
+
if (newAnchorId) {
|
|
31574
|
+
setPersistedStickyTurnReminder(args.db, args.sessionId, pendingUserTurnReminder.text, newAnchorId);
|
|
31575
|
+
sessionLog(args.sessionId, `sticky turn reminder re-anchored: ${pendingUserTurnReminder.messageId} \u2192 ${newAnchorId}`);
|
|
31576
|
+
} else {
|
|
31577
|
+
clearPersistedStickyTurnReminder(args.db, args.sessionId);
|
|
31578
|
+
sessionLog(args.sessionId, `sticky turn reminder cleared \u2014 anchor ${pendingUserTurnReminder.messageId} gone and no user message visible`);
|
|
31579
|
+
}
|
|
31580
|
+
} else {
|
|
31581
|
+
sessionLog(args.sessionId, `preserving sticky turn reminder anchor to avoid cache bust: messageId=${pendingUserTurnReminder.messageId}`);
|
|
31582
|
+
}
|
|
30748
31583
|
}
|
|
30749
31584
|
} else {
|
|
30750
31585
|
const anchoredMessageId = appendReminderToLatestUserMessage(args.messages, pendingUserTurnReminder.text);
|
|
@@ -30781,7 +31616,17 @@ function runPostTransformPhase(args) {
|
|
|
30781
31616
|
if (stickyNoteNudge) {
|
|
30782
31617
|
const reinjected = appendReminderToUserMessageById(args.messages, stickyNoteNudge.messageId, stickyNoteNudge.text);
|
|
30783
31618
|
if (!reinjected) {
|
|
30784
|
-
|
|
31619
|
+
if (isCacheBustingPass) {
|
|
31620
|
+
const newAnchorId = appendReminderToLatestUserMessage(args.messages, stickyNoteNudge.text);
|
|
31621
|
+
if (newAnchorId) {
|
|
31622
|
+
markNoteNudgeDelivered(args.db, args.sessionId, stickyNoteNudge.text, newAnchorId);
|
|
31623
|
+
sessionLog(args.sessionId, `sticky note nudge re-anchored: ${stickyNoteNudge.messageId} \u2192 ${newAnchorId}`);
|
|
31624
|
+
} else {
|
|
31625
|
+
sessionLog(args.sessionId, `sticky note nudge anchor ${stickyNoteNudge.messageId} gone \u2014 no user message visible to re-anchor`);
|
|
31626
|
+
}
|
|
31627
|
+
} else {
|
|
31628
|
+
sessionLog(args.sessionId, `preserving sticky note nudge anchor to avoid cache bust: messageId=${stickyNoteNudge.messageId}`);
|
|
31629
|
+
}
|
|
30785
31630
|
}
|
|
30786
31631
|
}
|
|
30787
31632
|
const deferredNoteText = peekNoteNudgeText(args.db, args.sessionId, args.currentTurnId, args.projectPath);
|
|
@@ -30856,7 +31701,7 @@ function createTransform(deps) {
|
|
|
30856
31701
|
const canRunCompartments = fullFeatureMode && deps.client !== undefined && compartmentDirectory.length > 0;
|
|
30857
31702
|
const contextUsageEarly = loadContextUsage(deps.contextUsageMap, db, sessionId);
|
|
30858
31703
|
const schedulerDecisionEarly = resolveSchedulerDecision(deps.scheduler, sessionMeta, contextUsageEarly, sessionId, deps.getModelKey?.(sessionId));
|
|
30859
|
-
const isCacheBusting = deps.flushedSessions.has(sessionId)
|
|
31704
|
+
const isCacheBusting = deps.flushedSessions.has(sessionId);
|
|
30860
31705
|
let pendingCompartmentInjection = null;
|
|
30861
31706
|
if (fullFeatureMode) {
|
|
30862
31707
|
const projectPath = deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined;
|
|
@@ -30947,7 +31792,9 @@ function createTransform(deps) {
|
|
|
30947
31792
|
projectPath: deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined,
|
|
30948
31793
|
injectionBudgetTokens: deps.memoryConfig?.injectionBudgetTokens,
|
|
30949
31794
|
getNotificationParams: rawGetNotifParams ? () => rawGetNotifParams(sessionId) : undefined,
|
|
30950
|
-
cacheAlreadyBusting: isCacheBusting
|
|
31795
|
+
cacheAlreadyBusting: isCacheBusting || schedulerDecisionEarly === "execute",
|
|
31796
|
+
experimentalCompactionMarkers: deps.experimentalCompactionMarkers,
|
|
31797
|
+
experimentalUserMemories: deps.experimentalUserMemories
|
|
30951
31798
|
});
|
|
30952
31799
|
pendingCompartmentInjection = compartmentPhase.pendingCompartmentInjection;
|
|
30953
31800
|
const awaitedCompartmentRun = compartmentPhase.awaitedCompartmentRun;
|
|
@@ -31137,7 +31984,7 @@ function createToolExecuteAfterHook(args) {
|
|
|
31137
31984
|
|
|
31138
31985
|
// src/hooks/magic-context/system-prompt-hash.ts
|
|
31139
31986
|
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
31140
|
-
import { join as
|
|
31987
|
+
import { join as join12 } from "path";
|
|
31141
31988
|
|
|
31142
31989
|
// src/agents/magic-context-prompt.ts
|
|
31143
31990
|
var BASE_INTRO = (protectedTags) => `Messages and tool outputs are tagged with \xA7N\xA7 identifiers (e.g., \xA71\xA7, \xA742\xA7).
|
|
@@ -31338,11 +32185,13 @@ Prefer many small targeted operations over one large blanket operation. Compress
|
|
|
31338
32185
|
init_logger();
|
|
31339
32186
|
var MAGIC_CONTEXT_MARKER = "## Magic Context";
|
|
31340
32187
|
var PROJECT_DOCS_MARKER = "<project-docs>";
|
|
32188
|
+
var USER_PROFILE_MARKER = "<user-profile>";
|
|
32189
|
+
var cachedUserProfileBySession = new Map;
|
|
31341
32190
|
var DOC_FILES = ["ARCHITECTURE.md", "STRUCTURE.md"];
|
|
31342
32191
|
function readProjectDocs(directory) {
|
|
31343
32192
|
const sections = [];
|
|
31344
32193
|
for (const filename of DOC_FILES) {
|
|
31345
|
-
const filePath =
|
|
32194
|
+
const filePath = join12(directory, filename);
|
|
31346
32195
|
try {
|
|
31347
32196
|
if (existsSync5(filePath)) {
|
|
31348
32197
|
const content = readFileSync4(filePath, "utf-8").trim();
|
|
@@ -31397,6 +32246,28 @@ function createSystemPromptHashHandler(deps) {
|
|
|
31397
32246
|
output.system.push(docsBlock);
|
|
31398
32247
|
}
|
|
31399
32248
|
}
|
|
32249
|
+
if (deps.experimentalUserMemories) {
|
|
32250
|
+
const hasCachedProfile = cachedUserProfileBySession.has(sessionId);
|
|
32251
|
+
if (!hasCachedProfile || isCacheBusting) {
|
|
32252
|
+
const memories = getActiveUserMemories(deps.db);
|
|
32253
|
+
if (memories.length > 0) {
|
|
32254
|
+
const items = memories.map((m) => `- ${m.content}`).join(`
|
|
32255
|
+
`);
|
|
32256
|
+
cachedUserProfileBySession.set(sessionId, `${USER_PROFILE_MARKER}
|
|
32257
|
+
${items}
|
|
32258
|
+
</user-profile>`);
|
|
32259
|
+
if (!hasCachedProfile) {
|
|
32260
|
+
sessionLog(sessionId, `loaded ${memories.length} user profile memorie(s)`);
|
|
32261
|
+
}
|
|
32262
|
+
} else {
|
|
32263
|
+
cachedUserProfileBySession.set(sessionId, null);
|
|
32264
|
+
}
|
|
32265
|
+
}
|
|
32266
|
+
const profileBlock = cachedUserProfileBySession.get(sessionId);
|
|
32267
|
+
if (profileBlock && !fullPrompt.includes(USER_PROFILE_MARKER)) {
|
|
32268
|
+
output.system.push(profileBlock);
|
|
32269
|
+
}
|
|
32270
|
+
}
|
|
31400
32271
|
const DATE_PATTERN = /Today's date: .+/;
|
|
31401
32272
|
for (let i = 0;i < output.system.length; i++) {
|
|
31402
32273
|
const match = output.system[i].match(DATE_PATTERN);
|
|
@@ -31530,7 +32401,9 @@ function createMagicContextHook(deps) {
|
|
|
31530
32401
|
const model = liveModelBySession.get(sessionId);
|
|
31531
32402
|
return resolveModelKey(model?.providerID, model?.modelID);
|
|
31532
32403
|
},
|
|
31533
|
-
projectPath
|
|
32404
|
+
projectPath,
|
|
32405
|
+
experimentalCompactionMarkers: deps.config.experimental?.compaction_markers,
|
|
32406
|
+
experimentalUserMemories: deps.config.experimental?.user_memories?.enabled
|
|
31534
32407
|
});
|
|
31535
32408
|
const eventHandler = createEventHandler2({
|
|
31536
32409
|
contextUsageMap,
|
|
@@ -31565,7 +32438,11 @@ function createMagicContextHook(deps) {
|
|
|
31565
32438
|
client: deps.client,
|
|
31566
32439
|
tasks: dreaming.tasks,
|
|
31567
32440
|
taskTimeoutMinutes: dreaming.task_timeout_minutes,
|
|
31568
|
-
maxRuntimeMinutes: dreaming.max_runtime_minutes
|
|
32441
|
+
maxRuntimeMinutes: dreaming.max_runtime_minutes,
|
|
32442
|
+
experimentalUserMemories: deps.config.experimental?.user_memories?.enabled ? {
|
|
32443
|
+
enabled: true,
|
|
32444
|
+
promotionThreshold: deps.config.experimental.user_memories?.promotion_threshold
|
|
32445
|
+
} : undefined
|
|
31569
32446
|
}).catch((error48) => {
|
|
31570
32447
|
log("[dreamer] scheduled queue processing failed:", error48);
|
|
31571
32448
|
});
|
|
@@ -31607,7 +32484,11 @@ function createMagicContextHook(deps) {
|
|
|
31607
32484
|
config: deps.config.dreamer,
|
|
31608
32485
|
projectPath,
|
|
31609
32486
|
client: deps.client,
|
|
31610
|
-
directory: deps.directory
|
|
32487
|
+
directory: deps.directory,
|
|
32488
|
+
experimentalUserMemories: deps.config.experimental?.user_memories?.enabled ? {
|
|
32489
|
+
enabled: true,
|
|
32490
|
+
promotionThreshold: deps.config.experimental.user_memories?.promotion_threshold
|
|
32491
|
+
} : undefined
|
|
31611
32492
|
} : undefined
|
|
31612
32493
|
});
|
|
31613
32494
|
const emergencyNudgeFired = new Set;
|
|
@@ -31619,7 +32500,8 @@ function createMagicContextHook(deps) {
|
|
|
31619
32500
|
injectDocs: deps.config.dreamer?.inject_docs !== false,
|
|
31620
32501
|
directory: deps.directory,
|
|
31621
32502
|
flushedSessions,
|
|
31622
|
-
lastHeuristicsTurnId
|
|
32503
|
+
lastHeuristicsTurnId,
|
|
32504
|
+
experimentalUserMemories: deps.config.experimental?.user_memories?.enabled
|
|
31623
32505
|
});
|
|
31624
32506
|
const eventHook = createEventHook({
|
|
31625
32507
|
eventHandler,
|
|
@@ -31696,7 +32578,8 @@ function createSessionHooks(args) {
|
|
|
31696
32578
|
historian_timeout_ms: pluginConfig.historian_timeout_ms,
|
|
31697
32579
|
memory: pluginConfig.memory,
|
|
31698
32580
|
sidekick: pluginConfig.sidekick,
|
|
31699
|
-
dreamer: pluginConfig.dreamer
|
|
32581
|
+
dreamer: pluginConfig.dreamer,
|
|
32582
|
+
experimental: pluginConfig.experimental
|
|
31700
32583
|
}
|
|
31701
32584
|
})
|
|
31702
32585
|
};
|
|
@@ -32079,9 +32962,9 @@ Use this for short goals, constraints, decisions, or reminders worth carrying fo
|
|
|
32079
32962
|
|
|
32080
32963
|
Actions:
|
|
32081
32964
|
- \`write\`: Append one note. Optionally provide \`surface_condition\` to create a smart note.
|
|
32082
|
-
- \`read\`: Show current notes
|
|
32083
|
-
- \`
|
|
32084
|
-
- \`
|
|
32965
|
+
- \`read\`: Show current notes. Defaults to active session notes + ready smart notes; use \`filter\` to inspect all, pending, ready, active, or dismissed notes.
|
|
32966
|
+
- \`dismiss\`: Dismiss a note by \`note_id\`.
|
|
32967
|
+
- \`update\`: Update a note by \`note_id\`.
|
|
32085
32968
|
|
|
32086
32969
|
**Smart Notes**: When \`surface_condition\` is provided with \`write\`, the note becomes a project-scoped smart note.
|
|
32087
32970
|
The dreamer evaluates smart note conditions during nightly runs and surfaces them when conditions are met.
|
|
@@ -32090,14 +32973,79 @@ Example: \`ctx_note(action="write", content="Implement X because Y", surface_con
|
|
|
32090
32973
|
Historian reads these notes, deduplicates them, and rewrites the remaining useful notes over time.`;
|
|
32091
32974
|
// src/tools/ctx-note/tools.ts
|
|
32092
32975
|
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
32976
|
+
function formatNoteLine(note) {
|
|
32977
|
+
const statusSuffix = note.status === "active" ? "" : ` (${note.status})`;
|
|
32978
|
+
const dismissHint = note.status === "dismissed" ? "" : ` _(dismiss with \`ctx_note(action="dismiss", note_id=${note.id})\`)_`;
|
|
32979
|
+
if (note.type === "session") {
|
|
32980
|
+
return `- **#${note.id}**${statusSuffix}: ${note.content}${dismissHint}`;
|
|
32981
|
+
}
|
|
32982
|
+
const conditionText = note.status === "ready" ? note.readyReason ?? note.surfaceCondition ?? "Condition satisfied" : note.surfaceCondition ?? "No condition recorded";
|
|
32983
|
+
const conditionLabel = note.status === "ready" ? "Condition met" : "Condition";
|
|
32984
|
+
return `- **#${note.id}**${statusSuffix}: ${note.content}
|
|
32985
|
+
${conditionLabel}: ${conditionText}${dismissHint}`;
|
|
32986
|
+
}
|
|
32987
|
+
function buildReadSections(args) {
|
|
32988
|
+
if (args.filter === undefined) {
|
|
32989
|
+
const sessionNotes2 = getSessionNotes(args.db, args.sessionId);
|
|
32990
|
+
const readySmartNotes = args.projectIdentity ? getReadySmartNotes(args.db, args.projectIdentity) : [];
|
|
32991
|
+
const sections2 = [];
|
|
32992
|
+
if (sessionNotes2.length > 0) {
|
|
32993
|
+
sections2.push(`## Session Notes
|
|
32994
|
+
|
|
32995
|
+
${sessionNotes2.map((note) => formatNoteLine(note)).join(`
|
|
32996
|
+
`)}`);
|
|
32997
|
+
}
|
|
32998
|
+
if (readySmartNotes.length > 0) {
|
|
32999
|
+
sections2.push(`## \uD83D\uDD14 Ready Smart Notes
|
|
33000
|
+
|
|
33001
|
+
${readySmartNotes.map((note) => formatNoteLine(note)).join(`
|
|
33002
|
+
|
|
33003
|
+
`)}`);
|
|
33004
|
+
}
|
|
33005
|
+
return sections2;
|
|
33006
|
+
}
|
|
33007
|
+
const statusByFilter = {
|
|
33008
|
+
active: "active",
|
|
33009
|
+
all: ["active", "pending", "ready", "dismissed"],
|
|
33010
|
+
dismissed: "dismissed",
|
|
33011
|
+
pending: "pending",
|
|
33012
|
+
ready: "ready"
|
|
33013
|
+
};
|
|
33014
|
+
const sessionNotes = getNotes(args.db, {
|
|
33015
|
+
sessionId: args.sessionId,
|
|
33016
|
+
type: "session",
|
|
33017
|
+
status: statusByFilter[args.filter]
|
|
33018
|
+
});
|
|
33019
|
+
const smartNotes = args.projectIdentity ? getNotes(args.db, {
|
|
33020
|
+
projectPath: args.projectIdentity,
|
|
33021
|
+
type: "smart",
|
|
33022
|
+
status: statusByFilter[args.filter]
|
|
33023
|
+
}) : [];
|
|
33024
|
+
const sections = [];
|
|
33025
|
+
if (sessionNotes.length > 0) {
|
|
33026
|
+
sections.push(`## Session Notes
|
|
33027
|
+
|
|
33028
|
+
${sessionNotes.map((note) => formatNoteLine(note)).join(`
|
|
33029
|
+
`)}`);
|
|
33030
|
+
}
|
|
33031
|
+
if (smartNotes.length > 0) {
|
|
33032
|
+
sections.push(`## Smart Notes
|
|
33033
|
+
|
|
33034
|
+
${smartNotes.map((note) => formatNoteLine(note)).join(`
|
|
33035
|
+
|
|
33036
|
+
`)}`);
|
|
33037
|
+
}
|
|
33038
|
+
return sections;
|
|
33039
|
+
}
|
|
32093
33040
|
function createCtxNoteTool(deps) {
|
|
32094
33041
|
return tool3({
|
|
32095
33042
|
description: CTX_NOTE_DESCRIPTION,
|
|
32096
33043
|
args: {
|
|
32097
|
-
action: tool3.schema.enum(["write", "read", "
|
|
33044
|
+
action: tool3.schema.enum(["write", "read", "dismiss", "update"]).optional().describe("Operation to perform. Defaults to 'write' when content is provided, otherwise 'read'."),
|
|
32098
33045
|
content: tool3.schema.string().optional().describe("Note text to store when action is 'write'."),
|
|
32099
33046
|
surface_condition: tool3.schema.string().optional().describe("Open-ended condition for smart notes. When provided, creates a project-scoped smart note that the dreamer evaluates nightly. The note surfaces when the condition is met."),
|
|
32100
|
-
|
|
33047
|
+
filter: tool3.schema.enum(["all", "active", "pending", "ready", "dismissed"]).optional().describe("Optional read filter. Defaults to active session notes + ready smart notes. Use 'all' to inspect every status or 'pending' to inspect unsurfaced smart notes."),
|
|
33048
|
+
note_id: tool3.schema.number().optional().describe("Note ID (required for 'dismiss' and 'update' actions).")
|
|
32101
33049
|
},
|
|
32102
33050
|
async execute(args, toolContext) {
|
|
32103
33051
|
const sessionId = toolContext.sessionID;
|
|
@@ -32114,48 +33062,59 @@ function createCtxNoteTool(deps) {
|
|
|
32114
33062
|
if (!deps.projectIdentity) {
|
|
32115
33063
|
return "Error: Could not resolve project identity for smart note.";
|
|
32116
33064
|
}
|
|
32117
|
-
const
|
|
32118
|
-
|
|
33065
|
+
const note2 = addNote(deps.db, "smart", {
|
|
33066
|
+
content,
|
|
33067
|
+
projectPath: deps.projectIdentity,
|
|
33068
|
+
sessionId,
|
|
33069
|
+
surfaceCondition: args.surface_condition.trim()
|
|
33070
|
+
});
|
|
33071
|
+
return `Created smart note #${note2.id}. Dreamer will evaluate the condition during nightly runs:
|
|
32119
33072
|
- Content: ${content}
|
|
32120
33073
|
- Condition: ${args.surface_condition.trim()}`;
|
|
32121
33074
|
}
|
|
32122
|
-
|
|
32123
|
-
|
|
32124
|
-
return `Saved session note ${total}. Historian will rewrite or deduplicate notes as needed.`;
|
|
33075
|
+
const note = addNote(deps.db, "session", { sessionId, content });
|
|
33076
|
+
return `Saved session note #${note.id}. Historian will rewrite or deduplicate notes as needed.`;
|
|
32125
33077
|
}
|
|
32126
33078
|
if (action === "dismiss") {
|
|
32127
33079
|
const noteId = args.note_id;
|
|
32128
33080
|
if (typeof noteId !== "number") {
|
|
32129
33081
|
return "Error: 'note_id' is required when action is 'dismiss'.";
|
|
32130
33082
|
}
|
|
32131
|
-
const dismissed =
|
|
32132
|
-
return dismissed ? `
|
|
32133
|
-
}
|
|
32134
|
-
if (action === "clear") {
|
|
32135
|
-
const existing = getSessionNotes(deps.db, sessionId);
|
|
32136
|
-
clearSessionNotes(deps.db, sessionId);
|
|
32137
|
-
return existing.length === 0 ? "Session notes were already empty." : `Cleared ${existing.length} session note${existing.length === 1 ? "" : "s"}.`;
|
|
32138
|
-
}
|
|
32139
|
-
const notes = getSessionNotes(deps.db, sessionId);
|
|
32140
|
-
const readySmartNotes = deps.projectIdentity ? getReadySmartNotes(deps.db, deps.projectIdentity) : [];
|
|
32141
|
-
const sections = [];
|
|
32142
|
-
if (notes.length > 0) {
|
|
32143
|
-
const lines = notes.map((note, index) => `${index + 1}. ${note.content}`);
|
|
32144
|
-
sections.push(`## Session Notes
|
|
32145
|
-
|
|
32146
|
-
${lines.join(`
|
|
32147
|
-
`)}`);
|
|
32148
|
-
}
|
|
32149
|
-
if (readySmartNotes.length > 0) {
|
|
32150
|
-
const lines = readySmartNotes.map((n) => `- **#${n.id}**: ${n.content}
|
|
32151
|
-
Condition met: ${n.readyReason ?? n.surfaceCondition}
|
|
32152
|
-
_(dismiss with \`ctx_note(action="dismiss", note_id=${n.id})\`)_`);
|
|
32153
|
-
sections.push(`## \uD83D\uDD14 Ready Smart Notes
|
|
32154
|
-
|
|
32155
|
-
${lines.join(`
|
|
32156
|
-
|
|
32157
|
-
`)}`);
|
|
33083
|
+
const dismissed = dismissNote(deps.db, noteId);
|
|
33084
|
+
return dismissed ? `Note #${noteId} dismissed.` : `Note #${noteId} not found or already dismissed.`;
|
|
32158
33085
|
}
|
|
33086
|
+
if (action === "update") {
|
|
33087
|
+
const noteId = args.note_id;
|
|
33088
|
+
if (typeof noteId !== "number") {
|
|
33089
|
+
return "Error: 'note_id' is required when action is 'update'.";
|
|
33090
|
+
}
|
|
33091
|
+
const updates = {};
|
|
33092
|
+
if (args.content?.trim())
|
|
33093
|
+
updates.content = args.content.trim();
|
|
33094
|
+
if (args.surface_condition?.trim())
|
|
33095
|
+
updates.surfaceCondition = args.surface_condition.trim();
|
|
33096
|
+
if (!updates.content && !updates.surfaceCondition) {
|
|
33097
|
+
return "Error: Provide 'content' and/or 'surface_condition' to update.";
|
|
33098
|
+
}
|
|
33099
|
+
const updated = updateNote(deps.db, noteId, updates);
|
|
33100
|
+
if (!updated) {
|
|
33101
|
+
return `Note #${noteId} not found or has no compatible fields to update.`;
|
|
33102
|
+
}
|
|
33103
|
+
const parts = [];
|
|
33104
|
+
if (updates.content)
|
|
33105
|
+
parts.push(`Content: ${updates.content}`);
|
|
33106
|
+
if (updates.surfaceCondition)
|
|
33107
|
+
parts.push(`Condition: ${updates.surfaceCondition}`);
|
|
33108
|
+
return `Updated note #${noteId}:
|
|
33109
|
+
${parts.join(`
|
|
33110
|
+
`)}`;
|
|
33111
|
+
}
|
|
33112
|
+
const sections = buildReadSections({
|
|
33113
|
+
db: deps.db,
|
|
33114
|
+
filter: args.filter,
|
|
33115
|
+
projectIdentity: deps.projectIdentity,
|
|
33116
|
+
sessionId
|
|
33117
|
+
});
|
|
32159
33118
|
if (sections.length === 0) {
|
|
32160
33119
|
return `## Notes
|
|
32161
33120
|
|
|
@@ -32789,6 +33748,118 @@ function createToolRegistry(args) {
|
|
|
32789
33748
|
return allTools;
|
|
32790
33749
|
}
|
|
32791
33750
|
|
|
33751
|
+
// src/features/magic-context/plugin-messages.ts
|
|
33752
|
+
function isPluginMessageRow(row) {
|
|
33753
|
+
if (row === null || typeof row !== "object")
|
|
33754
|
+
return false;
|
|
33755
|
+
const r = row;
|
|
33756
|
+
return typeof r.id === "number" && typeof r.direction === "string" && typeof r.type === "string" && typeof r.payload === "string" && typeof r.created_at === "number";
|
|
33757
|
+
}
|
|
33758
|
+
function toPluginMessage(row) {
|
|
33759
|
+
let payload = {};
|
|
33760
|
+
try {
|
|
33761
|
+
payload = JSON.parse(row.payload);
|
|
33762
|
+
} catch {}
|
|
33763
|
+
return {
|
|
33764
|
+
id: row.id,
|
|
33765
|
+
direction: row.direction,
|
|
33766
|
+
type: row.type,
|
|
33767
|
+
payload,
|
|
33768
|
+
sessionId: row.session_id,
|
|
33769
|
+
createdAt: row.created_at,
|
|
33770
|
+
consumedAt: row.consumed_at
|
|
33771
|
+
};
|
|
33772
|
+
}
|
|
33773
|
+
var CLEANUP_THRESHOLD_MS = 5 * 60 * 1000;
|
|
33774
|
+
function sendToTui(db, type, payload, sessionId) {
|
|
33775
|
+
const result = db.prepare("INSERT INTO plugin_messages (direction, type, payload, session_id, created_at) VALUES (?, ?, ?, ?, ?)").run("server_to_tui", type, JSON.stringify(payload), sessionId ?? null, Date.now());
|
|
33776
|
+
return Number(result.lastInsertRowid);
|
|
33777
|
+
}
|
|
33778
|
+
function consumeMessages(db, direction, options) {
|
|
33779
|
+
const now = Date.now();
|
|
33780
|
+
const conditions = ["direction = ?", "consumed_at IS NULL"];
|
|
33781
|
+
const params = [direction];
|
|
33782
|
+
if (options?.type) {
|
|
33783
|
+
conditions.push("type = ?");
|
|
33784
|
+
params.push(options.type);
|
|
33785
|
+
}
|
|
33786
|
+
if (options?.sessionId) {
|
|
33787
|
+
conditions.push("session_id = ?");
|
|
33788
|
+
params.push(options.sessionId);
|
|
33789
|
+
}
|
|
33790
|
+
const query = `SELECT * FROM plugin_messages WHERE ${conditions.join(" AND ")} ORDER BY created_at ASC`;
|
|
33791
|
+
const messages = db.transaction(() => {
|
|
33792
|
+
const rows = db.prepare(query).all(...params);
|
|
33793
|
+
const result = rows.filter(isPluginMessageRow).map(toPluginMessage);
|
|
33794
|
+
if (result.length > 0) {
|
|
33795
|
+
const ids = result.map((m) => m.id);
|
|
33796
|
+
db.prepare(`UPDATE plugin_messages SET consumed_at = ? WHERE id IN (${ids.map(() => "?").join(",")})`).run(now, ...ids);
|
|
33797
|
+
}
|
|
33798
|
+
return result;
|
|
33799
|
+
})();
|
|
33800
|
+
db.prepare("DELETE FROM plugin_messages WHERE created_at < ?").run(now - CLEANUP_THRESHOLD_MS);
|
|
33801
|
+
return messages;
|
|
33802
|
+
}
|
|
33803
|
+
function sendTuiToast(db, message, options) {
|
|
33804
|
+
return sendToTui(db, "toast", {
|
|
33805
|
+
message,
|
|
33806
|
+
variant: options?.variant ?? "info",
|
|
33807
|
+
duration: options?.duration ?? 5000
|
|
33808
|
+
}, options?.sessionId);
|
|
33809
|
+
}
|
|
33810
|
+
|
|
33811
|
+
// src/plugin/tui-action-consumer.ts
|
|
33812
|
+
init_logger();
|
|
33813
|
+
var DEFAULT_COMPARTMENT_TOKEN_BUDGET2 = 20000;
|
|
33814
|
+
var DEFAULT_HISTORIAN_TIMEOUT_MS2 = 10 * 60 * 1000;
|
|
33815
|
+
var TUI_ACTION_POLL_INTERVAL_MS = 2000;
|
|
33816
|
+
function startTuiActionConsumer(args) {
|
|
33817
|
+
const { client, directory, config: config2 } = args;
|
|
33818
|
+
const timer = setInterval(() => {
|
|
33819
|
+
try {
|
|
33820
|
+
const db = openDatabase();
|
|
33821
|
+
const actions = consumeMessages(db, "tui_to_server", { type: "action" });
|
|
33822
|
+
for (const msg of actions) {
|
|
33823
|
+
const command = msg.payload.command;
|
|
33824
|
+
const sessionId = msg.sessionId;
|
|
33825
|
+
if (command === "recomp" && sessionId) {
|
|
33826
|
+
log(`[magic-context] TUI action: recomp requested for session ${sessionId}`);
|
|
33827
|
+
sendTuiToast(db, "Historian recomp started", {
|
|
33828
|
+
variant: "info",
|
|
33829
|
+
sessionId
|
|
33830
|
+
});
|
|
33831
|
+
executeContextRecomp({
|
|
33832
|
+
client,
|
|
33833
|
+
db,
|
|
33834
|
+
sessionId,
|
|
33835
|
+
tokenBudget: config2.compartment_token_budget ?? DEFAULT_COMPARTMENT_TOKEN_BUDGET2,
|
|
33836
|
+
historianTimeoutMs: config2.historian_timeout_ms ?? DEFAULT_HISTORIAN_TIMEOUT_MS2,
|
|
33837
|
+
directory,
|
|
33838
|
+
getNotificationParams: () => ({})
|
|
33839
|
+
}).then((result) => {
|
|
33840
|
+
sendTuiToast(db, "Recomp completed", { variant: "success", sessionId });
|
|
33841
|
+
sendIgnoredMessage(client, sessionId, result, {}).catch(() => {});
|
|
33842
|
+
}).catch((error48) => {
|
|
33843
|
+
log("[magic-context] TUI recomp failed:", error48);
|
|
33844
|
+
sendTuiToast(db, `Recomp failed: ${error48 instanceof Error ? error48.message : "unknown error"}`, { variant: "error", sessionId });
|
|
33845
|
+
});
|
|
33846
|
+
} else {
|
|
33847
|
+
log(`[magic-context] TUI action: unknown command=${String(command)} session=${String(sessionId)}`);
|
|
33848
|
+
}
|
|
33849
|
+
}
|
|
33850
|
+
} catch (error48) {
|
|
33851
|
+
log("[magic-context] TUI action consumer error:", error48);
|
|
33852
|
+
}
|
|
33853
|
+
}, TUI_ACTION_POLL_INTERVAL_MS);
|
|
33854
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
33855
|
+
timer.unref();
|
|
33856
|
+
}
|
|
33857
|
+
log("[magic-context] started TUI action consumer (2s poll)");
|
|
33858
|
+
return () => {
|
|
33859
|
+
clearInterval(timer);
|
|
33860
|
+
};
|
|
33861
|
+
}
|
|
33862
|
+
|
|
32792
33863
|
// src/index.ts
|
|
32793
33864
|
init_conflict_detector();
|
|
32794
33865
|
init_logger();
|
|
@@ -32818,7 +33889,16 @@ var plugin = async (ctx) => {
|
|
|
32818
33889
|
client: ctx.client,
|
|
32819
33890
|
dreamerConfig: pluginConfig.dreamer,
|
|
32820
33891
|
embeddingConfig: pluginConfig.embedding,
|
|
32821
|
-
memoryEnabled: pluginConfig.memory?.enabled === true
|
|
33892
|
+
memoryEnabled: pluginConfig.memory?.enabled === true,
|
|
33893
|
+
experimentalUserMemories: pluginConfig.experimental?.user_memories?.enabled ? {
|
|
33894
|
+
enabled: true,
|
|
33895
|
+
promotionThreshold: pluginConfig.experimental.user_memories?.promotion_threshold
|
|
33896
|
+
} : undefined
|
|
33897
|
+
});
|
|
33898
|
+
startTuiActionConsumer({
|
|
33899
|
+
client: ctx.client,
|
|
33900
|
+
directory: ctx.directory,
|
|
33901
|
+
config: pluginConfig
|
|
32822
33902
|
});
|
|
32823
33903
|
}
|
|
32824
33904
|
if (conflictResult?.hasConflict) {
|
|
@@ -32898,7 +33978,7 @@ var plugin = async (ctx) => {
|
|
|
32898
33978
|
config2.agent = {
|
|
32899
33979
|
...config2.agent ?? {},
|
|
32900
33980
|
[DREAMER_AGENT]: buildHiddenAgentConfig(DREAMER_AGENT, DREAMER_SYSTEM_PROMPT, dreamerAgentOverrides),
|
|
32901
|
-
[HISTORIAN_AGENT]: buildHiddenAgentConfig(HISTORIAN_AGENT, COMPARTMENT_AGENT_SYSTEM_PROMPT, pluginConfig.historian),
|
|
33981
|
+
[HISTORIAN_AGENT]: buildHiddenAgentConfig(HISTORIAN_AGENT, pluginConfig.experimental?.user_memories?.enabled ? COMPARTMENT_AGENT_SYSTEM_PROMPT + USER_OBSERVATIONS_APPENDIX : COMPARTMENT_AGENT_SYSTEM_PROMPT, pluginConfig.historian),
|
|
32902
33982
|
[SIDEKICK_AGENT]: buildHiddenAgentConfig(SIDEKICK_AGENT, SIDEKICK_SYSTEM_PROMPT, sidekickAgentOverrides)
|
|
32903
33983
|
};
|
|
32904
33984
|
}
|