@cortexkit/opencode-magic-context 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli.js +23 -6
- 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/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-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 +1231 -151
- 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/shared/tui-config.d.ts.map +1 -1
- 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 +2 -1
- package/src/shared/assistant-message-extractor.ts +74 -0
- package/src/shared/conflict-detector.ts +310 -0
- package/src/shared/conflict-fixer.ts +216 -0
- package/src/shared/data-path.ts +10 -0
- package/src/shared/error-message.ts +3 -0
- package/src/shared/format-bytes.ts +5 -0
- package/src/shared/index.ts +4 -0
- package/src/shared/internal-initiator-marker.ts +1 -0
- package/src/shared/jsonc-parser.ts +138 -0
- package/src/shared/logger.ts +63 -0
- package/src/shared/model-requirements.ts +84 -0
- package/src/shared/model-suggestion-retry.ts +160 -0
- package/src/shared/normalize-sdk-response.ts +40 -0
- package/src/shared/opencode-compaction-detector.test.ts +222 -0
- package/src/shared/opencode-compaction-detector.ts +81 -0
- package/src/shared/opencode-config-dir-types.ts +15 -0
- package/src/shared/opencode-config-dir.ts +38 -0
- package/src/shared/record-type-guard.ts +3 -0
- package/src/shared/system-directive.ts +9 -0
- package/src/shared/tui-config.ts +60 -0
- 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))
|
|
@@ -8502,7 +8502,7 @@ function ensureTuiPluginEntry() {
|
|
|
8502
8502
|
if (plugins.some((p) => p === PLUGIN_NAME || p.startsWith(`${PLUGIN_NAME}@`))) {
|
|
8503
8503
|
return false;
|
|
8504
8504
|
}
|
|
8505
|
-
plugins.push(
|
|
8505
|
+
plugins.push(PLUGIN_ENTRY);
|
|
8506
8506
|
config2.plugin = plugins;
|
|
8507
8507
|
mkdirSync3(dirname(configPath), { recursive: true });
|
|
8508
8508
|
writeFileSync2(configPath, `${import_comment_json.stringify(config2, null, 2)}
|
|
@@ -8514,11 +8514,12 @@ function ensureTuiPluginEntry() {
|
|
|
8514
8514
|
return false;
|
|
8515
8515
|
}
|
|
8516
8516
|
}
|
|
8517
|
-
var import_comment_json, PLUGIN_NAME = "@cortexkit/opencode-magic-context";
|
|
8517
|
+
var import_comment_json, PLUGIN_NAME = "@cortexkit/opencode-magic-context", PLUGIN_ENTRY;
|
|
8518
8518
|
var init_tui_config = __esm(() => {
|
|
8519
8519
|
init_logger();
|
|
8520
8520
|
init_opencode_config_dir();
|
|
8521
8521
|
import_comment_json = __toESM(require_src2(), 1);
|
|
8522
|
+
PLUGIN_ENTRY = `${PLUGIN_NAME}@latest`;
|
|
8522
8523
|
});
|
|
8523
8524
|
|
|
8524
8525
|
// src/agents/dreamer.ts
|
|
@@ -22196,6 +22197,16 @@ var MagicContextConfigSchema = exports_external.object({
|
|
|
22196
22197
|
provider: "local",
|
|
22197
22198
|
model: DEFAULT_LOCAL_EMBEDDING_MODEL
|
|
22198
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
|
+
}),
|
|
22199
22210
|
memory: exports_external.object({
|
|
22200
22211
|
enabled: exports_external.boolean().default(true),
|
|
22201
22212
|
injection_budget_tokens: exports_external.number().min(500).max(20000).default(4000),
|
|
@@ -23261,6 +23272,22 @@ More summary text.
|
|
|
23261
23272
|
</output>
|
|
23262
23273
|
|
|
23263
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.`;
|
|
23264
23291
|
function buildCompressorPrompt(compartments, currentTokens, targetTokens, averageDepth = 0) {
|
|
23265
23292
|
const lines = [];
|
|
23266
23293
|
lines.push(`These ${compartments.length} compartments use approximately ${currentTokens} tokens. Compress them to approximately ${targetTokens} tokens.`);
|
|
@@ -23289,7 +23316,7 @@ function buildCompressorPrompt(compartments, currentTokens, targetTokens, averag
|
|
|
23289
23316
|
return lines.join(`
|
|
23290
23317
|
`);
|
|
23291
23318
|
}
|
|
23292
|
-
function buildCompartmentAgentPrompt(existingState, inputSource) {
|
|
23319
|
+
function buildCompartmentAgentPrompt(existingState, inputSource, options) {
|
|
23293
23320
|
return [
|
|
23294
23321
|
"Existing state (read-only context for continuity and fact normalization \u2014 do NOT re-emit these compartments):",
|
|
23295
23322
|
existingState,
|
|
@@ -23304,7 +23331,10 @@ function buildCompartmentAgentPrompt(existingState, inputSource) {
|
|
|
23304
23331
|
"- Rewrite every fact into terse, present-tense operational form. Merge semantic duplicates within each category.",
|
|
23305
23332
|
"- Drop any session fact already covered by a project memory in the existing state.",
|
|
23306
23333
|
"- Do not preserve prior narrative wording verbatim; if a fact is already canonical and still correct, keep or lightly normalize it.",
|
|
23307
|
-
"- 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
|
+
] : []
|
|
23308
23338
|
].join(`
|
|
23309
23339
|
`);
|
|
23310
23340
|
}
|
|
@@ -23954,40 +23984,96 @@ function getMemoryCountsByStatus(db, projectPath) {
|
|
|
23954
23984
|
return counts;
|
|
23955
23985
|
}
|
|
23956
23986
|
|
|
23957
|
-
// src/features/magic-context/storage-
|
|
23958
|
-
|
|
23987
|
+
// src/features/magic-context/storage-notes.ts
|
|
23988
|
+
var NOTE_TYPES = new Set(["session", "smart"]);
|
|
23989
|
+
var NOTE_STATUSES = new Set(["active", "pending", "ready", "dismissed"]);
|
|
23990
|
+
var DEFAULT_SMART_STATUSES = ["pending", "ready"];
|
|
23991
|
+
function toNullableString(value) {
|
|
23992
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
23993
|
+
}
|
|
23994
|
+
function toNullableNumber(value) {
|
|
23995
|
+
return typeof value === "number" ? value : null;
|
|
23996
|
+
}
|
|
23997
|
+
function isNoteRow(row) {
|
|
23959
23998
|
if (row === null || typeof row !== "object")
|
|
23960
23999
|
return false;
|
|
23961
|
-
const
|
|
23962
|
-
return typeof
|
|
24000
|
+
const candidate = row;
|
|
24001
|
+
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");
|
|
23963
24002
|
}
|
|
23964
|
-
function
|
|
24003
|
+
function toNote(row) {
|
|
23965
24004
|
return {
|
|
23966
24005
|
id: row.id,
|
|
23967
|
-
|
|
23968
|
-
content: row.content,
|
|
23969
|
-
surfaceCondition: row.surface_condition,
|
|
24006
|
+
type: row.type,
|
|
23970
24007
|
status: row.status,
|
|
23971
|
-
|
|
24008
|
+
content: row.content,
|
|
24009
|
+
sessionId: toNullableString(row.session_id),
|
|
24010
|
+
projectPath: toNullableString(row.project_path),
|
|
24011
|
+
surfaceCondition: toNullableString(row.surface_condition),
|
|
23972
24012
|
createdAt: row.created_at,
|
|
23973
24013
|
updatedAt: row.updated_at,
|
|
23974
|
-
lastCheckedAt: row.last_checked_at,
|
|
23975
|
-
readyAt: row.ready_at,
|
|
23976
|
-
readyReason: row.ready_reason
|
|
24014
|
+
lastCheckedAt: toNullableNumber(row.last_checked_at),
|
|
24015
|
+
readyAt: toNullableNumber(row.ready_at),
|
|
24016
|
+
readyReason: toNullableString(row.ready_reason)
|
|
23977
24017
|
};
|
|
23978
24018
|
}
|
|
23979
|
-
function
|
|
24019
|
+
function getNoteById(db, noteId) {
|
|
24020
|
+
const row = db.prepare("SELECT * FROM notes WHERE id = ?").get(noteId);
|
|
24021
|
+
return isNoteRow(row) ? toNote(row) : null;
|
|
24022
|
+
}
|
|
24023
|
+
function buildStatusClause(status) {
|
|
24024
|
+
if (status === undefined) {
|
|
24025
|
+
return null;
|
|
24026
|
+
}
|
|
24027
|
+
const statuses = Array.isArray(status) ? status : [status];
|
|
24028
|
+
if (statuses.length === 0) {
|
|
24029
|
+
return null;
|
|
24030
|
+
}
|
|
24031
|
+
const placeholders = statuses.map(() => "?").join(", ");
|
|
24032
|
+
return {
|
|
24033
|
+
sql: `status IN (${placeholders})`,
|
|
24034
|
+
params: statuses
|
|
24035
|
+
};
|
|
24036
|
+
}
|
|
24037
|
+
function getNotes(db, options = {}) {
|
|
24038
|
+
const clauses = [];
|
|
24039
|
+
const params = [];
|
|
24040
|
+
if (options.sessionId !== undefined) {
|
|
24041
|
+
clauses.push("session_id = ?");
|
|
24042
|
+
params.push(options.sessionId);
|
|
24043
|
+
}
|
|
24044
|
+
if (options.projectPath !== undefined) {
|
|
24045
|
+
clauses.push("project_path = ?");
|
|
24046
|
+
params.push(options.projectPath);
|
|
24047
|
+
}
|
|
24048
|
+
if (options.type !== undefined) {
|
|
24049
|
+
clauses.push("type = ?");
|
|
24050
|
+
params.push(options.type);
|
|
24051
|
+
}
|
|
24052
|
+
const statusClause = buildStatusClause(options.status);
|
|
24053
|
+
if (statusClause) {
|
|
24054
|
+
clauses.push(statusClause.sql);
|
|
24055
|
+
params.push(...statusClause.params);
|
|
24056
|
+
}
|
|
24057
|
+
const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
24058
|
+
return db.prepare(`SELECT * FROM notes${where} ORDER BY created_at ASC, id ASC`).all(...params).filter(isNoteRow).map(toNote);
|
|
24059
|
+
}
|
|
24060
|
+
function addNote(db, type, options) {
|
|
23980
24061
|
const now = Date.now();
|
|
23981
|
-
const result = db.prepare("INSERT INTO
|
|
23982
|
-
if (!
|
|
23983
|
-
throw new Error("[
|
|
24062
|
+
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);
|
|
24063
|
+
if (!isNoteRow(result)) {
|
|
24064
|
+
throw new Error("[notes] failed to insert note");
|
|
23984
24065
|
}
|
|
23985
|
-
return
|
|
24066
|
+
return toNote(result);
|
|
24067
|
+
}
|
|
24068
|
+
function getSessionNotes(db, sessionId) {
|
|
24069
|
+
return getNotes(db, { sessionId, type: "session", status: "active" });
|
|
23986
24070
|
}
|
|
23987
24071
|
function getSmartNotes(db, projectPath, status) {
|
|
23988
|
-
|
|
23989
|
-
|
|
23990
|
-
|
|
24072
|
+
return getNotes(db, {
|
|
24073
|
+
projectPath,
|
|
24074
|
+
type: "smart",
|
|
24075
|
+
status: status ?? DEFAULT_SMART_STATUSES
|
|
24076
|
+
});
|
|
23991
24077
|
}
|
|
23992
24078
|
function getPendingSmartNotes(db, projectPath) {
|
|
23993
24079
|
return getSmartNotes(db, projectPath, "pending");
|
|
@@ -23995,17 +24081,294 @@ function getPendingSmartNotes(db, projectPath) {
|
|
|
23995
24081
|
function getReadySmartNotes(db, projectPath) {
|
|
23996
24082
|
return getSmartNotes(db, projectPath, "ready");
|
|
23997
24083
|
}
|
|
23998
|
-
function
|
|
24084
|
+
function updateNote(db, noteId, updates) {
|
|
24085
|
+
const existing = getNoteById(db, noteId);
|
|
24086
|
+
if (!existing) {
|
|
24087
|
+
return null;
|
|
24088
|
+
}
|
|
24089
|
+
const now = Date.now();
|
|
24090
|
+
const sets = ["updated_at = ?"];
|
|
24091
|
+
const params = [now];
|
|
24092
|
+
if (updates.content !== undefined) {
|
|
24093
|
+
sets.push("content = ?");
|
|
24094
|
+
params.push(updates.content);
|
|
24095
|
+
}
|
|
24096
|
+
if (updates.sessionId !== undefined) {
|
|
24097
|
+
sets.push("session_id = ?");
|
|
24098
|
+
params.push(updates.sessionId);
|
|
24099
|
+
}
|
|
24100
|
+
if (updates.status !== undefined) {
|
|
24101
|
+
sets.push("status = ?");
|
|
24102
|
+
params.push(updates.status);
|
|
24103
|
+
}
|
|
24104
|
+
if (existing.type === "smart") {
|
|
24105
|
+
if (updates.projectPath !== undefined) {
|
|
24106
|
+
sets.push("project_path = ?");
|
|
24107
|
+
params.push(updates.projectPath);
|
|
24108
|
+
}
|
|
24109
|
+
if (updates.surfaceCondition !== undefined) {
|
|
24110
|
+
sets.push("surface_condition = ?");
|
|
24111
|
+
params.push(updates.surfaceCondition);
|
|
24112
|
+
}
|
|
24113
|
+
if (updates.lastCheckedAt !== undefined) {
|
|
24114
|
+
sets.push("last_checked_at = ?");
|
|
24115
|
+
params.push(updates.lastCheckedAt);
|
|
24116
|
+
}
|
|
24117
|
+
if (updates.readyAt !== undefined) {
|
|
24118
|
+
sets.push("ready_at = ?");
|
|
24119
|
+
params.push(updates.readyAt);
|
|
24120
|
+
}
|
|
24121
|
+
if (updates.readyReason !== undefined) {
|
|
24122
|
+
sets.push("ready_reason = ?");
|
|
24123
|
+
params.push(updates.readyReason);
|
|
24124
|
+
}
|
|
24125
|
+
}
|
|
24126
|
+
if (sets.length === 1) {
|
|
24127
|
+
return null;
|
|
24128
|
+
}
|
|
24129
|
+
params.push(noteId);
|
|
24130
|
+
const result = db.prepare(`UPDATE notes SET ${sets.join(", ")} WHERE id = ? RETURNING *`).get(...params);
|
|
24131
|
+
return isNoteRow(result) ? toNote(result) : null;
|
|
24132
|
+
}
|
|
24133
|
+
function dismissNote(db, noteId) {
|
|
24134
|
+
const result = db.prepare("UPDATE notes SET status = 'dismissed', updated_at = ? WHERE id = ? AND status != 'dismissed'").run(Date.now(), noteId);
|
|
24135
|
+
return result.changes > 0;
|
|
24136
|
+
}
|
|
24137
|
+
function markNoteReady(db, noteId, reason) {
|
|
23999
24138
|
const now = Date.now();
|
|
24000
|
-
db.prepare("UPDATE
|
|
24139
|
+
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);
|
|
24001
24140
|
}
|
|
24002
|
-
function
|
|
24141
|
+
function markNoteChecked(db, noteId) {
|
|
24003
24142
|
const now = Date.now();
|
|
24004
|
-
db.prepare("UPDATE
|
|
24143
|
+
db.prepare("UPDATE notes SET last_checked_at = ?, updated_at = ? WHERE id = ? AND type = 'smart'").run(now, now, noteId);
|
|
24005
24144
|
}
|
|
24006
|
-
|
|
24007
|
-
|
|
24008
|
-
|
|
24145
|
+
|
|
24146
|
+
// src/features/magic-context/user-memory/review-user-memories.ts
|
|
24147
|
+
init_logger();
|
|
24148
|
+
|
|
24149
|
+
// src/features/magic-context/user-memory/storage-user-memory.ts
|
|
24150
|
+
function insertUserMemoryCandidates(db, candidates) {
|
|
24151
|
+
if (candidates.length === 0)
|
|
24152
|
+
return;
|
|
24153
|
+
const now = Date.now();
|
|
24154
|
+
const stmt = db.prepare("INSERT INTO user_memory_candidates (content, session_id, source_compartment_start, source_compartment_end, created_at) VALUES (?, ?, ?, ?, ?)");
|
|
24155
|
+
db.transaction(() => {
|
|
24156
|
+
for (const c of candidates) {
|
|
24157
|
+
stmt.run(c.content, c.sessionId, c.sourceCompartmentStart ?? null, c.sourceCompartmentEnd ?? null, now);
|
|
24158
|
+
}
|
|
24159
|
+
})();
|
|
24160
|
+
}
|
|
24161
|
+
function getUserMemoryCandidates(db) {
|
|
24162
|
+
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();
|
|
24163
|
+
return rows.map((r) => ({
|
|
24164
|
+
id: r.id,
|
|
24165
|
+
content: r.content,
|
|
24166
|
+
sessionId: r.session_id,
|
|
24167
|
+
sourceCompartmentStart: r.source_compartment_start,
|
|
24168
|
+
sourceCompartmentEnd: r.source_compartment_end,
|
|
24169
|
+
createdAt: r.created_at
|
|
24170
|
+
}));
|
|
24171
|
+
}
|
|
24172
|
+
function deleteUserMemoryCandidates(db, ids) {
|
|
24173
|
+
if (ids.length === 0)
|
|
24174
|
+
return;
|
|
24175
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
24176
|
+
db.prepare(`DELETE FROM user_memory_candidates WHERE id IN (${placeholders})`).run(...ids);
|
|
24177
|
+
}
|
|
24178
|
+
function insertUserMemory(db, content, sourceCandidateIds) {
|
|
24179
|
+
const now = Date.now();
|
|
24180
|
+
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);
|
|
24181
|
+
return Number(result.lastInsertRowid);
|
|
24182
|
+
}
|
|
24183
|
+
function getActiveUserMemories(db) {
|
|
24184
|
+
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();
|
|
24185
|
+
return rows.map(parseUserMemoryRow);
|
|
24186
|
+
}
|
|
24187
|
+
function updateUserMemoryContent(db, id, content) {
|
|
24188
|
+
db.prepare("UPDATE user_memories SET content = ?, updated_at = ? WHERE id = ?").run(content, Date.now(), id);
|
|
24189
|
+
}
|
|
24190
|
+
function dismissUserMemory(db, id) {
|
|
24191
|
+
db.prepare("UPDATE user_memories SET status = 'dismissed', updated_at = ? WHERE id = ?").run(Date.now(), id);
|
|
24192
|
+
}
|
|
24193
|
+
function parseUserMemoryRow(row) {
|
|
24194
|
+
let candidateIds = [];
|
|
24195
|
+
try {
|
|
24196
|
+
candidateIds = JSON.parse(row.source_candidate_ids);
|
|
24197
|
+
} catch {}
|
|
24198
|
+
return {
|
|
24199
|
+
id: row.id,
|
|
24200
|
+
content: row.content,
|
|
24201
|
+
status: row.status === "dismissed" ? "dismissed" : "active",
|
|
24202
|
+
promotedAt: row.promoted_at,
|
|
24203
|
+
sourceCandidateIds: candidateIds,
|
|
24204
|
+
createdAt: row.created_at,
|
|
24205
|
+
updatedAt: row.updated_at
|
|
24206
|
+
};
|
|
24207
|
+
}
|
|
24208
|
+
|
|
24209
|
+
// src/features/magic-context/user-memory/review-user-memories.ts
|
|
24210
|
+
async function reviewUserMemories(args) {
|
|
24211
|
+
const result = { promoted: 0, merged: 0, dismissed: 0, candidatesConsumed: 0 };
|
|
24212
|
+
const candidates = getUserMemoryCandidates(args.db);
|
|
24213
|
+
if (candidates.length < args.promotionThreshold) {
|
|
24214
|
+
log(`[dreamer] user-memories: ${candidates.length} candidate(s), need ${args.promotionThreshold} \u2014 skipping`);
|
|
24215
|
+
return result;
|
|
24216
|
+
}
|
|
24217
|
+
const stableMemories = getActiveUserMemories(args.db);
|
|
24218
|
+
log(`[dreamer] user-memories: reviewing ${candidates.length} candidate(s) against ${stableMemories.length} stable memorie(s)`);
|
|
24219
|
+
const candidateList = candidates.map((c) => `- Candidate #${c.id} [session ${c.sessionId.slice(0, 12)}]: "${c.content}"`).join(`
|
|
24220
|
+
`);
|
|
24221
|
+
const stableList = stableMemories.length > 0 ? stableMemories.map((m) => `- Memory #${m.id}: "${m.content}"`).join(`
|
|
24222
|
+
`) : "(none)";
|
|
24223
|
+
const prompt = `## Task: Review User Memory Candidates
|
|
24224
|
+
|
|
24225
|
+
You are reviewing behavioral observations about a human user to decide which patterns are real and persistent.
|
|
24226
|
+
|
|
24227
|
+
### Current Stable User Memories
|
|
24228
|
+
${stableList}
|
|
24229
|
+
|
|
24230
|
+
### Candidate Observations (from recent historian runs)
|
|
24231
|
+
${candidateList}
|
|
24232
|
+
|
|
24233
|
+
### Instructions
|
|
24234
|
+
|
|
24235
|
+
1. Look for **recurring patterns** across multiple candidates \u2014 observations that appear independently from different sessions or historian runs indicate a real user trait.
|
|
24236
|
+
2. A candidate must appear in at least ${args.promotionThreshold} semantically similar variants before promotion.
|
|
24237
|
+
3. Only promote **truly universal** user traits \u2014 communication style, expertise level, review focus, decision-making patterns, working habits.
|
|
24238
|
+
4. Do NOT promote: project-specific preferences, framework choices, one-off moods, task-local frustrations.
|
|
24239
|
+
5. If a candidate is semantically equivalent to an existing stable memory, mark it as already covered.
|
|
24240
|
+
6. If multiple candidates describe the same trait, merge them into one clean statement.
|
|
24241
|
+
7. If an existing stable memory should be updated based on new evidence, include the update.
|
|
24242
|
+
|
|
24243
|
+
### Output Format
|
|
24244
|
+
|
|
24245
|
+
Return valid JSON (no markdown fencing):
|
|
24246
|
+
|
|
24247
|
+
{
|
|
24248
|
+
"promote": [
|
|
24249
|
+
{ "content": "Clean universal observation text", "candidate_ids": [1, 3, 7] }
|
|
24250
|
+
],
|
|
24251
|
+
"update_existing": [
|
|
24252
|
+
{ "memory_id": 5, "content": "Updated text incorporating new evidence", "candidate_ids": [2] }
|
|
24253
|
+
],
|
|
24254
|
+
"dismiss_existing": [
|
|
24255
|
+
{ "memory_id": 3, "reason": "No longer supported by recent observations" }
|
|
24256
|
+
],
|
|
24257
|
+
"consume_candidate_ids": [1, 2, 3, 4, 5, 7, 8]
|
|
24258
|
+
}
|
|
24259
|
+
|
|
24260
|
+
- \`promote\`: new stable memories to create from candidates
|
|
24261
|
+
- \`update_existing\`: existing stable memories to rewrite with new evidence
|
|
24262
|
+
- \`dismiss_existing\`: existing stable memories that are no longer valid
|
|
24263
|
+
- \`consume_candidate_ids\`: ALL candidate IDs that were reviewed (promoted, merged, or rejected) \u2014 they will be deleted from the candidate pool
|
|
24264
|
+
|
|
24265
|
+
If no promotions are warranted, return empty arrays. Always consume reviewed candidates so they don't accumulate indefinitely.`;
|
|
24266
|
+
let agentSessionId = null;
|
|
24267
|
+
const abortController = new AbortController;
|
|
24268
|
+
const leaseInterval = setInterval(() => {
|
|
24269
|
+
try {
|
|
24270
|
+
if (!renewLease(args.db, args.holderId)) {
|
|
24271
|
+
log("[dreamer] user-memories: lease renewal failed \u2014 aborting");
|
|
24272
|
+
abortController.abort();
|
|
24273
|
+
}
|
|
24274
|
+
} catch {
|
|
24275
|
+
abortController.abort();
|
|
24276
|
+
}
|
|
24277
|
+
}, 60000);
|
|
24278
|
+
try {
|
|
24279
|
+
const createResponse = await args.client.session.create({
|
|
24280
|
+
body: {
|
|
24281
|
+
...args.parentSessionId ? { parentID: args.parentSessionId } : {},
|
|
24282
|
+
title: "magic-context-dream-user-memories"
|
|
24283
|
+
},
|
|
24284
|
+
query: { directory: args.sessionDirectory }
|
|
24285
|
+
});
|
|
24286
|
+
const created = normalizeSDKResponse(createResponse, null, { preferResponseOnMissingData: true });
|
|
24287
|
+
agentSessionId = typeof created?.id === "string" ? created.id : null;
|
|
24288
|
+
if (!agentSessionId)
|
|
24289
|
+
throw new Error("Could not create user memory review session.");
|
|
24290
|
+
log(`[dreamer] user-memories: child session created ${agentSessionId}`);
|
|
24291
|
+
const remainingMs = Math.max(0, args.deadline - Date.now());
|
|
24292
|
+
await promptSyncWithModelSuggestionRetry(args.client, {
|
|
24293
|
+
path: { id: agentSessionId },
|
|
24294
|
+
query: { directory: args.sessionDirectory },
|
|
24295
|
+
body: {
|
|
24296
|
+
agent: DREAMER_AGENT,
|
|
24297
|
+
system: DREAMER_SYSTEM_PROMPT,
|
|
24298
|
+
parts: [{ type: "text", text: prompt }]
|
|
24299
|
+
}
|
|
24300
|
+
}, { timeoutMs: Math.min(remainingMs, 5 * 60 * 1000), signal: abortController.signal });
|
|
24301
|
+
const messagesResponse = await args.client.session.messages({
|
|
24302
|
+
path: { id: agentSessionId },
|
|
24303
|
+
query: { directory: args.sessionDirectory }
|
|
24304
|
+
});
|
|
24305
|
+
const messages = normalizeSDKResponse(messagesResponse, [], {
|
|
24306
|
+
preferResponseOnMissingData: true
|
|
24307
|
+
});
|
|
24308
|
+
const responseText = extractLatestAssistantText(messages);
|
|
24309
|
+
if (!responseText) {
|
|
24310
|
+
log("[dreamer] user-memories: no response from review agent");
|
|
24311
|
+
return result;
|
|
24312
|
+
}
|
|
24313
|
+
const jsonMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)```/) ?? responseText.match(/(\{[\s\S]*\})/);
|
|
24314
|
+
if (!jsonMatch) {
|
|
24315
|
+
log("[dreamer] user-memories: could not parse JSON from response");
|
|
24316
|
+
return result;
|
|
24317
|
+
}
|
|
24318
|
+
let parsed;
|
|
24319
|
+
try {
|
|
24320
|
+
parsed = JSON.parse(jsonMatch[1]);
|
|
24321
|
+
} catch {
|
|
24322
|
+
log("[dreamer] user-memories: JSON parse failed");
|
|
24323
|
+
return result;
|
|
24324
|
+
}
|
|
24325
|
+
if (parsed.promote) {
|
|
24326
|
+
for (const p of parsed.promote) {
|
|
24327
|
+
if (p.content?.trim()) {
|
|
24328
|
+
insertUserMemory(args.db, p.content.trim(), p.candidate_ids ?? []);
|
|
24329
|
+
result.promoted++;
|
|
24330
|
+
log(`[dreamer] user-memories: promoted "${p.content.trim().slice(0, 60)}..."`);
|
|
24331
|
+
}
|
|
24332
|
+
}
|
|
24333
|
+
}
|
|
24334
|
+
if (parsed.update_existing) {
|
|
24335
|
+
for (const u of parsed.update_existing) {
|
|
24336
|
+
if (u.memory_id && u.content?.trim()) {
|
|
24337
|
+
updateUserMemoryContent(args.db, u.memory_id, u.content.trim());
|
|
24338
|
+
result.merged++;
|
|
24339
|
+
log(`[dreamer] user-memories: updated memory #${u.memory_id}`);
|
|
24340
|
+
}
|
|
24341
|
+
}
|
|
24342
|
+
}
|
|
24343
|
+
if (parsed.dismiss_existing) {
|
|
24344
|
+
for (const d of parsed.dismiss_existing) {
|
|
24345
|
+
if (d.memory_id) {
|
|
24346
|
+
dismissUserMemory(args.db, d.memory_id);
|
|
24347
|
+
result.dismissed++;
|
|
24348
|
+
log(`[dreamer] user-memories: dismissed memory #${d.memory_id} \u2014 ${d.reason ?? "no reason"}`);
|
|
24349
|
+
}
|
|
24350
|
+
}
|
|
24351
|
+
}
|
|
24352
|
+
if (parsed.consume_candidate_ids && parsed.consume_candidate_ids.length > 0) {
|
|
24353
|
+
deleteUserMemoryCandidates(args.db, parsed.consume_candidate_ids);
|
|
24354
|
+
result.candidatesConsumed = parsed.consume_candidate_ids.length;
|
|
24355
|
+
log(`[dreamer] user-memories: consumed ${result.candidatesConsumed} candidate(s)`);
|
|
24356
|
+
}
|
|
24357
|
+
return result;
|
|
24358
|
+
} catch (error48) {
|
|
24359
|
+
log(`[dreamer] user-memories: review failed: ${getErrorMessage(error48)}`);
|
|
24360
|
+
return result;
|
|
24361
|
+
} finally {
|
|
24362
|
+
clearInterval(leaseInterval);
|
|
24363
|
+
if (agentSessionId) {
|
|
24364
|
+
await args.client.session.delete({
|
|
24365
|
+
path: { id: agentSessionId },
|
|
24366
|
+
query: { directory: args.sessionDirectory }
|
|
24367
|
+
}).catch((e) => {
|
|
24368
|
+
log(`[dreamer] user-memories: session cleanup failed: ${getErrorMessage(e)}`);
|
|
24369
|
+
});
|
|
24370
|
+
}
|
|
24371
|
+
}
|
|
24009
24372
|
}
|
|
24010
24373
|
|
|
24011
24374
|
// src/features/magic-context/dreamer/storage-dream-runs.ts
|
|
@@ -24185,6 +24548,24 @@ async function runDream(args) {
|
|
|
24185
24548
|
}
|
|
24186
24549
|
}
|
|
24187
24550
|
}
|
|
24551
|
+
if (args.experimentalUserMemories?.enabled && Date.now() <= deadline) {
|
|
24552
|
+
try {
|
|
24553
|
+
const reviewResult = await reviewUserMemories({
|
|
24554
|
+
db: args.db,
|
|
24555
|
+
client: args.client,
|
|
24556
|
+
parentSessionId,
|
|
24557
|
+
sessionDirectory: args.sessionDirectory,
|
|
24558
|
+
holderId,
|
|
24559
|
+
deadline,
|
|
24560
|
+
promotionThreshold: args.experimentalUserMemories.promotionThreshold
|
|
24561
|
+
});
|
|
24562
|
+
if (reviewResult.promoted > 0 || reviewResult.merged > 0 || reviewResult.dismissed > 0) {
|
|
24563
|
+
log(`[dreamer] user-memories: promoted=${reviewResult.promoted} merged=${reviewResult.merged} dismissed=${reviewResult.dismissed} consumed=${reviewResult.candidatesConsumed}`);
|
|
24564
|
+
}
|
|
24565
|
+
} catch (error48) {
|
|
24566
|
+
log(`[dreamer] user-memory review failed: ${getErrorMessage(error48)}`);
|
|
24567
|
+
}
|
|
24568
|
+
}
|
|
24188
24569
|
if (Date.now() <= deadline) {
|
|
24189
24570
|
try {
|
|
24190
24571
|
await evaluateSmartNotes({
|
|
@@ -24325,7 +24706,7 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
|
|
|
24325
24706
|
if (!jsonMatch) {
|
|
24326
24707
|
log("[dreamer] smart notes: no JSON array found in output, skipping");
|
|
24327
24708
|
for (const note of pendingNotes)
|
|
24328
|
-
|
|
24709
|
+
markNoteChecked(args.db, note.id);
|
|
24329
24710
|
throw new Error("Smart note evaluation returned no JSON array.");
|
|
24330
24711
|
}
|
|
24331
24712
|
let evaluations;
|
|
@@ -24334,7 +24715,7 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
|
|
|
24334
24715
|
} catch {
|
|
24335
24716
|
log(`[dreamer] smart notes: failed to parse JSON from LLM output, marking all checked`);
|
|
24336
24717
|
for (const note of pendingNotes)
|
|
24337
|
-
|
|
24718
|
+
markNoteChecked(args.db, note.id);
|
|
24338
24719
|
throw new Error("Smart note evaluation returned invalid JSON.");
|
|
24339
24720
|
}
|
|
24340
24721
|
let surfaced = 0;
|
|
@@ -24345,16 +24726,16 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
|
|
|
24345
24726
|
if (!note)
|
|
24346
24727
|
continue;
|
|
24347
24728
|
if (evaluation.met) {
|
|
24348
|
-
|
|
24729
|
+
markNoteReady(args.db, note.id, evaluation.reason);
|
|
24349
24730
|
surfaced++;
|
|
24350
24731
|
log(`[dreamer] smart notes: #${note.id} condition MET \u2014 "${evaluation.reason ?? "condition satisfied"}"`);
|
|
24351
24732
|
} else {
|
|
24352
|
-
|
|
24733
|
+
markNoteChecked(args.db, note.id);
|
|
24353
24734
|
}
|
|
24354
24735
|
}
|
|
24355
24736
|
for (const note of pendingNotes) {
|
|
24356
24737
|
if (!evaluations.some((e) => e.id === note.id)) {
|
|
24357
|
-
|
|
24738
|
+
markNoteChecked(args.db, note.id);
|
|
24358
24739
|
}
|
|
24359
24740
|
}
|
|
24360
24741
|
const durationMs = Date.now() - taskStartedAt;
|
|
@@ -24408,7 +24789,8 @@ async function processDreamQueue(args) {
|
|
|
24408
24789
|
tasks: args.tasks,
|
|
24409
24790
|
taskTimeoutMinutes: args.taskTimeoutMinutes,
|
|
24410
24791
|
maxRuntimeMinutes: args.maxRuntimeMinutes,
|
|
24411
|
-
sessionDirectory: projectDirectory
|
|
24792
|
+
sessionDirectory: projectDirectory,
|
|
24793
|
+
experimentalUserMemories: args.experimentalUserMemories
|
|
24412
24794
|
});
|
|
24413
24795
|
} catch (error48) {
|
|
24414
24796
|
log(`[dreamer] runDream threw for ${entry.projectIdentity}: ${getErrorMessage(error48)}`);
|
|
@@ -24460,7 +24842,8 @@ function isInScheduleWindow(schedule, now = new Date) {
|
|
|
24460
24842
|
function findProjectsNeedingDream(db) {
|
|
24461
24843
|
const projectRows = db.query(`SELECT DISTINCT project_path FROM memories WHERE status = 'active'
|
|
24462
24844
|
UNION
|
|
24463
|
-
SELECT DISTINCT project_path FROM
|
|
24845
|
+
SELECT DISTINCT project_path FROM notes
|
|
24846
|
+
WHERE type = 'smart' AND status = 'pending' AND project_path IS NOT NULL
|
|
24464
24847
|
ORDER BY project_path`).all();
|
|
24465
24848
|
const projects = [];
|
|
24466
24849
|
for (const row of projectRows) {
|
|
@@ -24469,8 +24852,8 @@ function findProjectsNeedingDream(db) {
|
|
|
24469
24852
|
const lastDreamAt = Number(lastDreamAtStr ?? fallbackStr ?? "0") || 0;
|
|
24470
24853
|
const updatedMemories = db.query(`SELECT COUNT(*) as cnt FROM memories
|
|
24471
24854
|
WHERE project_path = ? AND status = 'active' AND updated_at > ?`).get(row.project_path, lastDreamAt);
|
|
24472
|
-
const pendingSmartNotes = db.query(`SELECT COUNT(*) as cnt FROM
|
|
24473
|
-
WHERE project_path = ? AND status = 'pending'`).get(row.project_path);
|
|
24855
|
+
const pendingSmartNotes = db.query(`SELECT COUNT(*) as cnt FROM notes
|
|
24856
|
+
WHERE project_path = ? AND type = 'smart' AND status = 'pending'`).get(row.project_path);
|
|
24474
24857
|
if (updatedMemories && updatedMemories.cnt > 0 || pendingSmartNotes && pendingSmartNotes.cnt > 0) {
|
|
24475
24858
|
projects.push(row.project_path);
|
|
24476
24859
|
}
|
|
@@ -24517,6 +24900,22 @@ function cosineSimilarity(a, b) {
|
|
|
24517
24900
|
|
|
24518
24901
|
// src/features/magic-context/memory/embedding-local.ts
|
|
24519
24902
|
init_logger();
|
|
24903
|
+
async function withQuietConsole(fn) {
|
|
24904
|
+
const origWarn = console.warn;
|
|
24905
|
+
const origError = console.error;
|
|
24906
|
+
const redirect = (...args) => {
|
|
24907
|
+
const message = args.map((a) => typeof a === "string" ? a : String(a)).join(" ");
|
|
24908
|
+
log(`[transformers] ${message}`);
|
|
24909
|
+
};
|
|
24910
|
+
console.warn = redirect;
|
|
24911
|
+
console.error = redirect;
|
|
24912
|
+
try {
|
|
24913
|
+
return await fn();
|
|
24914
|
+
} finally {
|
|
24915
|
+
console.warn = origWarn;
|
|
24916
|
+
console.error = origError;
|
|
24917
|
+
}
|
|
24918
|
+
}
|
|
24520
24919
|
function isArrayLikeNumber(value) {
|
|
24521
24920
|
if (typeof value !== "object" || value === null || !("length" in value)) {
|
|
24522
24921
|
return false;
|
|
@@ -24572,10 +24971,16 @@ class LocalEmbeddingProvider {
|
|
|
24572
24971
|
this.initPromise = (async () => {
|
|
24573
24972
|
try {
|
|
24574
24973
|
const transformersModule = await import("@huggingface/transformers");
|
|
24974
|
+
const env = transformersModule.env;
|
|
24975
|
+
const LogLevel = transformersModule.LogLevel;
|
|
24976
|
+
if (LogLevel && "ERROR" in LogLevel) {
|
|
24977
|
+
env.logLevel = LogLevel.ERROR;
|
|
24978
|
+
}
|
|
24575
24979
|
const createPipeline = transformersModule.pipeline;
|
|
24576
|
-
this.pipeline = await createPipeline("feature-extraction", this.model, {
|
|
24577
|
-
quantized: true
|
|
24578
|
-
|
|
24980
|
+
this.pipeline = await withQuietConsole(() => createPipeline("feature-extraction", this.model, {
|
|
24981
|
+
quantized: true,
|
|
24982
|
+
dtype: "fp32"
|
|
24983
|
+
}));
|
|
24579
24984
|
log(`[magic-context] embedding model loaded: ${this.model}`);
|
|
24580
24985
|
} catch (error48) {
|
|
24581
24986
|
log("[magic-context] embedding model failed to load:", error48);
|
|
@@ -24592,13 +24997,14 @@ class LocalEmbeddingProvider {
|
|
|
24592
24997
|
return null;
|
|
24593
24998
|
}
|
|
24594
24999
|
try {
|
|
24595
|
-
|
|
25000
|
+
const pipeline = this.pipeline;
|
|
25001
|
+
if (!pipeline) {
|
|
24596
25002
|
return null;
|
|
24597
25003
|
}
|
|
24598
|
-
const result = await
|
|
25004
|
+
const result = await withQuietConsole(() => pipeline(text, {
|
|
24599
25005
|
pooling: "mean",
|
|
24600
25006
|
normalize: true
|
|
24601
|
-
});
|
|
25007
|
+
}));
|
|
24602
25008
|
return extractBatchEmbeddings(result, 1)[0] ?? null;
|
|
24603
25009
|
} catch (error48) {
|
|
24604
25010
|
log("[magic-context] embedding failed:", error48);
|
|
@@ -24613,13 +25019,14 @@ class LocalEmbeddingProvider {
|
|
|
24613
25019
|
return Array.from({ length: texts.length }, () => null);
|
|
24614
25020
|
}
|
|
24615
25021
|
try {
|
|
24616
|
-
|
|
25022
|
+
const pipeline = this.pipeline;
|
|
25023
|
+
if (!pipeline) {
|
|
24617
25024
|
return Array.from({ length: texts.length }, () => null);
|
|
24618
25025
|
}
|
|
24619
|
-
const result = await
|
|
25026
|
+
const result = await withQuietConsole(() => pipeline(texts, {
|
|
24620
25027
|
pooling: "mean",
|
|
24621
25028
|
normalize: true
|
|
24622
|
-
});
|
|
25029
|
+
}));
|
|
24623
25030
|
return extractBatchEmbeddings(result, texts.length);
|
|
24624
25031
|
} catch (error48) {
|
|
24625
25032
|
log("[magic-context] embedding batch failed:", error48);
|
|
@@ -25219,7 +25626,11 @@ function readRawSessionMessagesFromDb(db, sessionId) {
|
|
|
25219
25626
|
list.push(parseJsonUnknown(part.data));
|
|
25220
25627
|
partsByMessageId.set(part.message_id, list);
|
|
25221
25628
|
}
|
|
25222
|
-
|
|
25629
|
+
const filtered = messageRows.filter((row) => {
|
|
25630
|
+
const info = parseJsonRecord(row.data);
|
|
25631
|
+
return !(info?.summary === true && info?.finish === "stop");
|
|
25632
|
+
});
|
|
25633
|
+
return filtered.flatMap((row, index) => {
|
|
25223
25634
|
const info = parseJsonRecord(row.data);
|
|
25224
25635
|
if (!info)
|
|
25225
25636
|
return [];
|
|
@@ -25593,6 +26004,158 @@ import { Database as Database2 } from "bun:sqlite";
|
|
|
25593
26004
|
import { mkdirSync } from "fs";
|
|
25594
26005
|
import { join as join9 } from "path";
|
|
25595
26006
|
init_logger();
|
|
26007
|
+
|
|
26008
|
+
// src/features/magic-context/migrations.ts
|
|
26009
|
+
init_logger();
|
|
26010
|
+
var MIGRATIONS = [
|
|
26011
|
+
{
|
|
26012
|
+
version: 1,
|
|
26013
|
+
description: "Merge session_notes + smart_notes into unified notes table",
|
|
26014
|
+
up: (db) => {
|
|
26015
|
+
db.exec(`
|
|
26016
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
26017
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26018
|
+
type TEXT NOT NULL DEFAULT 'session',
|
|
26019
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
26020
|
+
content TEXT NOT NULL,
|
|
26021
|
+
session_id TEXT,
|
|
26022
|
+
project_path TEXT,
|
|
26023
|
+
surface_condition TEXT,
|
|
26024
|
+
created_at INTEGER NOT NULL,
|
|
26025
|
+
updated_at INTEGER NOT NULL,
|
|
26026
|
+
last_checked_at INTEGER,
|
|
26027
|
+
ready_at INTEGER,
|
|
26028
|
+
ready_reason TEXT
|
|
26029
|
+
);
|
|
26030
|
+
CREATE INDEX IF NOT EXISTS idx_notes_session_status ON notes(session_id, status);
|
|
26031
|
+
CREATE INDEX IF NOT EXISTS idx_notes_project_status ON notes(project_path, status);
|
|
26032
|
+
CREATE INDEX IF NOT EXISTS idx_notes_type_status ON notes(type, status);
|
|
26033
|
+
`);
|
|
26034
|
+
const hasSessionNotes = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='session_notes'").get();
|
|
26035
|
+
if (hasSessionNotes) {
|
|
26036
|
+
db.exec(`
|
|
26037
|
+
INSERT INTO notes (type, status, content, session_id, created_at, updated_at)
|
|
26038
|
+
SELECT 'session', 'active', content, session_id, created_at, created_at
|
|
26039
|
+
FROM session_notes
|
|
26040
|
+
`);
|
|
26041
|
+
}
|
|
26042
|
+
const hasSmartNotes = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='smart_notes'").get();
|
|
26043
|
+
if (hasSmartNotes) {
|
|
26044
|
+
db.exec(`
|
|
26045
|
+
INSERT INTO notes (type, status, content, session_id, project_path, surface_condition,
|
|
26046
|
+
created_at, updated_at, last_checked_at, ready_at, ready_reason)
|
|
26047
|
+
SELECT 'smart', status, content, created_session_id, project_path, surface_condition,
|
|
26048
|
+
created_at, updated_at, last_checked_at, ready_at, ready_reason
|
|
26049
|
+
FROM smart_notes
|
|
26050
|
+
`);
|
|
26051
|
+
}
|
|
26052
|
+
if (hasSessionNotes) {
|
|
26053
|
+
const sourceCount = db.prepare("SELECT COUNT(*) as c FROM session_notes").get().c;
|
|
26054
|
+
const migratedCount = db.prepare("SELECT COUNT(*) as c FROM notes WHERE type = 'session'").get().c;
|
|
26055
|
+
if (migratedCount >= sourceCount) {
|
|
26056
|
+
db.exec("DROP TABLE session_notes");
|
|
26057
|
+
} else {
|
|
26058
|
+
throw new Error(`session_notes migration verification failed: expected ${sourceCount} rows, got ${migratedCount}`);
|
|
26059
|
+
}
|
|
26060
|
+
}
|
|
26061
|
+
if (hasSmartNotes) {
|
|
26062
|
+
const sourceCount = db.prepare("SELECT COUNT(*) as c FROM smart_notes").get().c;
|
|
26063
|
+
const migratedCount = db.prepare("SELECT COUNT(*) as c FROM notes WHERE type = 'smart'").get().c;
|
|
26064
|
+
if (migratedCount >= sourceCount) {
|
|
26065
|
+
db.exec("DROP TABLE smart_notes");
|
|
26066
|
+
} else {
|
|
26067
|
+
throw new Error(`smart_notes migration verification failed: expected ${sourceCount} rows, got ${migratedCount}`);
|
|
26068
|
+
}
|
|
26069
|
+
}
|
|
26070
|
+
}
|
|
26071
|
+
},
|
|
26072
|
+
{
|
|
26073
|
+
version: 2,
|
|
26074
|
+
description: "Add plugin_messages table for TUI \u2194 server communication",
|
|
26075
|
+
up: (db) => {
|
|
26076
|
+
db.exec(`
|
|
26077
|
+
CREATE TABLE IF NOT EXISTS plugin_messages (
|
|
26078
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26079
|
+
direction TEXT NOT NULL,
|
|
26080
|
+
type TEXT NOT NULL,
|
|
26081
|
+
payload TEXT NOT NULL DEFAULT '{}',
|
|
26082
|
+
session_id TEXT,
|
|
26083
|
+
created_at INTEGER NOT NULL,
|
|
26084
|
+
consumed_at INTEGER
|
|
26085
|
+
);
|
|
26086
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_messages_direction_consumed
|
|
26087
|
+
ON plugin_messages(direction, consumed_at);
|
|
26088
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_messages_created
|
|
26089
|
+
ON plugin_messages(created_at);
|
|
26090
|
+
`);
|
|
26091
|
+
}
|
|
26092
|
+
},
|
|
26093
|
+
{
|
|
26094
|
+
version: 3,
|
|
26095
|
+
description: "Add user_memory_candidates and user_memories tables",
|
|
26096
|
+
up: (db) => {
|
|
26097
|
+
db.exec(`
|
|
26098
|
+
CREATE TABLE IF NOT EXISTS user_memory_candidates (
|
|
26099
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26100
|
+
content TEXT NOT NULL,
|
|
26101
|
+
session_id TEXT NOT NULL,
|
|
26102
|
+
source_compartment_start INTEGER,
|
|
26103
|
+
source_compartment_end INTEGER,
|
|
26104
|
+
created_at INTEGER NOT NULL
|
|
26105
|
+
);
|
|
26106
|
+
CREATE INDEX IF NOT EXISTS idx_umc_created ON user_memory_candidates(created_at);
|
|
26107
|
+
|
|
26108
|
+
CREATE TABLE IF NOT EXISTS user_memories (
|
|
26109
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26110
|
+
content TEXT NOT NULL,
|
|
26111
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
26112
|
+
promoted_at INTEGER NOT NULL,
|
|
26113
|
+
source_candidate_ids TEXT DEFAULT '[]',
|
|
26114
|
+
created_at INTEGER NOT NULL,
|
|
26115
|
+
updated_at INTEGER NOT NULL
|
|
26116
|
+
);
|
|
26117
|
+
CREATE INDEX IF NOT EXISTS idx_um_status ON user_memories(status);
|
|
26118
|
+
`);
|
|
26119
|
+
}
|
|
26120
|
+
}
|
|
26121
|
+
];
|
|
26122
|
+
function ensureMigrationsTable(db) {
|
|
26123
|
+
db.exec(`
|
|
26124
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
26125
|
+
version INTEGER PRIMARY KEY,
|
|
26126
|
+
description TEXT NOT NULL,
|
|
26127
|
+
applied_at INTEGER NOT NULL
|
|
26128
|
+
)
|
|
26129
|
+
`);
|
|
26130
|
+
}
|
|
26131
|
+
function getCurrentVersion(db) {
|
|
26132
|
+
const row = db.prepare("SELECT MAX(version) as version FROM schema_migrations").get();
|
|
26133
|
+
return row?.version ?? 0;
|
|
26134
|
+
}
|
|
26135
|
+
function runMigrations(db) {
|
|
26136
|
+
ensureMigrationsTable(db);
|
|
26137
|
+
const currentVersion = getCurrentVersion(db);
|
|
26138
|
+
const pendingMigrations = MIGRATIONS.filter((m) => m.version > currentVersion);
|
|
26139
|
+
if (pendingMigrations.length === 0) {
|
|
26140
|
+
return;
|
|
26141
|
+
}
|
|
26142
|
+
log(`[migrations] current schema version: ${currentVersion}, applying ${pendingMigrations.length} migration(s)`);
|
|
26143
|
+
for (const migration of pendingMigrations) {
|
|
26144
|
+
try {
|
|
26145
|
+
db.transaction(() => {
|
|
26146
|
+
migration.up(db);
|
|
26147
|
+
db.prepare("INSERT INTO schema_migrations (version, description, applied_at) VALUES (?, ?, ?)").run(migration.version, migration.description, Date.now());
|
|
26148
|
+
})();
|
|
26149
|
+
log(`[migrations] applied v${migration.version}: ${migration.description}`);
|
|
26150
|
+
} catch (error48) {
|
|
26151
|
+
log(`[migrations] FAILED v${migration.version}: ${migration.description} \u2014 ${error48 instanceof Error ? error48.message : String(error48)}`);
|
|
26152
|
+
throw new Error(`Migration v${migration.version} failed: ${error48 instanceof Error ? error48.message : String(error48)}. Database may need manual repair.`);
|
|
26153
|
+
}
|
|
26154
|
+
}
|
|
26155
|
+
log(`[migrations] schema version now: ${MIGRATIONS[MIGRATIONS.length - 1].version}`);
|
|
26156
|
+
}
|
|
26157
|
+
|
|
26158
|
+
// src/features/magic-context/storage-db.ts
|
|
25596
26159
|
var databases = new Map;
|
|
25597
26160
|
var FALLBACK_DATABASE_KEY = "__fallback__:memory:";
|
|
25598
26161
|
var persistenceByDatabase = new WeakMap;
|
|
@@ -25872,6 +26435,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
|
|
|
25872
26435
|
ensureColumn(db, "dream_queue", "retry_count", "INTEGER DEFAULT 0");
|
|
25873
26436
|
ensureColumn(db, "tags", "reasoning_byte_size", "INTEGER DEFAULT 0");
|
|
25874
26437
|
ensureColumn(db, "session_meta", "system_prompt_tokens", "INTEGER DEFAULT 0");
|
|
26438
|
+
ensureColumn(db, "session_meta", "compaction_marker_state", "TEXT DEFAULT ''");
|
|
25875
26439
|
}
|
|
25876
26440
|
function ensureColumn(db, table, column, definition) {
|
|
25877
26441
|
if (!/^[a-z_]+$/.test(table) || !/^[a-z_]+$/.test(column) || !/^[A-Z0-9_'(),\s]+$/i.test(definition)) {
|
|
@@ -25887,6 +26451,7 @@ function createFallbackDatabase() {
|
|
|
25887
26451
|
try {
|
|
25888
26452
|
const fallback = new Database2(":memory:");
|
|
25889
26453
|
initializeDatabase(fallback);
|
|
26454
|
+
runMigrations(fallback);
|
|
25890
26455
|
return fallback;
|
|
25891
26456
|
} catch (error48) {
|
|
25892
26457
|
throw new Error(`[magic-context] storage fatal: failed to initialize fallback database: ${getErrorMessage(error48)}`);
|
|
@@ -25905,6 +26470,7 @@ function openDatabase() {
|
|
|
25905
26470
|
mkdirSync(dbDir, { recursive: true });
|
|
25906
26471
|
const db = new Database2(dbPath);
|
|
25907
26472
|
initializeDatabase(db);
|
|
26473
|
+
runMigrations(db);
|
|
25908
26474
|
databases.set(dbPath, db);
|
|
25909
26475
|
persistenceByDatabase.set(db, true);
|
|
25910
26476
|
persistenceErrorByDatabase.delete(db);
|
|
@@ -26137,6 +26703,24 @@ function setPersistedDeliveredNoteNudge(db, sessionId, text, messageId = "") {
|
|
|
26137
26703
|
function clearPersistedNoteNudge(db, sessionId) {
|
|
26138
26704
|
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);
|
|
26139
26705
|
}
|
|
26706
|
+
function getPersistedCompactionMarkerState(db, sessionId) {
|
|
26707
|
+
const row = db.prepare("SELECT compaction_marker_state FROM session_meta WHERE session_id = ?").get(sessionId);
|
|
26708
|
+
const raw = row?.compaction_marker_state;
|
|
26709
|
+
if (!raw || raw.length === 0)
|
|
26710
|
+
return null;
|
|
26711
|
+
try {
|
|
26712
|
+
const parsed = JSON.parse(raw);
|
|
26713
|
+
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") {
|
|
26714
|
+
return parsed;
|
|
26715
|
+
}
|
|
26716
|
+
} catch {}
|
|
26717
|
+
return null;
|
|
26718
|
+
}
|
|
26719
|
+
function setPersistedCompactionMarkerState(db, sessionId, state) {
|
|
26720
|
+
ensureSessionMetaRow(db, sessionId);
|
|
26721
|
+
const json2 = state ? JSON.stringify(state) : "";
|
|
26722
|
+
db.prepare("UPDATE session_meta SET compaction_marker_state = ? WHERE session_id = ?").run(json2, sessionId);
|
|
26723
|
+
}
|
|
26140
26724
|
function getStrippedPlaceholderIds(db, sessionId) {
|
|
26141
26725
|
const row = db.prepare("SELECT stripped_placeholder_ids FROM session_meta WHERE session_id = ?").get(sessionId);
|
|
26142
26726
|
const raw = row?.stripped_placeholder_ids;
|
|
@@ -26207,37 +26791,12 @@ function clearSession(db, sessionId) {
|
|
|
26207
26791
|
db.prepare("DELETE FROM compartments WHERE session_id = ?").run(sessionId);
|
|
26208
26792
|
clearCompressionDepth(db, sessionId);
|
|
26209
26793
|
db.prepare("DELETE FROM session_facts WHERE session_id = ?").run(sessionId);
|
|
26210
|
-
db.prepare("DELETE FROM
|
|
26794
|
+
db.prepare("DELETE FROM notes WHERE session_id = ? AND type = 'session'").run(sessionId);
|
|
26211
26795
|
db.prepare("DELETE FROM recomp_compartments WHERE session_id = ?").run(sessionId);
|
|
26212
26796
|
db.prepare("DELETE FROM recomp_facts WHERE session_id = ?").run(sessionId);
|
|
26213
26797
|
clearIndexedMessages(db, sessionId);
|
|
26214
26798
|
})();
|
|
26215
26799
|
}
|
|
26216
|
-
// src/features/magic-context/storage-notes.ts
|
|
26217
|
-
function isSessionNoteRow(row) {
|
|
26218
|
-
if (row === null || typeof row !== "object")
|
|
26219
|
-
return false;
|
|
26220
|
-
const candidate = row;
|
|
26221
|
-
return typeof candidate.id === "number" && typeof candidate.session_id === "string" && typeof candidate.content === "string" && typeof candidate.created_at === "number";
|
|
26222
|
-
}
|
|
26223
|
-
function toSessionNote(row) {
|
|
26224
|
-
return {
|
|
26225
|
-
id: row.id,
|
|
26226
|
-
sessionId: row.session_id,
|
|
26227
|
-
content: row.content,
|
|
26228
|
-
createdAt: row.created_at
|
|
26229
|
-
};
|
|
26230
|
-
}
|
|
26231
|
-
function getSessionNotes(db, sessionId) {
|
|
26232
|
-
const rows = db.prepare("SELECT * FROM session_notes WHERE session_id = ? ORDER BY id ASC").all(sessionId).filter(isSessionNoteRow);
|
|
26233
|
-
return rows.map(toSessionNote);
|
|
26234
|
-
}
|
|
26235
|
-
function addSessionNote(db, sessionId, content) {
|
|
26236
|
-
db.prepare("INSERT INTO session_notes (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, Date.now());
|
|
26237
|
-
}
|
|
26238
|
-
function clearSessionNotes(db, sessionId) {
|
|
26239
|
-
db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
|
|
26240
|
-
}
|
|
26241
26800
|
// src/features/magic-context/storage-ops.ts
|
|
26242
26801
|
init_logger();
|
|
26243
26802
|
var queuePendingOpStatements = new WeakMap;
|
|
@@ -26454,7 +27013,14 @@ function getTopNBySize(db, sessionId, n) {
|
|
|
26454
27013
|
init_logger();
|
|
26455
27014
|
var DREAM_TIMER_INTERVAL_MS = 15 * 60 * 1000;
|
|
26456
27015
|
function startDreamScheduleTimer(args) {
|
|
26457
|
-
const {
|
|
27016
|
+
const {
|
|
27017
|
+
client,
|
|
27018
|
+
directory,
|
|
27019
|
+
dreamerConfig,
|
|
27020
|
+
embeddingConfig: embeddingConfig2,
|
|
27021
|
+
memoryEnabled,
|
|
27022
|
+
experimentalUserMemories
|
|
27023
|
+
} = args;
|
|
26458
27024
|
const dreamingEnabled = Boolean(dreamerConfig?.enabled && dreamerConfig.schedule?.trim());
|
|
26459
27025
|
const embeddingSweepEnabled = memoryEnabled && embeddingConfig2.provider !== "off";
|
|
26460
27026
|
if (!dreamingEnabled && !embeddingSweepEnabled) {
|
|
@@ -26482,7 +27048,8 @@ function startDreamScheduleTimer(args) {
|
|
|
26482
27048
|
client,
|
|
26483
27049
|
tasks: dreamerConfig.tasks,
|
|
26484
27050
|
taskTimeoutMinutes: dreamerConfig.task_timeout_minutes,
|
|
26485
|
-
maxRuntimeMinutes: dreamerConfig.max_runtime_minutes
|
|
27051
|
+
maxRuntimeMinutes: dreamerConfig.max_runtime_minutes,
|
|
27052
|
+
experimentalUserMemories
|
|
26486
27053
|
}).catch((error48) => {
|
|
26487
27054
|
log("[dreamer] timer-triggered queue processing failed:", error48);
|
|
26488
27055
|
});
|
|
@@ -27203,6 +27770,8 @@ async function sendUserPrompt(client, sessionId, text) {
|
|
|
27203
27770
|
}
|
|
27204
27771
|
|
|
27205
27772
|
// src/hooks/magic-context/command-handler.ts
|
|
27773
|
+
var recompConfirmationBySession = new Map;
|
|
27774
|
+
var RECOMP_CONFIRMATION_WINDOW_MS = 60000;
|
|
27206
27775
|
var SENTINEL_PREFIX = "__CONTEXT_MANAGEMENT_";
|
|
27207
27776
|
async function executeAugmentation(deps, sessionId, userPrompt) {
|
|
27208
27777
|
if (!deps.sidekick?.config) {
|
|
@@ -27278,7 +27847,8 @@ Dreaming is not configured for this project.`, {});
|
|
|
27278
27847
|
client: deps.dreamer.client,
|
|
27279
27848
|
tasks: deps.dreamer.config.tasks,
|
|
27280
27849
|
taskTimeoutMinutes: deps.dreamer.config.task_timeout_minutes,
|
|
27281
|
-
maxRuntimeMinutes: deps.dreamer.config.max_runtime_minutes
|
|
27850
|
+
maxRuntimeMinutes: deps.dreamer.config.max_runtime_minutes,
|
|
27851
|
+
experimentalUserMemories: deps.dreamer.experimentalUserMemories
|
|
27282
27852
|
});
|
|
27283
27853
|
await deps.sendNotification(sessionId, result ? summarizeDreamResult(result) : "Dream queued, but another worker is already processing the queue.", {});
|
|
27284
27854
|
throw new Error(`${SENTINEL_PREFIX}CTX-DREAM_HANDLED__`);
|
|
@@ -27321,12 +27891,40 @@ function createMagicContextCommandHandler(deps) {
|
|
|
27321
27891
|
${statusOutput}` : statusOutput;
|
|
27322
27892
|
}
|
|
27323
27893
|
if (isRecomp) {
|
|
27324
|
-
|
|
27325
|
-
|
|
27326
|
-
Historian recomp started. Rebuilding compartments and facts from raw session history now.`, {});
|
|
27327
|
-
result = deps.executeRecomp ? await deps.executeRecomp(sessionId) : `## Magic Recomp
|
|
27894
|
+
if (!deps.executeRecomp) {
|
|
27895
|
+
result = `## Magic Recomp
|
|
27328
27896
|
|
|
27329
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
|
+
}
|
|
27330
27928
|
}
|
|
27331
27929
|
await deps.sendNotification(sessionId, result, {});
|
|
27332
27930
|
sessionLog(sessionId, `command ${input.command} handled via command.execute.before`);
|
|
@@ -27338,6 +27936,175 @@ Historian recomp started. Rebuilding compartments and facts from raw session his
|
|
|
27338
27936
|
// src/hooks/magic-context/event-handler.ts
|
|
27339
27937
|
init_logger();
|
|
27340
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
|
+
|
|
27341
28108
|
// src/hooks/magic-context/event-payloads.ts
|
|
27342
28109
|
function getSessionProperties(properties) {
|
|
27343
28110
|
if (!isRecord(properties)) {
|
|
@@ -27634,6 +28401,11 @@ function createEventHandler2(deps) {
|
|
|
27634
28401
|
clearNoteNudgeState(deps.db, info.sessionID, { persist: false });
|
|
27635
28402
|
sessionLog(info.sessionID, "event message.removed: cleared in-memory note nudge state");
|
|
27636
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
|
+
}
|
|
27637
28409
|
deps.onSessionCacheInvalidated?.(info.sessionID);
|
|
27638
28410
|
sessionLog(info.sessionID, "event message.removed: cleared session injection cache");
|
|
27639
28411
|
} catch (error48) {
|
|
@@ -27651,6 +28423,11 @@ function createEventHandler2(deps) {
|
|
|
27651
28423
|
} catch (error48) {
|
|
27652
28424
|
sessionLog(sessionId, "event session.compacted handling failed:", error48);
|
|
27653
28425
|
}
|
|
28426
|
+
try {
|
|
28427
|
+
removeCompactionMarkerForSession(deps.db, sessionId);
|
|
28428
|
+
} catch (error48) {
|
|
28429
|
+
sessionLog(sessionId, "event session.compacted marker cleanup failed:", error48);
|
|
28430
|
+
}
|
|
27654
28431
|
deps.onSessionCacheInvalidated?.(sessionId);
|
|
27655
28432
|
return;
|
|
27656
28433
|
}
|
|
@@ -27661,6 +28438,7 @@ function createEventHandler2(deps) {
|
|
|
27661
28438
|
}
|
|
27662
28439
|
deps.nudgePlacements.clear(sessionId);
|
|
27663
28440
|
try {
|
|
28441
|
+
removeCompactionMarkerForSession(deps.db, sessionId);
|
|
27664
28442
|
clearSession(deps.db, sessionId);
|
|
27665
28443
|
} catch (error48) {
|
|
27666
28444
|
sessionLog(sessionId, "event session.deleted persistence failed:", error48);
|
|
@@ -27740,7 +28518,10 @@ function trimMemoriesToBudget(sessionId, memories, budgetTokens) {
|
|
|
27740
28518
|
return -1;
|
|
27741
28519
|
if (b.status === "permanent" && a.status !== "permanent")
|
|
27742
28520
|
return 1;
|
|
27743
|
-
|
|
28521
|
+
const seenDiff = b.seenCount - a.seenCount;
|
|
28522
|
+
if (seenDiff !== 0)
|
|
28523
|
+
return seenDiff;
|
|
28524
|
+
return a.id - b.id;
|
|
27744
28525
|
});
|
|
27745
28526
|
const result = [];
|
|
27746
28527
|
let usedTokens = 0;
|
|
@@ -28478,6 +29259,9 @@ function searchMemoriesFTS(db, projectPath, query, limit = DEFAULT_SEARCH_LIMIT)
|
|
|
28478
29259
|
const rows = getSearchStatement(db).all(projectPath, Date.now(), sanitized, limit).filter(isMemoryRow);
|
|
28479
29260
|
return rows.map(toMemory);
|
|
28480
29261
|
}
|
|
29262
|
+
// src/hooks/magic-context/compartment-runner-incremental.ts
|
|
29263
|
+
init_logger();
|
|
29264
|
+
|
|
28481
29265
|
// src/hooks/magic-context/compartment-runner-compressor.ts
|
|
28482
29266
|
init_logger();
|
|
28483
29267
|
|
|
@@ -28486,6 +29270,8 @@ var COMPARTMENT_REGEX = /<compartment\s+(?:id="[^"]*"\s+)?start="(\d+)"\s+end="(
|
|
|
28486
29270
|
var CATEGORY_BLOCK_REGEX = /<(WORKFLOW_RULES|ARCHITECTURE_DECISIONS|CONSTRAINTS|CONFIG_DEFAULTS|KNOWN_ISSUES|ENVIRONMENT|NAMING|USER_PREFERENCES|USER_DIRECTIVES)>(.*?)<\/\1>/gs;
|
|
28487
29271
|
var FACT_ITEM_REGEX = /^\s*\*\s*(.+)$/gm;
|
|
28488
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;
|
|
28489
29275
|
function parseCompartmentOutput(text) {
|
|
28490
29276
|
const compartments = [];
|
|
28491
29277
|
const facts = [];
|
|
@@ -28510,8 +29296,17 @@ function parseCompartmentOutput(text) {
|
|
|
28510
29296
|
}
|
|
28511
29297
|
const unprocessedMatch = text.match(UNPROCESSED_REGEX);
|
|
28512
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
|
+
}
|
|
28513
29308
|
compartments.sort((a, b) => a.startMessage - b.startMessage);
|
|
28514
|
-
return { compartments, facts, unprocessedFrom };
|
|
29309
|
+
return { compartments, facts, unprocessedFrom, userObservations };
|
|
28515
29310
|
}
|
|
28516
29311
|
function unescapeXml(s) {
|
|
28517
29312
|
return s.replace(/&/g, "&").replace(/'/g, "'").replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -28670,6 +29465,7 @@ ${compartment.content}
|
|
|
28670
29465
|
}
|
|
28671
29466
|
}
|
|
28672
29467
|
replaceAllCompartmentState(db, sessionId, allCompartments, facts.map((f) => ({ category: f.category, content: f.content })));
|
|
29468
|
+
clearInjectionCache(sessionId);
|
|
28673
29469
|
incrementCompressionDepth(db, sessionId, originalStart, originalEnd);
|
|
28674
29470
|
sessionLog(sessionId, `compressor: replaced ${selectedCompartments.length} compartments with ${compressed.length} compressed compartments`);
|
|
28675
29471
|
sessionLog(sessionId, `compressor: incremented compression depth for messages ${originalStart}-${originalEnd}`);
|
|
@@ -28789,7 +29585,7 @@ function queueDropsForCompartmentalizedMessages(db, sessionId, upToMessageIndex)
|
|
|
28789
29585
|
// src/hooks/magic-context/compartment-runner-historian.ts
|
|
28790
29586
|
import { mkdirSync as mkdirSync2, unlinkSync, writeFileSync } from "fs";
|
|
28791
29587
|
import { tmpdir as tmpdir2 } from "os";
|
|
28792
|
-
import { join as
|
|
29588
|
+
import { join as join11 } from "path";
|
|
28793
29589
|
|
|
28794
29590
|
// src/hooks/magic-context/compartment-runner-mapping.ts
|
|
28795
29591
|
function mapParsedCompartmentsToChunk(compartments, chunk, sequenceOffset) {
|
|
@@ -28856,7 +29652,8 @@ function validateHistorianOutput(text, _sessionId, chunk, _priorCompartments, se
|
|
|
28856
29652
|
return {
|
|
28857
29653
|
ok: true,
|
|
28858
29654
|
compartments: mapped.compartments,
|
|
28859
|
-
facts: parsed.facts
|
|
29655
|
+
facts: parsed.facts,
|
|
29656
|
+
userObservations: parsed.userObservations.length > 0 ? parsed.userObservations : undefined
|
|
28860
29657
|
};
|
|
28861
29658
|
}
|
|
28862
29659
|
function buildHistorianRepairPrompt(originalPrompt, previousOutput, validationError) {
|
|
@@ -28949,7 +29746,7 @@ function getReducedRecompTokenBudget(currentBudget) {
|
|
|
28949
29746
|
}
|
|
28950
29747
|
|
|
28951
29748
|
// src/hooks/magic-context/compartment-runner-historian.ts
|
|
28952
|
-
var HISTORIAN_RESPONSE_DUMP_DIR =
|
|
29749
|
+
var HISTORIAN_RESPONSE_DUMP_DIR = join11(tmpdir2(), "magic-context-historian");
|
|
28953
29750
|
async function runValidatedHistorianPass(args) {
|
|
28954
29751
|
const firstRun = await runHistorianPrompt({
|
|
28955
29752
|
...args,
|
|
@@ -29051,7 +29848,7 @@ function dumpHistorianResponse(sessionId, label, text) {
|
|
|
29051
29848
|
mkdirSync2(HISTORIAN_RESPONSE_DUMP_DIR, { recursive: true });
|
|
29052
29849
|
const safeSessionId = sanitizeDumpName(sessionId);
|
|
29053
29850
|
const safeLabel = sanitizeDumpName(label);
|
|
29054
|
-
const dumpPath =
|
|
29851
|
+
const dumpPath = join11(HISTORIAN_RESPONSE_DUMP_DIR, `${safeSessionId}-${safeLabel}-${Date.now()}.xml`);
|
|
29055
29852
|
writeFileSync(dumpPath, text, "utf8");
|
|
29056
29853
|
sessionLog(sessionId, "compartment agent: historian response dumped", {
|
|
29057
29854
|
label,
|
|
@@ -29200,11 +29997,15 @@ No new compartments or facts were written. Check the historian model/output and
|
|
|
29200
29997
|
appendCompartments(db, sessionId, newCompartments);
|
|
29201
29998
|
replaceSessionFacts(db, sessionId, validatedPass.facts ?? []);
|
|
29202
29999
|
})();
|
|
30000
|
+
clearInjectionCache(sessionId);
|
|
29203
30001
|
if (deps.directory) {
|
|
29204
30002
|
promoteSessionFactsToMemory(db, sessionId, resolveProjectIdentity(deps.directory), validatedPass.facts ?? []);
|
|
29205
30003
|
}
|
|
29206
30004
|
const lastCompartmentEnd = lastNewEnd;
|
|
29207
30005
|
queueDropsForCompartmentalizedMessages(db, sessionId, lastCompartmentEnd);
|
|
30006
|
+
if (deps.experimentalCompactionMarkers) {
|
|
30007
|
+
updateCompactionMarkerAfterPublication(db, sessionId, lastCompartmentEnd, sessionDirectory);
|
|
30008
|
+
}
|
|
29208
30009
|
if (deps.historyBudgetTokens && deps.historyBudgetTokens > 0) {
|
|
29209
30010
|
await runCompressionPassIfNeeded({
|
|
29210
30011
|
client,
|
|
@@ -29218,6 +30019,20 @@ No new compartments or facts were written. Check the historian model/output and
|
|
|
29218
30019
|
updateSessionMeta(db, sessionId, { compartmentInProgress: false });
|
|
29219
30020
|
completedSuccessfully = true;
|
|
29220
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
|
+
}
|
|
29221
30036
|
} catch (error48) {
|
|
29222
30037
|
const msg = getErrorMessage(error48);
|
|
29223
30038
|
if (!issueNotified) {
|
|
@@ -29258,6 +30073,7 @@ async function executeContextRecompInternal(deps) {
|
|
|
29258
30073
|
const promoted2 = promoteRecompStaging(db, sessionId);
|
|
29259
30074
|
if (!promoted2)
|
|
29260
30075
|
return null;
|
|
30076
|
+
clearInjectionCache(sessionId);
|
|
29261
30077
|
if (deps.directory) {
|
|
29262
30078
|
promoteSessionFactsToMemory(db, sessionId, resolveProjectIdentity(deps.directory), promoted2.facts);
|
|
29263
30079
|
}
|
|
@@ -29265,6 +30081,9 @@ async function executeContextRecompInternal(deps) {
|
|
|
29265
30081
|
if (lastCompartmentEnd2 > 0) {
|
|
29266
30082
|
queueDropsForCompartmentalizedMessages(db, sessionId, lastCompartmentEnd2);
|
|
29267
30083
|
}
|
|
30084
|
+
if (deps.experimentalCompactionMarkers && lastCompartmentEnd2 > 0) {
|
|
30085
|
+
updateCompactionMarkerAfterPublication(db, sessionId, lastCompartmentEnd2, deps.directory);
|
|
30086
|
+
}
|
|
29268
30087
|
return [
|
|
29269
30088
|
`Persisted ${promoted2.compartments.length} compartment${promoted2.compartments.length === 1 ? "" : "s"} from ${passCount} successful pass${passCount === 1 ? "" : "es"}.`,
|
|
29270
30089
|
`Covered raw history 1-${lastCompartmentEnd2} out of ${rawMessageCount} total messages.`,
|
|
@@ -29419,6 +30238,7 @@ Nothing was written.`;
|
|
|
29419
30238
|
replaceAllCompartmentState(db, sessionId, candidateCompartments, candidateFacts);
|
|
29420
30239
|
clearRecompStaging(db, sessionId);
|
|
29421
30240
|
}
|
|
30241
|
+
clearInjectionCache(sessionId);
|
|
29422
30242
|
const finalCompartments = promoted?.compartments ?? candidateCompartments;
|
|
29423
30243
|
const finalFacts = promoted?.facts ?? candidateFacts;
|
|
29424
30244
|
if (deps.directory) {
|
|
@@ -29548,7 +30368,9 @@ async function runCompartmentPhase(args) {
|
|
|
29548
30368
|
historyBudgetTokens: args.historyBudgetTokens,
|
|
29549
30369
|
historianTimeoutMs: args.historianTimeoutMs,
|
|
29550
30370
|
directory: args.compartmentDirectory,
|
|
29551
|
-
getNotificationParams: args.getNotificationParams
|
|
30371
|
+
getNotificationParams: args.getNotificationParams,
|
|
30372
|
+
experimentalCompactionMarkers: args.experimentalCompactionMarkers,
|
|
30373
|
+
experimentalUserMemories: args.experimentalUserMemories
|
|
29552
30374
|
});
|
|
29553
30375
|
compartmentInProgress = true;
|
|
29554
30376
|
}
|
|
@@ -29574,7 +30396,9 @@ async function runCompartmentPhase(args) {
|
|
|
29574
30396
|
historyBudgetTokens: args.historyBudgetTokens,
|
|
29575
30397
|
historianTimeoutMs: args.historianTimeoutMs,
|
|
29576
30398
|
directory: args.compartmentDirectory,
|
|
29577
|
-
getNotificationParams: args.getNotificationParams
|
|
30399
|
+
getNotificationParams: args.getNotificationParams,
|
|
30400
|
+
experimentalCompactionMarkers: args.experimentalCompactionMarkers,
|
|
30401
|
+
experimentalUserMemories: args.experimentalUserMemories
|
|
29578
30402
|
});
|
|
29579
30403
|
activeRun = getActiveCompartmentRun(args.sessionId);
|
|
29580
30404
|
} else if (!activeRun && hasEligibleHistoryForCompartment()) {
|
|
@@ -29588,7 +30412,7 @@ async function runCompartmentPhase(args) {
|
|
|
29588
30412
|
}
|
|
29589
30413
|
if (args.cacheAlreadyBusting && args.historyBudgetTokens && args.historyBudgetTokens > 0 && args.client && !compartmentInProgress && !awaitedCompartmentRun) {
|
|
29590
30414
|
try {
|
|
29591
|
-
await runCompressionPassIfNeeded({
|
|
30415
|
+
const compressed = await runCompressionPassIfNeeded({
|
|
29592
30416
|
client: args.client,
|
|
29593
30417
|
db: args.db,
|
|
29594
30418
|
sessionId: args.sessionId,
|
|
@@ -29596,6 +30420,9 @@ async function runCompartmentPhase(args) {
|
|
|
29596
30420
|
historyBudgetTokens: args.historyBudgetTokens,
|
|
29597
30421
|
historianTimeoutMs: args.historianTimeoutMs
|
|
29598
30422
|
});
|
|
30423
|
+
if (compressed && args.projectPath !== undefined) {
|
|
30424
|
+
pendingCompartmentInjection = prepareCompartmentInjection(args.db, args.sessionId, args.messages, true, args.projectPath, args.injectionBudgetTokens);
|
|
30425
|
+
}
|
|
29599
30426
|
} catch (error48) {
|
|
29600
30427
|
sessionLog(args.sessionId, "transform: independent compressor check failed:", getErrorMessage(error48));
|
|
29601
30428
|
}
|
|
@@ -30743,7 +31570,18 @@ function runPostTransformPhase(args) {
|
|
|
30743
31570
|
if (pendingUserTurnReminder.messageId) {
|
|
30744
31571
|
const reinjected = appendReminderToUserMessageById(args.messages, pendingUserTurnReminder.messageId, pendingUserTurnReminder.text);
|
|
30745
31572
|
if (!reinjected) {
|
|
30746
|
-
|
|
31573
|
+
if (isCacheBustingPass) {
|
|
31574
|
+
const newAnchorId = appendReminderToLatestUserMessage(args.messages, pendingUserTurnReminder.text);
|
|
31575
|
+
if (newAnchorId) {
|
|
31576
|
+
setPersistedStickyTurnReminder(args.db, args.sessionId, pendingUserTurnReminder.text, newAnchorId);
|
|
31577
|
+
sessionLog(args.sessionId, `sticky turn reminder re-anchored: ${pendingUserTurnReminder.messageId} \u2192 ${newAnchorId}`);
|
|
31578
|
+
} else {
|
|
31579
|
+
clearPersistedStickyTurnReminder(args.db, args.sessionId);
|
|
31580
|
+
sessionLog(args.sessionId, `sticky turn reminder cleared \u2014 anchor ${pendingUserTurnReminder.messageId} gone and no user message visible`);
|
|
31581
|
+
}
|
|
31582
|
+
} else {
|
|
31583
|
+
sessionLog(args.sessionId, `preserving sticky turn reminder anchor to avoid cache bust: messageId=${pendingUserTurnReminder.messageId}`);
|
|
31584
|
+
}
|
|
30747
31585
|
}
|
|
30748
31586
|
} else {
|
|
30749
31587
|
const anchoredMessageId = appendReminderToLatestUserMessage(args.messages, pendingUserTurnReminder.text);
|
|
@@ -30780,7 +31618,17 @@ function runPostTransformPhase(args) {
|
|
|
30780
31618
|
if (stickyNoteNudge) {
|
|
30781
31619
|
const reinjected = appendReminderToUserMessageById(args.messages, stickyNoteNudge.messageId, stickyNoteNudge.text);
|
|
30782
31620
|
if (!reinjected) {
|
|
30783
|
-
|
|
31621
|
+
if (isCacheBustingPass) {
|
|
31622
|
+
const newAnchorId = appendReminderToLatestUserMessage(args.messages, stickyNoteNudge.text);
|
|
31623
|
+
if (newAnchorId) {
|
|
31624
|
+
markNoteNudgeDelivered(args.db, args.sessionId, stickyNoteNudge.text, newAnchorId);
|
|
31625
|
+
sessionLog(args.sessionId, `sticky note nudge re-anchored: ${stickyNoteNudge.messageId} \u2192 ${newAnchorId}`);
|
|
31626
|
+
} else {
|
|
31627
|
+
sessionLog(args.sessionId, `sticky note nudge anchor ${stickyNoteNudge.messageId} gone \u2014 no user message visible to re-anchor`);
|
|
31628
|
+
}
|
|
31629
|
+
} else {
|
|
31630
|
+
sessionLog(args.sessionId, `preserving sticky note nudge anchor to avoid cache bust: messageId=${stickyNoteNudge.messageId}`);
|
|
31631
|
+
}
|
|
30784
31632
|
}
|
|
30785
31633
|
}
|
|
30786
31634
|
const deferredNoteText = peekNoteNudgeText(args.db, args.sessionId, args.currentTurnId, args.projectPath);
|
|
@@ -30855,7 +31703,7 @@ function createTransform(deps) {
|
|
|
30855
31703
|
const canRunCompartments = fullFeatureMode && deps.client !== undefined && compartmentDirectory.length > 0;
|
|
30856
31704
|
const contextUsageEarly = loadContextUsage(deps.contextUsageMap, db, sessionId);
|
|
30857
31705
|
const schedulerDecisionEarly = resolveSchedulerDecision(deps.scheduler, sessionMeta, contextUsageEarly, sessionId, deps.getModelKey?.(sessionId));
|
|
30858
|
-
const isCacheBusting = deps.flushedSessions.has(sessionId)
|
|
31706
|
+
const isCacheBusting = deps.flushedSessions.has(sessionId);
|
|
30859
31707
|
let pendingCompartmentInjection = null;
|
|
30860
31708
|
if (fullFeatureMode) {
|
|
30861
31709
|
const projectPath = deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined;
|
|
@@ -30946,7 +31794,9 @@ function createTransform(deps) {
|
|
|
30946
31794
|
projectPath: deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined,
|
|
30947
31795
|
injectionBudgetTokens: deps.memoryConfig?.injectionBudgetTokens,
|
|
30948
31796
|
getNotificationParams: rawGetNotifParams ? () => rawGetNotifParams(sessionId) : undefined,
|
|
30949
|
-
cacheAlreadyBusting: isCacheBusting
|
|
31797
|
+
cacheAlreadyBusting: isCacheBusting || schedulerDecisionEarly === "execute",
|
|
31798
|
+
experimentalCompactionMarkers: deps.experimentalCompactionMarkers,
|
|
31799
|
+
experimentalUserMemories: deps.experimentalUserMemories
|
|
30950
31800
|
});
|
|
30951
31801
|
pendingCompartmentInjection = compartmentPhase.pendingCompartmentInjection;
|
|
30952
31802
|
const awaitedCompartmentRun = compartmentPhase.awaitedCompartmentRun;
|
|
@@ -31136,7 +31986,7 @@ function createToolExecuteAfterHook(args) {
|
|
|
31136
31986
|
|
|
31137
31987
|
// src/hooks/magic-context/system-prompt-hash.ts
|
|
31138
31988
|
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
31139
|
-
import { join as
|
|
31989
|
+
import { join as join12 } from "path";
|
|
31140
31990
|
|
|
31141
31991
|
// src/agents/magic-context-prompt.ts
|
|
31142
31992
|
var BASE_INTRO = (protectedTags) => `Messages and tool outputs are tagged with \xA7N\xA7 identifiers (e.g., \xA71\xA7, \xA742\xA7).
|
|
@@ -31337,11 +32187,13 @@ Prefer many small targeted operations over one large blanket operation. Compress
|
|
|
31337
32187
|
init_logger();
|
|
31338
32188
|
var MAGIC_CONTEXT_MARKER = "## Magic Context";
|
|
31339
32189
|
var PROJECT_DOCS_MARKER = "<project-docs>";
|
|
32190
|
+
var USER_PROFILE_MARKER = "<user-profile>";
|
|
32191
|
+
var cachedUserProfileBySession = new Map;
|
|
31340
32192
|
var DOC_FILES = ["ARCHITECTURE.md", "STRUCTURE.md"];
|
|
31341
32193
|
function readProjectDocs(directory) {
|
|
31342
32194
|
const sections = [];
|
|
31343
32195
|
for (const filename of DOC_FILES) {
|
|
31344
|
-
const filePath =
|
|
32196
|
+
const filePath = join12(directory, filename);
|
|
31345
32197
|
try {
|
|
31346
32198
|
if (existsSync5(filePath)) {
|
|
31347
32199
|
const content = readFileSync4(filePath, "utf-8").trim();
|
|
@@ -31396,6 +32248,28 @@ function createSystemPromptHashHandler(deps) {
|
|
|
31396
32248
|
output.system.push(docsBlock);
|
|
31397
32249
|
}
|
|
31398
32250
|
}
|
|
32251
|
+
if (deps.experimentalUserMemories) {
|
|
32252
|
+
const hasCachedProfile = cachedUserProfileBySession.has(sessionId);
|
|
32253
|
+
if (!hasCachedProfile || isCacheBusting) {
|
|
32254
|
+
const memories = getActiveUserMemories(deps.db);
|
|
32255
|
+
if (memories.length > 0) {
|
|
32256
|
+
const items = memories.map((m) => `- ${m.content}`).join(`
|
|
32257
|
+
`);
|
|
32258
|
+
cachedUserProfileBySession.set(sessionId, `${USER_PROFILE_MARKER}
|
|
32259
|
+
${items}
|
|
32260
|
+
</user-profile>`);
|
|
32261
|
+
if (!hasCachedProfile) {
|
|
32262
|
+
sessionLog(sessionId, `loaded ${memories.length} user profile memorie(s)`);
|
|
32263
|
+
}
|
|
32264
|
+
} else {
|
|
32265
|
+
cachedUserProfileBySession.set(sessionId, null);
|
|
32266
|
+
}
|
|
32267
|
+
}
|
|
32268
|
+
const profileBlock = cachedUserProfileBySession.get(sessionId);
|
|
32269
|
+
if (profileBlock && !fullPrompt.includes(USER_PROFILE_MARKER)) {
|
|
32270
|
+
output.system.push(profileBlock);
|
|
32271
|
+
}
|
|
32272
|
+
}
|
|
31399
32273
|
const DATE_PATTERN = /Today's date: .+/;
|
|
31400
32274
|
for (let i = 0;i < output.system.length; i++) {
|
|
31401
32275
|
const match = output.system[i].match(DATE_PATTERN);
|
|
@@ -31529,7 +32403,9 @@ function createMagicContextHook(deps) {
|
|
|
31529
32403
|
const model = liveModelBySession.get(sessionId);
|
|
31530
32404
|
return resolveModelKey(model?.providerID, model?.modelID);
|
|
31531
32405
|
},
|
|
31532
|
-
projectPath
|
|
32406
|
+
projectPath,
|
|
32407
|
+
experimentalCompactionMarkers: deps.config.experimental?.compaction_markers,
|
|
32408
|
+
experimentalUserMemories: deps.config.experimental?.user_memories?.enabled
|
|
31533
32409
|
});
|
|
31534
32410
|
const eventHandler = createEventHandler2({
|
|
31535
32411
|
contextUsageMap,
|
|
@@ -31564,7 +32440,11 @@ function createMagicContextHook(deps) {
|
|
|
31564
32440
|
client: deps.client,
|
|
31565
32441
|
tasks: dreaming.tasks,
|
|
31566
32442
|
taskTimeoutMinutes: dreaming.task_timeout_minutes,
|
|
31567
|
-
maxRuntimeMinutes: dreaming.max_runtime_minutes
|
|
32443
|
+
maxRuntimeMinutes: dreaming.max_runtime_minutes,
|
|
32444
|
+
experimentalUserMemories: deps.config.experimental?.user_memories?.enabled ? {
|
|
32445
|
+
enabled: true,
|
|
32446
|
+
promotionThreshold: deps.config.experimental.user_memories?.promotion_threshold
|
|
32447
|
+
} : undefined
|
|
31568
32448
|
}).catch((error48) => {
|
|
31569
32449
|
log("[dreamer] scheduled queue processing failed:", error48);
|
|
31570
32450
|
});
|
|
@@ -31606,7 +32486,11 @@ function createMagicContextHook(deps) {
|
|
|
31606
32486
|
config: deps.config.dreamer,
|
|
31607
32487
|
projectPath,
|
|
31608
32488
|
client: deps.client,
|
|
31609
|
-
directory: deps.directory
|
|
32489
|
+
directory: deps.directory,
|
|
32490
|
+
experimentalUserMemories: deps.config.experimental?.user_memories?.enabled ? {
|
|
32491
|
+
enabled: true,
|
|
32492
|
+
promotionThreshold: deps.config.experimental.user_memories?.promotion_threshold
|
|
32493
|
+
} : undefined
|
|
31610
32494
|
} : undefined
|
|
31611
32495
|
});
|
|
31612
32496
|
const emergencyNudgeFired = new Set;
|
|
@@ -31618,7 +32502,8 @@ function createMagicContextHook(deps) {
|
|
|
31618
32502
|
injectDocs: deps.config.dreamer?.inject_docs !== false,
|
|
31619
32503
|
directory: deps.directory,
|
|
31620
32504
|
flushedSessions,
|
|
31621
|
-
lastHeuristicsTurnId
|
|
32505
|
+
lastHeuristicsTurnId,
|
|
32506
|
+
experimentalUserMemories: deps.config.experimental?.user_memories?.enabled
|
|
31622
32507
|
});
|
|
31623
32508
|
const eventHook = createEventHook({
|
|
31624
32509
|
eventHandler,
|
|
@@ -31695,7 +32580,8 @@ function createSessionHooks(args) {
|
|
|
31695
32580
|
historian_timeout_ms: pluginConfig.historian_timeout_ms,
|
|
31696
32581
|
memory: pluginConfig.memory,
|
|
31697
32582
|
sidekick: pluginConfig.sidekick,
|
|
31698
|
-
dreamer: pluginConfig.dreamer
|
|
32583
|
+
dreamer: pluginConfig.dreamer,
|
|
32584
|
+
experimental: pluginConfig.experimental
|
|
31699
32585
|
}
|
|
31700
32586
|
})
|
|
31701
32587
|
};
|
|
@@ -32078,9 +32964,9 @@ Use this for short goals, constraints, decisions, or reminders worth carrying fo
|
|
|
32078
32964
|
|
|
32079
32965
|
Actions:
|
|
32080
32966
|
- \`write\`: Append one note. Optionally provide \`surface_condition\` to create a smart note.
|
|
32081
|
-
- \`read\`: Show current notes
|
|
32082
|
-
- \`
|
|
32083
|
-
- \`
|
|
32967
|
+
- \`read\`: Show current notes. Defaults to active session notes + ready smart notes; use \`filter\` to inspect all, pending, ready, active, or dismissed notes.
|
|
32968
|
+
- \`dismiss\`: Dismiss a note by \`note_id\`.
|
|
32969
|
+
- \`update\`: Update a note by \`note_id\`.
|
|
32084
32970
|
|
|
32085
32971
|
**Smart Notes**: When \`surface_condition\` is provided with \`write\`, the note becomes a project-scoped smart note.
|
|
32086
32972
|
The dreamer evaluates smart note conditions during nightly runs and surfaces them when conditions are met.
|
|
@@ -32089,14 +32975,79 @@ Example: \`ctx_note(action="write", content="Implement X because Y", surface_con
|
|
|
32089
32975
|
Historian reads these notes, deduplicates them, and rewrites the remaining useful notes over time.`;
|
|
32090
32976
|
// src/tools/ctx-note/tools.ts
|
|
32091
32977
|
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
32978
|
+
function formatNoteLine(note) {
|
|
32979
|
+
const statusSuffix = note.status === "active" ? "" : ` (${note.status})`;
|
|
32980
|
+
const dismissHint = note.status === "dismissed" ? "" : ` _(dismiss with \`ctx_note(action="dismiss", note_id=${note.id})\`)_`;
|
|
32981
|
+
if (note.type === "session") {
|
|
32982
|
+
return `- **#${note.id}**${statusSuffix}: ${note.content}${dismissHint}`;
|
|
32983
|
+
}
|
|
32984
|
+
const conditionText = note.status === "ready" ? note.readyReason ?? note.surfaceCondition ?? "Condition satisfied" : note.surfaceCondition ?? "No condition recorded";
|
|
32985
|
+
const conditionLabel = note.status === "ready" ? "Condition met" : "Condition";
|
|
32986
|
+
return `- **#${note.id}**${statusSuffix}: ${note.content}
|
|
32987
|
+
${conditionLabel}: ${conditionText}${dismissHint}`;
|
|
32988
|
+
}
|
|
32989
|
+
function buildReadSections(args) {
|
|
32990
|
+
if (args.filter === undefined) {
|
|
32991
|
+
const sessionNotes2 = getSessionNotes(args.db, args.sessionId);
|
|
32992
|
+
const readySmartNotes = args.projectIdentity ? getReadySmartNotes(args.db, args.projectIdentity) : [];
|
|
32993
|
+
const sections2 = [];
|
|
32994
|
+
if (sessionNotes2.length > 0) {
|
|
32995
|
+
sections2.push(`## Session Notes
|
|
32996
|
+
|
|
32997
|
+
${sessionNotes2.map((note) => formatNoteLine(note)).join(`
|
|
32998
|
+
`)}`);
|
|
32999
|
+
}
|
|
33000
|
+
if (readySmartNotes.length > 0) {
|
|
33001
|
+
sections2.push(`## \uD83D\uDD14 Ready Smart Notes
|
|
33002
|
+
|
|
33003
|
+
${readySmartNotes.map((note) => formatNoteLine(note)).join(`
|
|
33004
|
+
|
|
33005
|
+
`)}`);
|
|
33006
|
+
}
|
|
33007
|
+
return sections2;
|
|
33008
|
+
}
|
|
33009
|
+
const statusByFilter = {
|
|
33010
|
+
active: "active",
|
|
33011
|
+
all: ["active", "pending", "ready", "dismissed"],
|
|
33012
|
+
dismissed: "dismissed",
|
|
33013
|
+
pending: "pending",
|
|
33014
|
+
ready: "ready"
|
|
33015
|
+
};
|
|
33016
|
+
const sessionNotes = getNotes(args.db, {
|
|
33017
|
+
sessionId: args.sessionId,
|
|
33018
|
+
type: "session",
|
|
33019
|
+
status: statusByFilter[args.filter]
|
|
33020
|
+
});
|
|
33021
|
+
const smartNotes = args.projectIdentity ? getNotes(args.db, {
|
|
33022
|
+
projectPath: args.projectIdentity,
|
|
33023
|
+
type: "smart",
|
|
33024
|
+
status: statusByFilter[args.filter]
|
|
33025
|
+
}) : [];
|
|
33026
|
+
const sections = [];
|
|
33027
|
+
if (sessionNotes.length > 0) {
|
|
33028
|
+
sections.push(`## Session Notes
|
|
33029
|
+
|
|
33030
|
+
${sessionNotes.map((note) => formatNoteLine(note)).join(`
|
|
33031
|
+
`)}`);
|
|
33032
|
+
}
|
|
33033
|
+
if (smartNotes.length > 0) {
|
|
33034
|
+
sections.push(`## Smart Notes
|
|
33035
|
+
|
|
33036
|
+
${smartNotes.map((note) => formatNoteLine(note)).join(`
|
|
33037
|
+
|
|
33038
|
+
`)}`);
|
|
33039
|
+
}
|
|
33040
|
+
return sections;
|
|
33041
|
+
}
|
|
32092
33042
|
function createCtxNoteTool(deps) {
|
|
32093
33043
|
return tool3({
|
|
32094
33044
|
description: CTX_NOTE_DESCRIPTION,
|
|
32095
33045
|
args: {
|
|
32096
|
-
action: tool3.schema.enum(["write", "read", "
|
|
33046
|
+
action: tool3.schema.enum(["write", "read", "dismiss", "update"]).optional().describe("Operation to perform. Defaults to 'write' when content is provided, otherwise 'read'."),
|
|
32097
33047
|
content: tool3.schema.string().optional().describe("Note text to store when action is 'write'."),
|
|
32098
33048
|
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."),
|
|
32099
|
-
|
|
33049
|
+
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."),
|
|
33050
|
+
note_id: tool3.schema.number().optional().describe("Note ID (required for 'dismiss' and 'update' actions).")
|
|
32100
33051
|
},
|
|
32101
33052
|
async execute(args, toolContext) {
|
|
32102
33053
|
const sessionId = toolContext.sessionID;
|
|
@@ -32113,48 +33064,59 @@ function createCtxNoteTool(deps) {
|
|
|
32113
33064
|
if (!deps.projectIdentity) {
|
|
32114
33065
|
return "Error: Could not resolve project identity for smart note.";
|
|
32115
33066
|
}
|
|
32116
|
-
const
|
|
32117
|
-
|
|
33067
|
+
const note2 = addNote(deps.db, "smart", {
|
|
33068
|
+
content,
|
|
33069
|
+
projectPath: deps.projectIdentity,
|
|
33070
|
+
sessionId,
|
|
33071
|
+
surfaceCondition: args.surface_condition.trim()
|
|
33072
|
+
});
|
|
33073
|
+
return `Created smart note #${note2.id}. Dreamer will evaluate the condition during nightly runs:
|
|
32118
33074
|
- Content: ${content}
|
|
32119
33075
|
- Condition: ${args.surface_condition.trim()}`;
|
|
32120
33076
|
}
|
|
32121
|
-
|
|
32122
|
-
|
|
32123
|
-
return `Saved session note ${total}. Historian will rewrite or deduplicate notes as needed.`;
|
|
33077
|
+
const note = addNote(deps.db, "session", { sessionId, content });
|
|
33078
|
+
return `Saved session note #${note.id}. Historian will rewrite or deduplicate notes as needed.`;
|
|
32124
33079
|
}
|
|
32125
33080
|
if (action === "dismiss") {
|
|
32126
33081
|
const noteId = args.note_id;
|
|
32127
33082
|
if (typeof noteId !== "number") {
|
|
32128
33083
|
return "Error: 'note_id' is required when action is 'dismiss'.";
|
|
32129
33084
|
}
|
|
32130
|
-
const dismissed =
|
|
32131
|
-
return dismissed ? `
|
|
32132
|
-
}
|
|
32133
|
-
if (action === "clear") {
|
|
32134
|
-
const existing = getSessionNotes(deps.db, sessionId);
|
|
32135
|
-
clearSessionNotes(deps.db, sessionId);
|
|
32136
|
-
return existing.length === 0 ? "Session notes were already empty." : `Cleared ${existing.length} session note${existing.length === 1 ? "" : "s"}.`;
|
|
32137
|
-
}
|
|
32138
|
-
const notes = getSessionNotes(deps.db, sessionId);
|
|
32139
|
-
const readySmartNotes = deps.projectIdentity ? getReadySmartNotes(deps.db, deps.projectIdentity) : [];
|
|
32140
|
-
const sections = [];
|
|
32141
|
-
if (notes.length > 0) {
|
|
32142
|
-
const lines = notes.map((note, index) => `${index + 1}. ${note.content}`);
|
|
32143
|
-
sections.push(`## Session Notes
|
|
32144
|
-
|
|
32145
|
-
${lines.join(`
|
|
32146
|
-
`)}`);
|
|
32147
|
-
}
|
|
32148
|
-
if (readySmartNotes.length > 0) {
|
|
32149
|
-
const lines = readySmartNotes.map((n) => `- **#${n.id}**: ${n.content}
|
|
32150
|
-
Condition met: ${n.readyReason ?? n.surfaceCondition}
|
|
32151
|
-
_(dismiss with \`ctx_note(action="dismiss", note_id=${n.id})\`)_`);
|
|
32152
|
-
sections.push(`## \uD83D\uDD14 Ready Smart Notes
|
|
32153
|
-
|
|
32154
|
-
${lines.join(`
|
|
32155
|
-
|
|
32156
|
-
`)}`);
|
|
33085
|
+
const dismissed = dismissNote(deps.db, noteId);
|
|
33086
|
+
return dismissed ? `Note #${noteId} dismissed.` : `Note #${noteId} not found or already dismissed.`;
|
|
32157
33087
|
}
|
|
33088
|
+
if (action === "update") {
|
|
33089
|
+
const noteId = args.note_id;
|
|
33090
|
+
if (typeof noteId !== "number") {
|
|
33091
|
+
return "Error: 'note_id' is required when action is 'update'.";
|
|
33092
|
+
}
|
|
33093
|
+
const updates = {};
|
|
33094
|
+
if (args.content?.trim())
|
|
33095
|
+
updates.content = args.content.trim();
|
|
33096
|
+
if (args.surface_condition?.trim())
|
|
33097
|
+
updates.surfaceCondition = args.surface_condition.trim();
|
|
33098
|
+
if (!updates.content && !updates.surfaceCondition) {
|
|
33099
|
+
return "Error: Provide 'content' and/or 'surface_condition' to update.";
|
|
33100
|
+
}
|
|
33101
|
+
const updated = updateNote(deps.db, noteId, updates);
|
|
33102
|
+
if (!updated) {
|
|
33103
|
+
return `Note #${noteId} not found or has no compatible fields to update.`;
|
|
33104
|
+
}
|
|
33105
|
+
const parts = [];
|
|
33106
|
+
if (updates.content)
|
|
33107
|
+
parts.push(`Content: ${updates.content}`);
|
|
33108
|
+
if (updates.surfaceCondition)
|
|
33109
|
+
parts.push(`Condition: ${updates.surfaceCondition}`);
|
|
33110
|
+
return `Updated note #${noteId}:
|
|
33111
|
+
${parts.join(`
|
|
33112
|
+
`)}`;
|
|
33113
|
+
}
|
|
33114
|
+
const sections = buildReadSections({
|
|
33115
|
+
db: deps.db,
|
|
33116
|
+
filter: args.filter,
|
|
33117
|
+
projectIdentity: deps.projectIdentity,
|
|
33118
|
+
sessionId
|
|
33119
|
+
});
|
|
32158
33120
|
if (sections.length === 0) {
|
|
32159
33121
|
return `## Notes
|
|
32160
33122
|
|
|
@@ -32788,6 +33750,115 @@ function createToolRegistry(args) {
|
|
|
32788
33750
|
return allTools;
|
|
32789
33751
|
}
|
|
32790
33752
|
|
|
33753
|
+
// src/features/magic-context/plugin-messages.ts
|
|
33754
|
+
function isPluginMessageRow(row) {
|
|
33755
|
+
if (row === null || typeof row !== "object")
|
|
33756
|
+
return false;
|
|
33757
|
+
const r = row;
|
|
33758
|
+
return typeof r.id === "number" && typeof r.direction === "string" && typeof r.type === "string" && typeof r.payload === "string" && typeof r.created_at === "number";
|
|
33759
|
+
}
|
|
33760
|
+
function toPluginMessage(row) {
|
|
33761
|
+
let payload = {};
|
|
33762
|
+
try {
|
|
33763
|
+
payload = JSON.parse(row.payload);
|
|
33764
|
+
} catch {}
|
|
33765
|
+
return {
|
|
33766
|
+
id: row.id,
|
|
33767
|
+
direction: row.direction,
|
|
33768
|
+
type: row.type,
|
|
33769
|
+
payload,
|
|
33770
|
+
sessionId: row.session_id,
|
|
33771
|
+
createdAt: row.created_at,
|
|
33772
|
+
consumedAt: row.consumed_at
|
|
33773
|
+
};
|
|
33774
|
+
}
|
|
33775
|
+
var CLEANUP_THRESHOLD_MS = 5 * 60 * 1000;
|
|
33776
|
+
function sendToTui(db, type, payload, sessionId) {
|
|
33777
|
+
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());
|
|
33778
|
+
return Number(result.lastInsertRowid);
|
|
33779
|
+
}
|
|
33780
|
+
function consumeMessages(db, direction, options) {
|
|
33781
|
+
const now = Date.now();
|
|
33782
|
+
const conditions = ["direction = ?", "consumed_at IS NULL"];
|
|
33783
|
+
const params = [direction];
|
|
33784
|
+
if (options?.type) {
|
|
33785
|
+
conditions.push("type = ?");
|
|
33786
|
+
params.push(options.type);
|
|
33787
|
+
}
|
|
33788
|
+
if (options?.sessionId) {
|
|
33789
|
+
conditions.push("session_id = ?");
|
|
33790
|
+
params.push(options.sessionId);
|
|
33791
|
+
}
|
|
33792
|
+
const query = `SELECT * FROM plugin_messages WHERE ${conditions.join(" AND ")} ORDER BY created_at ASC`;
|
|
33793
|
+
const rows = db.prepare(query).all(...params);
|
|
33794
|
+
const messages = rows.filter(isPluginMessageRow).map(toPluginMessage);
|
|
33795
|
+
if (messages.length > 0) {
|
|
33796
|
+
const ids = messages.map((m) => m.id);
|
|
33797
|
+
db.prepare(`UPDATE plugin_messages SET consumed_at = ? WHERE id IN (${ids.map(() => "?").join(",")})`).run(now, ...ids);
|
|
33798
|
+
}
|
|
33799
|
+
db.prepare("DELETE FROM plugin_messages WHERE created_at < ?").run(now - CLEANUP_THRESHOLD_MS);
|
|
33800
|
+
return messages;
|
|
33801
|
+
}
|
|
33802
|
+
function sendTuiToast(db, message, options) {
|
|
33803
|
+
return sendToTui(db, "toast", {
|
|
33804
|
+
message,
|
|
33805
|
+
variant: options?.variant ?? "info",
|
|
33806
|
+
duration: options?.duration ?? 5000
|
|
33807
|
+
}, options?.sessionId);
|
|
33808
|
+
}
|
|
33809
|
+
|
|
33810
|
+
// src/plugin/tui-action-consumer.ts
|
|
33811
|
+
init_logger();
|
|
33812
|
+
var DEFAULT_COMPARTMENT_TOKEN_BUDGET2 = 20000;
|
|
33813
|
+
var DEFAULT_HISTORIAN_TIMEOUT_MS2 = 10 * 60 * 1000;
|
|
33814
|
+
var TUI_ACTION_POLL_INTERVAL_MS = 2000;
|
|
33815
|
+
function startTuiActionConsumer(args) {
|
|
33816
|
+
const { client, directory, config: config2 } = args;
|
|
33817
|
+
const timer = setInterval(() => {
|
|
33818
|
+
try {
|
|
33819
|
+
const db = openDatabase();
|
|
33820
|
+
const actions = consumeMessages(db, "tui_to_server", { type: "action" });
|
|
33821
|
+
for (const msg of actions) {
|
|
33822
|
+
const command = msg.payload.command;
|
|
33823
|
+
const sessionId = msg.sessionId;
|
|
33824
|
+
if (command === "recomp" && sessionId) {
|
|
33825
|
+
log(`[magic-context] TUI action: recomp requested for session ${sessionId}`);
|
|
33826
|
+
sendTuiToast(db, "Historian recomp started", {
|
|
33827
|
+
variant: "info",
|
|
33828
|
+
sessionId
|
|
33829
|
+
});
|
|
33830
|
+
executeContextRecomp({
|
|
33831
|
+
client,
|
|
33832
|
+
db,
|
|
33833
|
+
sessionId,
|
|
33834
|
+
tokenBudget: config2.compartment_token_budget ?? DEFAULT_COMPARTMENT_TOKEN_BUDGET2,
|
|
33835
|
+
historianTimeoutMs: config2.historian_timeout_ms ?? DEFAULT_HISTORIAN_TIMEOUT_MS2,
|
|
33836
|
+
directory,
|
|
33837
|
+
getNotificationParams: () => ({})
|
|
33838
|
+
}).then((result) => {
|
|
33839
|
+
sendTuiToast(db, "Recomp completed", { variant: "success", sessionId });
|
|
33840
|
+
sendIgnoredMessage(client, sessionId, result, {}).catch(() => {});
|
|
33841
|
+
}).catch((error48) => {
|
|
33842
|
+
log("[magic-context] TUI recomp failed:", error48);
|
|
33843
|
+
sendTuiToast(db, `Recomp failed: ${error48 instanceof Error ? error48.message : "unknown error"}`, { variant: "error", sessionId });
|
|
33844
|
+
});
|
|
33845
|
+
} else {
|
|
33846
|
+
log(`[magic-context] TUI action: unknown command=${String(command)} session=${String(sessionId)}`);
|
|
33847
|
+
}
|
|
33848
|
+
}
|
|
33849
|
+
} catch (error48) {
|
|
33850
|
+
log("[magic-context] TUI action consumer error:", error48);
|
|
33851
|
+
}
|
|
33852
|
+
}, TUI_ACTION_POLL_INTERVAL_MS);
|
|
33853
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
33854
|
+
timer.unref();
|
|
33855
|
+
}
|
|
33856
|
+
log("[magic-context] started TUI action consumer (2s poll)");
|
|
33857
|
+
return () => {
|
|
33858
|
+
clearInterval(timer);
|
|
33859
|
+
};
|
|
33860
|
+
}
|
|
33861
|
+
|
|
32791
33862
|
// src/index.ts
|
|
32792
33863
|
init_conflict_detector();
|
|
32793
33864
|
init_logger();
|
|
@@ -32817,7 +33888,16 @@ var plugin = async (ctx) => {
|
|
|
32817
33888
|
client: ctx.client,
|
|
32818
33889
|
dreamerConfig: pluginConfig.dreamer,
|
|
32819
33890
|
embeddingConfig: pluginConfig.embedding,
|
|
32820
|
-
memoryEnabled: pluginConfig.memory?.enabled === true
|
|
33891
|
+
memoryEnabled: pluginConfig.memory?.enabled === true,
|
|
33892
|
+
experimentalUserMemories: pluginConfig.experimental?.user_memories?.enabled ? {
|
|
33893
|
+
enabled: true,
|
|
33894
|
+
promotionThreshold: pluginConfig.experimental.user_memories?.promotion_threshold
|
|
33895
|
+
} : undefined
|
|
33896
|
+
});
|
|
33897
|
+
startTuiActionConsumer({
|
|
33898
|
+
client: ctx.client,
|
|
33899
|
+
directory: ctx.directory,
|
|
33900
|
+
config: pluginConfig
|
|
32821
33901
|
});
|
|
32822
33902
|
}
|
|
32823
33903
|
if (conflictResult?.hasConflict) {
|
|
@@ -32897,7 +33977,7 @@ var plugin = async (ctx) => {
|
|
|
32897
33977
|
config2.agent = {
|
|
32898
33978
|
...config2.agent ?? {},
|
|
32899
33979
|
[DREAMER_AGENT]: buildHiddenAgentConfig(DREAMER_AGENT, DREAMER_SYSTEM_PROMPT, dreamerAgentOverrides),
|
|
32900
|
-
[HISTORIAN_AGENT]: buildHiddenAgentConfig(HISTORIAN_AGENT, COMPARTMENT_AGENT_SYSTEM_PROMPT, pluginConfig.historian),
|
|
33980
|
+
[HISTORIAN_AGENT]: buildHiddenAgentConfig(HISTORIAN_AGENT, pluginConfig.experimental?.user_memories?.enabled ? COMPARTMENT_AGENT_SYSTEM_PROMPT + USER_OBSERVATIONS_APPENDIX : COMPARTMENT_AGENT_SYSTEM_PROMPT, pluginConfig.historian),
|
|
32901
33981
|
[SIDEKICK_AGENT]: buildHiddenAgentConfig(SIDEKICK_AGENT, SIDEKICK_SYSTEM_PROMPT, sidekickAgentOverrides)
|
|
32902
33982
|
};
|
|
32903
33983
|
}
|