@cortexkit/opencode-magic-context 0.5.2 → 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.
Files changed (73) hide show
  1. package/README.md +18 -2
  2. package/dist/cli/doctor.d.ts.map +1 -1
  3. package/dist/cli.js +14 -0
  4. package/dist/config/schema/magic-context.d.ts +21 -0
  5. package/dist/config/schema/magic-context.d.ts.map +1 -1
  6. package/dist/features/magic-context/compaction-marker.d.ts +72 -0
  7. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -0
  8. package/dist/features/magic-context/dreamer/runner.d.ts +8 -0
  9. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  10. package/dist/features/magic-context/dreamer/scheduler.d.ts.map +1 -1
  11. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  12. package/dist/features/magic-context/migrations.d.ts +8 -0
  13. package/dist/features/magic-context/migrations.d.ts.map +1 -0
  14. package/dist/features/magic-context/plugin-messages.d.ts +75 -0
  15. package/dist/features/magic-context/plugin-messages.d.ts.map +1 -0
  16. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  17. package/dist/features/magic-context/storage-meta-persisted.d.ts +10 -0
  18. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  19. package/dist/features/magic-context/storage-notes.d.ts +51 -5
  20. package/dist/features/magic-context/storage-notes.d.ts.map +1 -1
  21. package/dist/features/magic-context/storage.d.ts +1 -2
  22. package/dist/features/magic-context/storage.d.ts.map +1 -1
  23. package/dist/features/magic-context/user-memory/review-user-memories.d.ts +20 -0
  24. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -0
  25. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +33 -0
  26. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -0
  27. package/dist/hooks/magic-context/command-handler.d.ts +4 -0
  28. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  29. package/dist/hooks/magic-context/compaction-marker-manager.d.ts +27 -0
  30. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -0
  31. package/dist/hooks/magic-context/compartment-parser.d.ts +1 -0
  32. package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/compartment-prompt.d.ts +4 -1
  34. package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
  35. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/compartment-runner-types.d.ts +5 -0
  39. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  40. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  42. package/dist/hooks/magic-context/hook.d.ts +7 -0
  43. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/note-nudger.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/system-prompt-hash.d.ts +2 -0
  48. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +4 -0
  50. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  51. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  52. package/dist/hooks/magic-context/transform.d.ts +2 -0
  53. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +1228 -149
  56. package/dist/plugin/dream-timer.d.ts +4 -0
  57. package/dist/plugin/dream-timer.d.ts.map +1 -1
  58. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  59. package/dist/plugin/tui-action-consumer.d.ts +13 -0
  60. package/dist/plugin/tui-action-consumer.d.ts.map +1 -0
  61. package/dist/tools/ctx-note/constants.d.ts +1 -1
  62. package/dist/tools/ctx-note/constants.d.ts.map +1 -1
  63. package/dist/tools/ctx-note/tools.d.ts.map +1 -1
  64. package/dist/tools/ctx-note/types.d.ts +3 -1
  65. package/dist/tools/ctx-note/types.d.ts.map +1 -1
  66. package/dist/tui/data/context-db.d.ts +20 -0
  67. package/dist/tui/data/context-db.d.ts.map +1 -1
  68. package/package.json +1 -1
  69. package/src/tui/data/context-db.ts +114 -6
  70. package/src/tui/index.tsx +77 -2
  71. package/src/tui/slots/sidebar-content.tsx +3 -2
  72. package/dist/features/magic-context/storage-smart-notes.d.ts +0 -24
  73. 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 join12 = (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;
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 join12(comment, inside, gap);
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 = join12(after_comma, process_comments(value, BEFORE(i), deeper_gap), deeper_gap);
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 += join12(after_comma, process_comments(value, PREFIX_AFTER, deeper_gap), deeper_gap);
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 = join12(after_comma, process_comments(value, BEFORE(key), deeper_gap), deeper_gap);
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 += join12(after_comma, process_comments(value, PREFIX_AFTER, deeper_gap), deeper_gap);
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 join12 } from "path";
8482
+ import { dirname, join as join13 } from "path";
8483
8483
  function resolveTuiConfigPath() {
8484
8484
  const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
8485
- const jsoncPath = join12(configDir, "tui.jsonc");
8486
- const jsonPath = join12(configDir, "tui.json");
8485
+ const jsoncPath = join13(configDir, "tui.jsonc");
8486
+ const jsonPath = join13(configDir, "tui.json");
8487
8487
  if (existsSync6(jsoncPath))
8488
8488
  return jsoncPath;
8489
8489
  if (existsSync6(jsonPath))
@@ -22197,6 +22197,16 @@ var MagicContextConfigSchema = exports_external.object({
22197
22197
  provider: "local",
22198
22198
  model: DEFAULT_LOCAL_EMBEDDING_MODEL
22199
22199
  }),
22200
+ experimental: exports_external.object({
22201
+ compaction_markers: exports_external.boolean().default(false),
22202
+ user_memories: exports_external.object({
22203
+ enabled: exports_external.boolean().default(false),
22204
+ promotion_threshold: exports_external.number().min(2).max(20).default(3)
22205
+ }).default({ enabled: false, promotion_threshold: 3 })
22206
+ }).default({
22207
+ compaction_markers: false,
22208
+ user_memories: { enabled: false, promotion_threshold: 3 }
22209
+ }),
22200
22210
  memory: exports_external.object({
22201
22211
  enabled: exports_external.boolean().default(true),
22202
22212
  injection_budget_tokens: exports_external.number().min(500).max(20000).default(4000),
@@ -23262,6 +23272,22 @@ More summary text.
23262
23272
  </output>
23263
23273
 
23264
23274
  Omit empty fact categories. Compartments must be ordered, contiguous for the ranges they cover, and non-overlapping.`;
23275
+ var USER_OBSERVATIONS_APPENDIX = `
23276
+
23277
+ User observation rules (EXPERIMENTAL):
23278
+ - After outputting compartments and facts, also output a <user_observations> section.
23279
+ - User observations capture UNIVERSAL behavioral patterns about the human user \u2014 not project-specific or technical.
23280
+ - Good observations: communication preferences, review focus areas, expertise level, decision-making patterns, frustration triggers, working style.
23281
+ - Bad observations (DO NOT emit): project-specific preferences, framework choices, coding language preferences, one-off moods, task-local frustration.
23282
+ - Each observation must be a single concise sentence in present tense.
23283
+ - Only emit observations you have strong evidence for from the conversation. Do not speculate.
23284
+ - Emit 0-5 observations per run. Zero is fine if nothing stands out.
23285
+ - The output shape inside <output> gains an additional section:
23286
+ <user_observations>
23287
+ * User prefers terse communication and dislikes verbose explanations.
23288
+ * User is technically deep \u2014 understands cache invalidation, SQLite internals, and prompt engineering.
23289
+ </user_observations>
23290
+ If no observations, omit the <user_observations> section entirely.`;
23265
23291
  function buildCompressorPrompt(compartments, currentTokens, targetTokens, averageDepth = 0) {
23266
23292
  const lines = [];
23267
23293
  lines.push(`These ${compartments.length} compartments use approximately ${currentTokens} tokens. Compress them to approximately ${targetTokens} tokens.`);
@@ -23290,7 +23316,7 @@ function buildCompressorPrompt(compartments, currentTokens, targetTokens, averag
23290
23316
  return lines.join(`
23291
23317
  `);
23292
23318
  }
23293
- function buildCompartmentAgentPrompt(existingState, inputSource) {
23319
+ function buildCompartmentAgentPrompt(existingState, inputSource, options) {
23294
23320
  return [
23295
23321
  "Existing state (read-only context for continuity and fact normalization \u2014 do NOT re-emit these compartments):",
23296
23322
  existingState,
@@ -23305,7 +23331,10 @@ function buildCompartmentAgentPrompt(existingState, inputSource) {
23305
23331
  "- Rewrite every fact into terse, present-tense operational form. Merge semantic duplicates within each category.",
23306
23332
  "- Drop any session fact already covered by a project memory in the existing state.",
23307
23333
  "- Do not preserve prior narrative wording verbatim; if a fact is already canonical and still correct, keep or lightly normalize it.",
23308
- "- Drop obsolete or task-local facts."
23334
+ "- Drop obsolete or task-local facts.",
23335
+ ...options?.userMemoriesEnabled ? [
23336
+ "- Also emit <user_observations> with universal behavioral observations about the user (see system prompt for rules)."
23337
+ ] : []
23309
23338
  ].join(`
23310
23339
  `);
23311
23340
  }
@@ -23955,40 +23984,96 @@ function getMemoryCountsByStatus(db, projectPath) {
23955
23984
  return counts;
23956
23985
  }
23957
23986
 
23958
- // src/features/magic-context/storage-smart-notes.ts
23959
- function isSmartNoteRow(row) {
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) {
23960
23998
  if (row === null || typeof row !== "object")
23961
23999
  return false;
23962
- const r = row;
23963
- return typeof r.id === "number" && typeof r.project_path === "string" && typeof r.content === "string" && typeof r.surface_condition === "string" && typeof r.status === "string" && typeof r.created_at === "number" && typeof r.updated_at === "number";
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");
23964
24002
  }
23965
- function toSmartNote(row) {
24003
+ function toNote(row) {
23966
24004
  return {
23967
24005
  id: row.id,
23968
- projectPath: row.project_path,
23969
- content: row.content,
23970
- surfaceCondition: row.surface_condition,
24006
+ type: row.type,
23971
24007
  status: row.status,
23972
- createdSessionId: row.created_session_id && row.created_session_id.length > 0 ? row.created_session_id : null,
24008
+ content: row.content,
24009
+ sessionId: toNullableString(row.session_id),
24010
+ projectPath: toNullableString(row.project_path),
24011
+ surfaceCondition: toNullableString(row.surface_condition),
23973
24012
  createdAt: row.created_at,
23974
24013
  updatedAt: row.updated_at,
23975
- lastCheckedAt: row.last_checked_at,
23976
- readyAt: row.ready_at,
23977
- readyReason: row.ready_reason && row.ready_reason.length > 0 ? row.ready_reason : null
24014
+ lastCheckedAt: toNullableNumber(row.last_checked_at),
24015
+ readyAt: toNullableNumber(row.ready_at),
24016
+ readyReason: toNullableString(row.ready_reason)
23978
24017
  };
23979
24018
  }
23980
- function addSmartNote(db, projectPath, content, surfaceCondition, sessionId) {
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) {
23981
24061
  const now = Date.now();
23982
- const result = db.prepare("INSERT INTO smart_notes (project_path, content, surface_condition, status, created_session_id, created_at, updated_at) VALUES (?, ?, ?, 'pending', ?, ?, ?) RETURNING *").get(projectPath, content, surfaceCondition, sessionId ?? null, now, now);
23983
- if (!isSmartNoteRow(result)) {
23984
- throw new Error("[smart-notes] failed to insert smart note");
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");
23985
24065
  }
23986
- return toSmartNote(result);
24066
+ return toNote(result);
24067
+ }
24068
+ function getSessionNotes(db, sessionId) {
24069
+ return getNotes(db, { sessionId, type: "session", status: "active" });
23987
24070
  }
23988
24071
  function getSmartNotes(db, projectPath, status) {
23989
- const query = status ? "SELECT * FROM smart_notes WHERE project_path = ? AND status = ? ORDER BY created_at ASC" : "SELECT * FROM smart_notes WHERE project_path = ? AND status != 'dismissed' ORDER BY created_at ASC";
23990
- const params = status ? [projectPath, status] : [projectPath];
23991
- return db.prepare(query).all(...params).filter(isSmartNoteRow).map(toSmartNote);
24072
+ return getNotes(db, {
24073
+ projectPath,
24074
+ type: "smart",
24075
+ status: status ?? DEFAULT_SMART_STATUSES
24076
+ });
23992
24077
  }
23993
24078
  function getPendingSmartNotes(db, projectPath) {
23994
24079
  return getSmartNotes(db, projectPath, "pending");
@@ -23996,17 +24081,294 @@ function getPendingSmartNotes(db, projectPath) {
23996
24081
  function getReadySmartNotes(db, projectPath) {
23997
24082
  return getSmartNotes(db, projectPath, "ready");
23998
24083
  }
23999
- function markSmartNoteReady(db, noteId, readyReason) {
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) {
24000
24138
  const now = Date.now();
24001
- db.prepare("UPDATE smart_notes SET status = 'ready', ready_at = ?, ready_reason = ?, updated_at = ?, last_checked_at = ? WHERE id = ?").run(now, readyReason ?? null, now, now, noteId);
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);
24002
24140
  }
24003
- function markSmartNoteChecked(db, noteId) {
24141
+ function markNoteChecked(db, noteId) {
24004
24142
  const now = Date.now();
24005
- db.prepare("UPDATE smart_notes SET last_checked_at = ?, updated_at = ? WHERE id = ?").run(now, now, noteId);
24143
+ db.prepare("UPDATE notes SET last_checked_at = ?, updated_at = ? WHERE id = ? AND type = 'smart'").run(now, now, noteId);
24006
24144
  }
24007
- function dismissSmartNote(db, noteId) {
24008
- const result = db.prepare("UPDATE smart_notes SET status = 'dismissed', updated_at = ? WHERE id = ?").run(Date.now(), noteId);
24009
- return result.changes > 0;
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
+ }
24010
24372
  }
24011
24373
 
24012
24374
  // src/features/magic-context/dreamer/storage-dream-runs.ts
@@ -24186,6 +24548,24 @@ async function runDream(args) {
24186
24548
  }
24187
24549
  }
24188
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
+ }
24189
24569
  if (Date.now() <= deadline) {
24190
24570
  try {
24191
24571
  await evaluateSmartNotes({
@@ -24326,7 +24706,7 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
24326
24706
  if (!jsonMatch) {
24327
24707
  log("[dreamer] smart notes: no JSON array found in output, skipping");
24328
24708
  for (const note of pendingNotes)
24329
- markSmartNoteChecked(args.db, note.id);
24709
+ markNoteChecked(args.db, note.id);
24330
24710
  throw new Error("Smart note evaluation returned no JSON array.");
24331
24711
  }
24332
24712
  let evaluations;
@@ -24335,7 +24715,7 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
24335
24715
  } catch {
24336
24716
  log(`[dreamer] smart notes: failed to parse JSON from LLM output, marking all checked`);
24337
24717
  for (const note of pendingNotes)
24338
- markSmartNoteChecked(args.db, note.id);
24718
+ markNoteChecked(args.db, note.id);
24339
24719
  throw new Error("Smart note evaluation returned invalid JSON.");
24340
24720
  }
24341
24721
  let surfaced = 0;
@@ -24346,16 +24726,16 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
24346
24726
  if (!note)
24347
24727
  continue;
24348
24728
  if (evaluation.met) {
24349
- markSmartNoteReady(args.db, note.id, evaluation.reason);
24729
+ markNoteReady(args.db, note.id, evaluation.reason);
24350
24730
  surfaced++;
24351
24731
  log(`[dreamer] smart notes: #${note.id} condition MET \u2014 "${evaluation.reason ?? "condition satisfied"}"`);
24352
24732
  } else {
24353
- markSmartNoteChecked(args.db, note.id);
24733
+ markNoteChecked(args.db, note.id);
24354
24734
  }
24355
24735
  }
24356
24736
  for (const note of pendingNotes) {
24357
24737
  if (!evaluations.some((e) => e.id === note.id)) {
24358
- markSmartNoteChecked(args.db, note.id);
24738
+ markNoteChecked(args.db, note.id);
24359
24739
  }
24360
24740
  }
24361
24741
  const durationMs = Date.now() - taskStartedAt;
@@ -24409,7 +24789,8 @@ async function processDreamQueue(args) {
24409
24789
  tasks: args.tasks,
24410
24790
  taskTimeoutMinutes: args.taskTimeoutMinutes,
24411
24791
  maxRuntimeMinutes: args.maxRuntimeMinutes,
24412
- sessionDirectory: projectDirectory
24792
+ sessionDirectory: projectDirectory,
24793
+ experimentalUserMemories: args.experimentalUserMemories
24413
24794
  });
24414
24795
  } catch (error48) {
24415
24796
  log(`[dreamer] runDream threw for ${entry.projectIdentity}: ${getErrorMessage(error48)}`);
@@ -24461,7 +24842,8 @@ function isInScheduleWindow(schedule, now = new Date) {
24461
24842
  function findProjectsNeedingDream(db) {
24462
24843
  const projectRows = db.query(`SELECT DISTINCT project_path FROM memories WHERE status = 'active'
24463
24844
  UNION
24464
- SELECT DISTINCT project_path FROM smart_notes WHERE status = 'pending'
24845
+ SELECT DISTINCT project_path FROM notes
24846
+ WHERE type = 'smart' AND status = 'pending' AND project_path IS NOT NULL
24465
24847
  ORDER BY project_path`).all();
24466
24848
  const projects = [];
24467
24849
  for (const row of projectRows) {
@@ -24470,8 +24852,8 @@ function findProjectsNeedingDream(db) {
24470
24852
  const lastDreamAt = Number(lastDreamAtStr ?? fallbackStr ?? "0") || 0;
24471
24853
  const updatedMemories = db.query(`SELECT COUNT(*) as cnt FROM memories
24472
24854
  WHERE project_path = ? AND status = 'active' AND updated_at > ?`).get(row.project_path, lastDreamAt);
24473
- const pendingSmartNotes = db.query(`SELECT COUNT(*) as cnt FROM smart_notes
24474
- 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);
24475
24857
  if (updatedMemories && updatedMemories.cnt > 0 || pendingSmartNotes && pendingSmartNotes.cnt > 0) {
24476
24858
  projects.push(row.project_path);
24477
24859
  }
@@ -24518,6 +24900,22 @@ function cosineSimilarity(a, b) {
24518
24900
 
24519
24901
  // src/features/magic-context/memory/embedding-local.ts
24520
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
+ }
24521
24919
  function isArrayLikeNumber(value) {
24522
24920
  if (typeof value !== "object" || value === null || !("length" in value)) {
24523
24921
  return false;
@@ -24573,10 +24971,16 @@ class LocalEmbeddingProvider {
24573
24971
  this.initPromise = (async () => {
24574
24972
  try {
24575
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
+ }
24576
24979
  const createPipeline = transformersModule.pipeline;
24577
- this.pipeline = await createPipeline("feature-extraction", this.model, {
24578
- quantized: true
24579
- });
24980
+ this.pipeline = await withQuietConsole(() => createPipeline("feature-extraction", this.model, {
24981
+ quantized: true,
24982
+ dtype: "fp32"
24983
+ }));
24580
24984
  log(`[magic-context] embedding model loaded: ${this.model}`);
24581
24985
  } catch (error48) {
24582
24986
  log("[magic-context] embedding model failed to load:", error48);
@@ -24593,13 +24997,14 @@ class LocalEmbeddingProvider {
24593
24997
  return null;
24594
24998
  }
24595
24999
  try {
24596
- if (!this.pipeline) {
25000
+ const pipeline = this.pipeline;
25001
+ if (!pipeline) {
24597
25002
  return null;
24598
25003
  }
24599
- const result = await this.pipeline(text, {
25004
+ const result = await withQuietConsole(() => pipeline(text, {
24600
25005
  pooling: "mean",
24601
25006
  normalize: true
24602
- });
25007
+ }));
24603
25008
  return extractBatchEmbeddings(result, 1)[0] ?? null;
24604
25009
  } catch (error48) {
24605
25010
  log("[magic-context] embedding failed:", error48);
@@ -24614,13 +25019,14 @@ class LocalEmbeddingProvider {
24614
25019
  return Array.from({ length: texts.length }, () => null);
24615
25020
  }
24616
25021
  try {
24617
- if (!this.pipeline) {
25022
+ const pipeline = this.pipeline;
25023
+ if (!pipeline) {
24618
25024
  return Array.from({ length: texts.length }, () => null);
24619
25025
  }
24620
- const result = await this.pipeline(texts, {
25026
+ const result = await withQuietConsole(() => pipeline(texts, {
24621
25027
  pooling: "mean",
24622
25028
  normalize: true
24623
- });
25029
+ }));
24624
25030
  return extractBatchEmbeddings(result, texts.length);
24625
25031
  } catch (error48) {
24626
25032
  log("[magic-context] embedding batch failed:", error48);
@@ -25220,7 +25626,11 @@ function readRawSessionMessagesFromDb(db, sessionId) {
25220
25626
  list.push(parseJsonUnknown(part.data));
25221
25627
  partsByMessageId.set(part.message_id, list);
25222
25628
  }
25223
- return messageRows.flatMap((row, index) => {
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) => {
25224
25634
  const info = parseJsonRecord(row.data);
25225
25635
  if (!info)
25226
25636
  return [];
@@ -25594,6 +26004,158 @@ import { Database as Database2 } from "bun:sqlite";
25594
26004
  import { mkdirSync } from "fs";
25595
26005
  import { join as join9 } from "path";
25596
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
25597
26159
  var databases = new Map;
25598
26160
  var FALLBACK_DATABASE_KEY = "__fallback__:memory:";
25599
26161
  var persistenceByDatabase = new WeakMap;
@@ -25873,6 +26435,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
25873
26435
  ensureColumn(db, "dream_queue", "retry_count", "INTEGER DEFAULT 0");
25874
26436
  ensureColumn(db, "tags", "reasoning_byte_size", "INTEGER DEFAULT 0");
25875
26437
  ensureColumn(db, "session_meta", "system_prompt_tokens", "INTEGER DEFAULT 0");
26438
+ ensureColumn(db, "session_meta", "compaction_marker_state", "TEXT DEFAULT ''");
25876
26439
  }
25877
26440
  function ensureColumn(db, table, column, definition) {
25878
26441
  if (!/^[a-z_]+$/.test(table) || !/^[a-z_]+$/.test(column) || !/^[A-Z0-9_'(),\s]+$/i.test(definition)) {
@@ -25888,6 +26451,7 @@ function createFallbackDatabase() {
25888
26451
  try {
25889
26452
  const fallback = new Database2(":memory:");
25890
26453
  initializeDatabase(fallback);
26454
+ runMigrations(fallback);
25891
26455
  return fallback;
25892
26456
  } catch (error48) {
25893
26457
  throw new Error(`[magic-context] storage fatal: failed to initialize fallback database: ${getErrorMessage(error48)}`);
@@ -25906,6 +26470,7 @@ function openDatabase() {
25906
26470
  mkdirSync(dbDir, { recursive: true });
25907
26471
  const db = new Database2(dbPath);
25908
26472
  initializeDatabase(db);
26473
+ runMigrations(db);
25909
26474
  databases.set(dbPath, db);
25910
26475
  persistenceByDatabase.set(db, true);
25911
26476
  persistenceErrorByDatabase.delete(db);
@@ -26138,6 +26703,24 @@ function setPersistedDeliveredNoteNudge(db, sessionId, text, messageId = "") {
26138
26703
  function clearPersistedNoteNudge(db, sessionId) {
26139
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);
26140
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
+ }
26141
26724
  function getStrippedPlaceholderIds(db, sessionId) {
26142
26725
  const row = db.prepare("SELECT stripped_placeholder_ids FROM session_meta WHERE session_id = ?").get(sessionId);
26143
26726
  const raw = row?.stripped_placeholder_ids;
@@ -26208,37 +26791,12 @@ function clearSession(db, sessionId) {
26208
26791
  db.prepare("DELETE FROM compartments WHERE session_id = ?").run(sessionId);
26209
26792
  clearCompressionDepth(db, sessionId);
26210
26793
  db.prepare("DELETE FROM session_facts WHERE session_id = ?").run(sessionId);
26211
- db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
26794
+ db.prepare("DELETE FROM notes WHERE session_id = ? AND type = 'session'").run(sessionId);
26212
26795
  db.prepare("DELETE FROM recomp_compartments WHERE session_id = ?").run(sessionId);
26213
26796
  db.prepare("DELETE FROM recomp_facts WHERE session_id = ?").run(sessionId);
26214
26797
  clearIndexedMessages(db, sessionId);
26215
26798
  })();
26216
26799
  }
26217
- // src/features/magic-context/storage-notes.ts
26218
- function isSessionNoteRow(row) {
26219
- if (row === null || typeof row !== "object")
26220
- return false;
26221
- const candidate = row;
26222
- return typeof candidate.id === "number" && typeof candidate.session_id === "string" && typeof candidate.content === "string" && typeof candidate.created_at === "number";
26223
- }
26224
- function toSessionNote(row) {
26225
- return {
26226
- id: row.id,
26227
- sessionId: row.session_id,
26228
- content: row.content,
26229
- createdAt: row.created_at
26230
- };
26231
- }
26232
- function getSessionNotes(db, sessionId) {
26233
- const rows = db.prepare("SELECT * FROM session_notes WHERE session_id = ? ORDER BY id ASC").all(sessionId).filter(isSessionNoteRow);
26234
- return rows.map(toSessionNote);
26235
- }
26236
- function addSessionNote(db, sessionId, content) {
26237
- db.prepare("INSERT INTO session_notes (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, Date.now());
26238
- }
26239
- function clearSessionNotes(db, sessionId) {
26240
- db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
26241
- }
26242
26800
  // src/features/magic-context/storage-ops.ts
26243
26801
  init_logger();
26244
26802
  var queuePendingOpStatements = new WeakMap;
@@ -26455,7 +27013,14 @@ function getTopNBySize(db, sessionId, n) {
26455
27013
  init_logger();
26456
27014
  var DREAM_TIMER_INTERVAL_MS = 15 * 60 * 1000;
26457
27015
  function startDreamScheduleTimer(args) {
26458
- const { client, directory, dreamerConfig, embeddingConfig: embeddingConfig2, memoryEnabled } = args;
27016
+ const {
27017
+ client,
27018
+ directory,
27019
+ dreamerConfig,
27020
+ embeddingConfig: embeddingConfig2,
27021
+ memoryEnabled,
27022
+ experimentalUserMemories
27023
+ } = args;
26459
27024
  const dreamingEnabled = Boolean(dreamerConfig?.enabled && dreamerConfig.schedule?.trim());
26460
27025
  const embeddingSweepEnabled = memoryEnabled && embeddingConfig2.provider !== "off";
26461
27026
  if (!dreamingEnabled && !embeddingSweepEnabled) {
@@ -26483,7 +27048,8 @@ function startDreamScheduleTimer(args) {
26483
27048
  client,
26484
27049
  tasks: dreamerConfig.tasks,
26485
27050
  taskTimeoutMinutes: dreamerConfig.task_timeout_minutes,
26486
- maxRuntimeMinutes: dreamerConfig.max_runtime_minutes
27051
+ maxRuntimeMinutes: dreamerConfig.max_runtime_minutes,
27052
+ experimentalUserMemories
26487
27053
  }).catch((error48) => {
26488
27054
  log("[dreamer] timer-triggered queue processing failed:", error48);
26489
27055
  });
@@ -27204,6 +27770,8 @@ async function sendUserPrompt(client, sessionId, text) {
27204
27770
  }
27205
27771
 
27206
27772
  // src/hooks/magic-context/command-handler.ts
27773
+ var recompConfirmationBySession = new Map;
27774
+ var RECOMP_CONFIRMATION_WINDOW_MS = 60000;
27207
27775
  var SENTINEL_PREFIX = "__CONTEXT_MANAGEMENT_";
27208
27776
  async function executeAugmentation(deps, sessionId, userPrompt) {
27209
27777
  if (!deps.sidekick?.config) {
@@ -27279,7 +27847,8 @@ Dreaming is not configured for this project.`, {});
27279
27847
  client: deps.dreamer.client,
27280
27848
  tasks: deps.dreamer.config.tasks,
27281
27849
  taskTimeoutMinutes: deps.dreamer.config.task_timeout_minutes,
27282
- maxRuntimeMinutes: deps.dreamer.config.max_runtime_minutes
27850
+ maxRuntimeMinutes: deps.dreamer.config.max_runtime_minutes,
27851
+ experimentalUserMemories: deps.dreamer.experimentalUserMemories
27283
27852
  });
27284
27853
  await deps.sendNotification(sessionId, result ? summarizeDreamResult(result) : "Dream queued, but another worker is already processing the queue.", {});
27285
27854
  throw new Error(`${SENTINEL_PREFIX}CTX-DREAM_HANDLED__`);
@@ -27322,12 +27891,40 @@ function createMagicContextCommandHandler(deps) {
27322
27891
  ${statusOutput}` : statusOutput;
27323
27892
  }
27324
27893
  if (isRecomp) {
27325
- await deps.sendNotification(sessionId, `## Magic Recomp
27326
-
27327
- Historian recomp started. Rebuilding compartments and facts from raw session history now.`, {});
27328
- result = deps.executeRecomp ? await deps.executeRecomp(sessionId) : `## Magic Recomp
27894
+ if (!deps.executeRecomp) {
27895
+ result = `## Magic Recomp
27329
27896
 
27330
27897
  /ctx-recomp is unavailable because the recomp handler is not configured.`;
27898
+ } else {
27899
+ const lastConfirmation = recompConfirmationBySession.get(sessionId);
27900
+ const now = Date.now();
27901
+ if (lastConfirmation && now - lastConfirmation < RECOMP_CONFIRMATION_WINDOW_MS) {
27902
+ recompConfirmationBySession.delete(sessionId);
27903
+ await deps.sendNotification(sessionId, `## Magic Recomp
27904
+
27905
+ Historian recomp started. Rebuilding compartments and facts from raw session history now.`, {});
27906
+ result = await deps.executeRecomp(sessionId);
27907
+ } else {
27908
+ recompConfirmationBySession.set(sessionId, now);
27909
+ const compartments = getCompartments(deps.db, sessionId);
27910
+ const compartmentCount = compartments.length;
27911
+ const warningLines = [
27912
+ "## \u26A0\uFE0F Recomp Confirmation Required",
27913
+ "",
27914
+ `You currently have **${compartmentCount}** compartments.`,
27915
+ "Running /ctx-recomp will **regenerate all compartments and facts** from raw session history.",
27916
+ "",
27917
+ "This operation:",
27918
+ "- May take a long time (minutes to hours for long sessions)",
27919
+ "- Will consume significant tokens on your historian model",
27920
+ "- Cannot be interrupted cleanly once started",
27921
+ "",
27922
+ "**To confirm, run `/ctx-recomp` again within 60 seconds.**"
27923
+ ];
27924
+ result = warningLines.join(`
27925
+ `);
27926
+ }
27927
+ }
27331
27928
  }
27332
27929
  await deps.sendNotification(sessionId, result, {});
27333
27930
  sessionLog(sessionId, `command ${input.command} handled via command.execute.before`);
@@ -27339,6 +27936,175 @@ Historian recomp started. Rebuilding compartments and facts from raw session his
27339
27936
  // src/hooks/magic-context/event-handler.ts
27340
27937
  init_logger();
27341
27938
 
27939
+ // src/features/magic-context/compaction-marker.ts
27940
+ import { Database as Database3 } from "bun:sqlite";
27941
+ import { join as join10 } from "path";
27942
+ init_logger();
27943
+ var BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
27944
+ function randomBase62(length) {
27945
+ const chars = [];
27946
+ for (let i = 0;i < length; i++) {
27947
+ chars.push(BASE62_CHARS[Math.floor(Math.random() * BASE62_CHARS.length)]);
27948
+ }
27949
+ return chars.join("");
27950
+ }
27951
+ function generateId(prefix, timestampMs, counter = 0n) {
27952
+ const encoded = BigInt(timestampMs) * 0x1000n + counter;
27953
+ const hex3 = encoded.toString(16).padStart(14, "0");
27954
+ return `${prefix}_${hex3}${randomBase62(14)}`;
27955
+ }
27956
+ function generateMessageId(timestampMs, counter = 0n) {
27957
+ return generateId("msg", timestampMs, counter);
27958
+ }
27959
+ function generatePartId(timestampMs, counter = 0n) {
27960
+ return generateId("prt", timestampMs, counter);
27961
+ }
27962
+ function getOpenCodeDbPath2() {
27963
+ return join10(getDataDir(), "opencode", "opencode.db");
27964
+ }
27965
+ var cachedWriteDb = null;
27966
+ function getWritableOpenCodeDb() {
27967
+ const dbPath = getOpenCodeDbPath2();
27968
+ if (cachedWriteDb?.path === dbPath) {
27969
+ return cachedWriteDb.db;
27970
+ }
27971
+ if (cachedWriteDb) {
27972
+ try {
27973
+ cachedWriteDb.db.close(false);
27974
+ } catch {}
27975
+ }
27976
+ const db = new Database3(dbPath);
27977
+ db.exec("PRAGMA journal_mode=WAL");
27978
+ db.exec("PRAGMA busy_timeout=5000");
27979
+ cachedWriteDb = { path: dbPath, db };
27980
+ return db;
27981
+ }
27982
+ function findBoundaryUserMessage(sessionId, endOrdinal) {
27983
+ const db = getWritableOpenCodeDb();
27984
+ const rows = db.prepare("SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId);
27985
+ const filtered = rows.filter((row) => {
27986
+ try {
27987
+ const info = JSON.parse(row.data);
27988
+ return !(info.summary === true && info.finish === "stop");
27989
+ } catch {
27990
+ return true;
27991
+ }
27992
+ });
27993
+ let bestMatch = null;
27994
+ for (let i = 0;i < filtered.length && i < endOrdinal; i++) {
27995
+ const row = filtered[i];
27996
+ try {
27997
+ const info = JSON.parse(row.data);
27998
+ if (info.role === "user") {
27999
+ bestMatch = { id: row.id, timeCreated: row.time_created };
28000
+ }
28001
+ } catch {}
28002
+ }
28003
+ return bestMatch;
28004
+ }
28005
+ function injectCompactionMarker(args) {
28006
+ const boundary = findBoundaryUserMessage(args.sessionId, args.endOrdinal);
28007
+ if (!boundary) {
28008
+ log(`[magic-context] compaction-marker: no user message found at or before ordinal ${args.endOrdinal}`);
28009
+ return null;
28010
+ }
28011
+ const db = getWritableOpenCodeDb();
28012
+ const boundaryTime = boundary.timeCreated;
28013
+ const summaryMsgId = generateMessageId(boundaryTime + 1, 1n);
28014
+ const compactionPartId = generatePartId(boundaryTime, 1n);
28015
+ const summaryPartId = generatePartId(boundaryTime + 1, 2n);
28016
+ const summaryMsgData = JSON.stringify({
28017
+ role: "assistant",
28018
+ parentID: boundary.id,
28019
+ summary: true,
28020
+ finish: "stop",
28021
+ mode: "compaction",
28022
+ agent: "compaction",
28023
+ path: { cwd: args.directory, root: args.directory },
28024
+ cost: 0,
28025
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
28026
+ modelID: "magic-context",
28027
+ providerID: "magic-context",
28028
+ time: { created: boundaryTime + 1 }
28029
+ });
28030
+ try {
28031
+ db.transaction(() => {
28032
+ db.prepare("INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)").run(compactionPartId, boundary.id, args.sessionId, boundaryTime, boundaryTime, '{"type":"compaction","auto":true}');
28033
+ db.prepare("INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)").run(summaryMsgId, args.sessionId, boundaryTime + 1, boundaryTime + 1, summaryMsgData);
28034
+ db.prepare("INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)").run(summaryPartId, summaryMsgId, args.sessionId, boundaryTime + 1, boundaryTime + 1, JSON.stringify({ type: "text", text: args.summaryText }));
28035
+ })();
28036
+ log(`[magic-context] compaction-marker: injected boundary at user msg ${boundary.id} (ordinal ~${args.endOrdinal}), summary msg ${summaryMsgId}`);
28037
+ return {
28038
+ boundaryMessageId: boundary.id,
28039
+ summaryMessageId: summaryMsgId,
28040
+ compactionPartId,
28041
+ summaryPartId
28042
+ };
28043
+ } catch (error48) {
28044
+ log(`[magic-context] compaction-marker: injection failed: ${error48 instanceof Error ? error48.message : String(error48)}`);
28045
+ return null;
28046
+ }
28047
+ }
28048
+ function removeCompactionMarker(state) {
28049
+ try {
28050
+ const db = getWritableOpenCodeDb();
28051
+ db.transaction(() => {
28052
+ db.prepare("DELETE FROM part WHERE id = ?").run(state.summaryPartId);
28053
+ db.prepare("DELETE FROM message WHERE id = ?").run(state.summaryMessageId);
28054
+ db.prepare("DELETE FROM part WHERE id = ?").run(state.compactionPartId);
28055
+ })();
28056
+ return true;
28057
+ } catch (error48) {
28058
+ log(`[magic-context] compaction-marker: removal failed: ${error48 instanceof Error ? error48.message : String(error48)}`);
28059
+ return false;
28060
+ }
28061
+ }
28062
+
28063
+ // src/hooks/magic-context/compaction-marker-manager.ts
28064
+ init_logger();
28065
+ var MARKER_SUMMARY_TEXT = "[Compacted by magic-context \u2014 session history is managed by the plugin]";
28066
+ function updateCompactionMarkerAfterPublication(db, sessionId, lastCompartmentEnd, directory) {
28067
+ const existing = getPersistedCompactionMarkerState(db, sessionId);
28068
+ if (existing) {
28069
+ if (existing.boundaryOrdinal === lastCompartmentEnd) {
28070
+ return;
28071
+ }
28072
+ try {
28073
+ removeCompactionMarker(existing);
28074
+ setPersistedCompactionMarkerState(db, sessionId, null);
28075
+ sessionLog(sessionId, `compaction-marker: removed old boundary at ordinal ${existing.boundaryOrdinal}, moving to ${lastCompartmentEnd}`);
28076
+ } catch (error48) {
28077
+ sessionLog(sessionId, `compaction-marker: failed to remove old boundary at ordinal ${existing.boundaryOrdinal}, proceeding with new injection:`, error48);
28078
+ }
28079
+ }
28080
+ const result = injectCompactionMarker({
28081
+ sessionId,
28082
+ endOrdinal: lastCompartmentEnd,
28083
+ summaryText: MARKER_SUMMARY_TEXT,
28084
+ directory: directory ?? process.cwd()
28085
+ });
28086
+ if (result) {
28087
+ setPersistedCompactionMarkerState(db, sessionId, {
28088
+ ...result,
28089
+ boundaryOrdinal: lastCompartmentEnd
28090
+ });
28091
+ sessionLog(sessionId, `compaction-marker: injected at ordinal ${lastCompartmentEnd}, boundary user msg ${result.boundaryMessageId}`);
28092
+ }
28093
+ }
28094
+ function removeCompactionMarkerForSession(db, sessionId) {
28095
+ const existing = getPersistedCompactionMarkerState(db, sessionId);
28096
+ if (existing) {
28097
+ try {
28098
+ removeCompactionMarker(existing);
28099
+ setPersistedCompactionMarkerState(db, sessionId, null);
28100
+ sessionLog(sessionId, "compaction-marker: removed on session cleanup");
28101
+ } catch (error48) {
28102
+ setPersistedCompactionMarkerState(db, sessionId, null);
28103
+ sessionLog(sessionId, "compaction-marker: removal failed during session cleanup, cleared persisted state:", error48);
28104
+ }
28105
+ }
28106
+ }
28107
+
27342
28108
  // src/hooks/magic-context/event-payloads.ts
27343
28109
  function getSessionProperties(properties) {
27344
28110
  if (!isRecord(properties)) {
@@ -27635,6 +28401,11 @@ function createEventHandler2(deps) {
27635
28401
  clearNoteNudgeState(deps.db, info.sessionID, { persist: false });
27636
28402
  sessionLog(info.sessionID, "event message.removed: cleared in-memory note nudge state");
27637
28403
  }
28404
+ const markerState = getPersistedCompactionMarkerState(deps.db, info.sessionID);
28405
+ if (markerState && (markerState.boundaryMessageId === info.messageID || markerState.summaryMessageId === info.messageID)) {
28406
+ removeCompactionMarkerForSession(deps.db, info.sessionID);
28407
+ sessionLog(info.sessionID, `event message.removed: cleared compaction marker (boundary or summary message removed)`);
28408
+ }
27638
28409
  deps.onSessionCacheInvalidated?.(info.sessionID);
27639
28410
  sessionLog(info.sessionID, "event message.removed: cleared session injection cache");
27640
28411
  } catch (error48) {
@@ -27652,6 +28423,11 @@ function createEventHandler2(deps) {
27652
28423
  } catch (error48) {
27653
28424
  sessionLog(sessionId, "event session.compacted handling failed:", error48);
27654
28425
  }
28426
+ try {
28427
+ removeCompactionMarkerForSession(deps.db, sessionId);
28428
+ } catch (error48) {
28429
+ sessionLog(sessionId, "event session.compacted marker cleanup failed:", error48);
28430
+ }
27655
28431
  deps.onSessionCacheInvalidated?.(sessionId);
27656
28432
  return;
27657
28433
  }
@@ -27662,6 +28438,7 @@ function createEventHandler2(deps) {
27662
28438
  }
27663
28439
  deps.nudgePlacements.clear(sessionId);
27664
28440
  try {
28441
+ removeCompactionMarkerForSession(deps.db, sessionId);
27665
28442
  clearSession(deps.db, sessionId);
27666
28443
  } catch (error48) {
27667
28444
  sessionLog(sessionId, "event session.deleted persistence failed:", error48);
@@ -27741,7 +28518,10 @@ function trimMemoriesToBudget(sessionId, memories, budgetTokens) {
27741
28518
  return -1;
27742
28519
  if (b.status === "permanent" && a.status !== "permanent")
27743
28520
  return 1;
27744
- return b.seenCount - a.seenCount;
28521
+ const seenDiff = b.seenCount - a.seenCount;
28522
+ if (seenDiff !== 0)
28523
+ return seenDiff;
28524
+ return a.id - b.id;
27745
28525
  });
27746
28526
  const result = [];
27747
28527
  let usedTokens = 0;
@@ -28479,6 +29259,9 @@ function searchMemoriesFTS(db, projectPath, query, limit = DEFAULT_SEARCH_LIMIT)
28479
29259
  const rows = getSearchStatement(db).all(projectPath, Date.now(), sanitized, limit).filter(isMemoryRow);
28480
29260
  return rows.map(toMemory);
28481
29261
  }
29262
+ // src/hooks/magic-context/compartment-runner-incremental.ts
29263
+ init_logger();
29264
+
28482
29265
  // src/hooks/magic-context/compartment-runner-compressor.ts
28483
29266
  init_logger();
28484
29267
 
@@ -28487,6 +29270,8 @@ var COMPARTMENT_REGEX = /<compartment\s+(?:id="[^"]*"\s+)?start="(\d+)"\s+end="(
28487
29270
  var CATEGORY_BLOCK_REGEX = /<(WORKFLOW_RULES|ARCHITECTURE_DECISIONS|CONSTRAINTS|CONFIG_DEFAULTS|KNOWN_ISSUES|ENVIRONMENT|NAMING|USER_PREFERENCES|USER_DIRECTIVES)>(.*?)<\/\1>/gs;
28488
29271
  var FACT_ITEM_REGEX = /^\s*\*\s*(.+)$/gm;
28489
29272
  var UNPROCESSED_REGEX = /<unprocessed_from>(\d+)<\/unprocessed_from>/;
29273
+ var USER_OBSERVATIONS_REGEX = /<user_observations>(.*?)<\/user_observations>/s;
29274
+ var USER_OBS_ITEM_REGEX = /^\s*\*\s*(.+)$/gm;
28490
29275
  function parseCompartmentOutput(text) {
28491
29276
  const compartments = [];
28492
29277
  const facts = [];
@@ -28511,8 +29296,17 @@ function parseCompartmentOutput(text) {
28511
29296
  }
28512
29297
  const unprocessedMatch = text.match(UNPROCESSED_REGEX);
28513
29298
  const unprocessedFrom = unprocessedMatch ? parseInt(unprocessedMatch[1], 10) : null;
29299
+ const userObservations = [];
29300
+ const userObsMatch = text.match(USER_OBSERVATIONS_REGEX);
29301
+ if (userObsMatch) {
29302
+ for (const itemMatch of userObsMatch[1].matchAll(USER_OBS_ITEM_REGEX)) {
29303
+ const obs = unescapeXml(itemMatch[1].trim());
29304
+ if (obs)
29305
+ userObservations.push(obs);
29306
+ }
29307
+ }
28514
29308
  compartments.sort((a, b) => a.startMessage - b.startMessage);
28515
- return { compartments, facts, unprocessedFrom };
29309
+ return { compartments, facts, unprocessedFrom, userObservations };
28516
29310
  }
28517
29311
  function unescapeXml(s) {
28518
29312
  return s.replace(/&amp;/g, "&").replace(/&apos;/g, "'").replace(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">");
@@ -28671,6 +29465,7 @@ ${compartment.content}
28671
29465
  }
28672
29466
  }
28673
29467
  replaceAllCompartmentState(db, sessionId, allCompartments, facts.map((f) => ({ category: f.category, content: f.content })));
29468
+ clearInjectionCache(sessionId);
28674
29469
  incrementCompressionDepth(db, sessionId, originalStart, originalEnd);
28675
29470
  sessionLog(sessionId, `compressor: replaced ${selectedCompartments.length} compartments with ${compressed.length} compressed compartments`);
28676
29471
  sessionLog(sessionId, `compressor: incremented compression depth for messages ${originalStart}-${originalEnd}`);
@@ -28790,7 +29585,7 @@ function queueDropsForCompartmentalizedMessages(db, sessionId, upToMessageIndex)
28790
29585
  // src/hooks/magic-context/compartment-runner-historian.ts
28791
29586
  import { mkdirSync as mkdirSync2, unlinkSync, writeFileSync } from "fs";
28792
29587
  import { tmpdir as tmpdir2 } from "os";
28793
- import { join as join10 } from "path";
29588
+ import { join as join11 } from "path";
28794
29589
 
28795
29590
  // src/hooks/magic-context/compartment-runner-mapping.ts
28796
29591
  function mapParsedCompartmentsToChunk(compartments, chunk, sequenceOffset) {
@@ -28857,7 +29652,8 @@ function validateHistorianOutput(text, _sessionId, chunk, _priorCompartments, se
28857
29652
  return {
28858
29653
  ok: true,
28859
29654
  compartments: mapped.compartments,
28860
- facts: parsed.facts
29655
+ facts: parsed.facts,
29656
+ userObservations: parsed.userObservations.length > 0 ? parsed.userObservations : undefined
28861
29657
  };
28862
29658
  }
28863
29659
  function buildHistorianRepairPrompt(originalPrompt, previousOutput, validationError) {
@@ -28950,7 +29746,7 @@ function getReducedRecompTokenBudget(currentBudget) {
28950
29746
  }
28951
29747
 
28952
29748
  // src/hooks/magic-context/compartment-runner-historian.ts
28953
- var HISTORIAN_RESPONSE_DUMP_DIR = join10(tmpdir2(), "magic-context-historian");
29749
+ var HISTORIAN_RESPONSE_DUMP_DIR = join11(tmpdir2(), "magic-context-historian");
28954
29750
  async function runValidatedHistorianPass(args) {
28955
29751
  const firstRun = await runHistorianPrompt({
28956
29752
  ...args,
@@ -29052,7 +29848,7 @@ function dumpHistorianResponse(sessionId, label, text) {
29052
29848
  mkdirSync2(HISTORIAN_RESPONSE_DUMP_DIR, { recursive: true });
29053
29849
  const safeSessionId = sanitizeDumpName(sessionId);
29054
29850
  const safeLabel = sanitizeDumpName(label);
29055
- const dumpPath = join10(HISTORIAN_RESPONSE_DUMP_DIR, `${safeSessionId}-${safeLabel}-${Date.now()}.xml`);
29851
+ const dumpPath = join11(HISTORIAN_RESPONSE_DUMP_DIR, `${safeSessionId}-${safeLabel}-${Date.now()}.xml`);
29056
29852
  writeFileSync(dumpPath, text, "utf8");
29057
29853
  sessionLog(sessionId, "compartment agent: historian response dumped", {
29058
29854
  label,
@@ -29201,11 +29997,15 @@ No new compartments or facts were written. Check the historian model/output and
29201
29997
  appendCompartments(db, sessionId, newCompartments);
29202
29998
  replaceSessionFacts(db, sessionId, validatedPass.facts ?? []);
29203
29999
  })();
30000
+ clearInjectionCache(sessionId);
29204
30001
  if (deps.directory) {
29205
30002
  promoteSessionFactsToMemory(db, sessionId, resolveProjectIdentity(deps.directory), validatedPass.facts ?? []);
29206
30003
  }
29207
30004
  const lastCompartmentEnd = lastNewEnd;
29208
30005
  queueDropsForCompartmentalizedMessages(db, sessionId, lastCompartmentEnd);
30006
+ if (deps.experimentalCompactionMarkers) {
30007
+ updateCompactionMarkerAfterPublication(db, sessionId, lastCompartmentEnd, sessionDirectory);
30008
+ }
29209
30009
  if (deps.historyBudgetTokens && deps.historyBudgetTokens > 0) {
29210
30010
  await runCompressionPassIfNeeded({
29211
30011
  client,
@@ -29219,6 +30019,20 @@ No new compartments or facts were written. Check the historian model/output and
29219
30019
  updateSessionMeta(db, sessionId, { compartmentInProgress: false });
29220
30020
  completedSuccessfully = true;
29221
30021
  onNoteTrigger(db, sessionId, "historian_complete");
30022
+ if (validatedPass.userObservations && validatedPass.userObservations.length > 0) {
30023
+ try {
30024
+ const lastNew = newCompartments[newCompartments.length - 1];
30025
+ insertUserMemoryCandidates(db, validatedPass.userObservations.map((obs) => ({
30026
+ content: obs,
30027
+ sessionId,
30028
+ sourceCompartmentStart: newCompartments[0]?.startMessage,
30029
+ sourceCompartmentEnd: lastNew?.endMessage
30030
+ })));
30031
+ sessionLog(sessionId, `stored ${validatedPass.userObservations.length} user memory candidate(s)`);
30032
+ } catch (error48) {
30033
+ sessionLog(sessionId, "failed to store user memory candidates:", error48);
30034
+ }
30035
+ }
29222
30036
  } catch (error48) {
29223
30037
  const msg = getErrorMessage(error48);
29224
30038
  if (!issueNotified) {
@@ -29259,6 +30073,7 @@ async function executeContextRecompInternal(deps) {
29259
30073
  const promoted2 = promoteRecompStaging(db, sessionId);
29260
30074
  if (!promoted2)
29261
30075
  return null;
30076
+ clearInjectionCache(sessionId);
29262
30077
  if (deps.directory) {
29263
30078
  promoteSessionFactsToMemory(db, sessionId, resolveProjectIdentity(deps.directory), promoted2.facts);
29264
30079
  }
@@ -29266,6 +30081,9 @@ async function executeContextRecompInternal(deps) {
29266
30081
  if (lastCompartmentEnd2 > 0) {
29267
30082
  queueDropsForCompartmentalizedMessages(db, sessionId, lastCompartmentEnd2);
29268
30083
  }
30084
+ if (deps.experimentalCompactionMarkers && lastCompartmentEnd2 > 0) {
30085
+ updateCompactionMarkerAfterPublication(db, sessionId, lastCompartmentEnd2, deps.directory);
30086
+ }
29269
30087
  return [
29270
30088
  `Persisted ${promoted2.compartments.length} compartment${promoted2.compartments.length === 1 ? "" : "s"} from ${passCount} successful pass${passCount === 1 ? "" : "es"}.`,
29271
30089
  `Covered raw history 1-${lastCompartmentEnd2} out of ${rawMessageCount} total messages.`,
@@ -29420,6 +30238,7 @@ Nothing was written.`;
29420
30238
  replaceAllCompartmentState(db, sessionId, candidateCompartments, candidateFacts);
29421
30239
  clearRecompStaging(db, sessionId);
29422
30240
  }
30241
+ clearInjectionCache(sessionId);
29423
30242
  const finalCompartments = promoted?.compartments ?? candidateCompartments;
29424
30243
  const finalFacts = promoted?.facts ?? candidateFacts;
29425
30244
  if (deps.directory) {
@@ -29549,7 +30368,9 @@ async function runCompartmentPhase(args) {
29549
30368
  historyBudgetTokens: args.historyBudgetTokens,
29550
30369
  historianTimeoutMs: args.historianTimeoutMs,
29551
30370
  directory: args.compartmentDirectory,
29552
- getNotificationParams: args.getNotificationParams
30371
+ getNotificationParams: args.getNotificationParams,
30372
+ experimentalCompactionMarkers: args.experimentalCompactionMarkers,
30373
+ experimentalUserMemories: args.experimentalUserMemories
29553
30374
  });
29554
30375
  compartmentInProgress = true;
29555
30376
  }
@@ -29575,7 +30396,9 @@ async function runCompartmentPhase(args) {
29575
30396
  historyBudgetTokens: args.historyBudgetTokens,
29576
30397
  historianTimeoutMs: args.historianTimeoutMs,
29577
30398
  directory: args.compartmentDirectory,
29578
- getNotificationParams: args.getNotificationParams
30399
+ getNotificationParams: args.getNotificationParams,
30400
+ experimentalCompactionMarkers: args.experimentalCompactionMarkers,
30401
+ experimentalUserMemories: args.experimentalUserMemories
29579
30402
  });
29580
30403
  activeRun = getActiveCompartmentRun(args.sessionId);
29581
30404
  } else if (!activeRun && hasEligibleHistoryForCompartment()) {
@@ -29589,7 +30412,7 @@ async function runCompartmentPhase(args) {
29589
30412
  }
29590
30413
  if (args.cacheAlreadyBusting && args.historyBudgetTokens && args.historyBudgetTokens > 0 && args.client && !compartmentInProgress && !awaitedCompartmentRun) {
29591
30414
  try {
29592
- await runCompressionPassIfNeeded({
30415
+ const compressed = await runCompressionPassIfNeeded({
29593
30416
  client: args.client,
29594
30417
  db: args.db,
29595
30418
  sessionId: args.sessionId,
@@ -29597,6 +30420,9 @@ async function runCompartmentPhase(args) {
29597
30420
  historyBudgetTokens: args.historyBudgetTokens,
29598
30421
  historianTimeoutMs: args.historianTimeoutMs
29599
30422
  });
30423
+ if (compressed && args.projectPath !== undefined) {
30424
+ pendingCompartmentInjection = prepareCompartmentInjection(args.db, args.sessionId, args.messages, true, args.projectPath, args.injectionBudgetTokens);
30425
+ }
29600
30426
  } catch (error48) {
29601
30427
  sessionLog(args.sessionId, "transform: independent compressor check failed:", getErrorMessage(error48));
29602
30428
  }
@@ -30744,7 +31570,18 @@ function runPostTransformPhase(args) {
30744
31570
  if (pendingUserTurnReminder.messageId) {
30745
31571
  const reinjected = appendReminderToUserMessageById(args.messages, pendingUserTurnReminder.messageId, pendingUserTurnReminder.text);
30746
31572
  if (!reinjected) {
30747
- sessionLog(args.sessionId, `preserving sticky turn reminder anchor to avoid cache bust: messageId=${pendingUserTurnReminder.messageId}`);
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
+ }
30748
31585
  }
30749
31586
  } else {
30750
31587
  const anchoredMessageId = appendReminderToLatestUserMessage(args.messages, pendingUserTurnReminder.text);
@@ -30781,7 +31618,17 @@ function runPostTransformPhase(args) {
30781
31618
  if (stickyNoteNudge) {
30782
31619
  const reinjected = appendReminderToUserMessageById(args.messages, stickyNoteNudge.messageId, stickyNoteNudge.text);
30783
31620
  if (!reinjected) {
30784
- sessionLog(args.sessionId, `preserving sticky note nudge anchor to avoid cache bust: messageId=${stickyNoteNudge.messageId}`);
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
+ }
30785
31632
  }
30786
31633
  }
30787
31634
  const deferredNoteText = peekNoteNudgeText(args.db, args.sessionId, args.currentTurnId, args.projectPath);
@@ -30856,7 +31703,7 @@ function createTransform(deps) {
30856
31703
  const canRunCompartments = fullFeatureMode && deps.client !== undefined && compartmentDirectory.length > 0;
30857
31704
  const contextUsageEarly = loadContextUsage(deps.contextUsageMap, db, sessionId);
30858
31705
  const schedulerDecisionEarly = resolveSchedulerDecision(deps.scheduler, sessionMeta, contextUsageEarly, sessionId, deps.getModelKey?.(sessionId));
30859
- const isCacheBusting = deps.flushedSessions.has(sessionId) || schedulerDecisionEarly === "execute";
31706
+ const isCacheBusting = deps.flushedSessions.has(sessionId);
30860
31707
  let pendingCompartmentInjection = null;
30861
31708
  if (fullFeatureMode) {
30862
31709
  const projectPath = deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined;
@@ -30947,7 +31794,9 @@ function createTransform(deps) {
30947
31794
  projectPath: deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined,
30948
31795
  injectionBudgetTokens: deps.memoryConfig?.injectionBudgetTokens,
30949
31796
  getNotificationParams: rawGetNotifParams ? () => rawGetNotifParams(sessionId) : undefined,
30950
- cacheAlreadyBusting: isCacheBusting
31797
+ cacheAlreadyBusting: isCacheBusting || schedulerDecisionEarly === "execute",
31798
+ experimentalCompactionMarkers: deps.experimentalCompactionMarkers,
31799
+ experimentalUserMemories: deps.experimentalUserMemories
30951
31800
  });
30952
31801
  pendingCompartmentInjection = compartmentPhase.pendingCompartmentInjection;
30953
31802
  const awaitedCompartmentRun = compartmentPhase.awaitedCompartmentRun;
@@ -31137,7 +31986,7 @@ function createToolExecuteAfterHook(args) {
31137
31986
 
31138
31987
  // src/hooks/magic-context/system-prompt-hash.ts
31139
31988
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
31140
- import { join as join11 } from "path";
31989
+ import { join as join12 } from "path";
31141
31990
 
31142
31991
  // src/agents/magic-context-prompt.ts
31143
31992
  var BASE_INTRO = (protectedTags) => `Messages and tool outputs are tagged with \xA7N\xA7 identifiers (e.g., \xA71\xA7, \xA742\xA7).
@@ -31338,11 +32187,13 @@ Prefer many small targeted operations over one large blanket operation. Compress
31338
32187
  init_logger();
31339
32188
  var MAGIC_CONTEXT_MARKER = "## Magic Context";
31340
32189
  var PROJECT_DOCS_MARKER = "<project-docs>";
32190
+ var USER_PROFILE_MARKER = "<user-profile>";
32191
+ var cachedUserProfileBySession = new Map;
31341
32192
  var DOC_FILES = ["ARCHITECTURE.md", "STRUCTURE.md"];
31342
32193
  function readProjectDocs(directory) {
31343
32194
  const sections = [];
31344
32195
  for (const filename of DOC_FILES) {
31345
- const filePath = join11(directory, filename);
32196
+ const filePath = join12(directory, filename);
31346
32197
  try {
31347
32198
  if (existsSync5(filePath)) {
31348
32199
  const content = readFileSync4(filePath, "utf-8").trim();
@@ -31397,6 +32248,28 @@ function createSystemPromptHashHandler(deps) {
31397
32248
  output.system.push(docsBlock);
31398
32249
  }
31399
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
+ }
31400
32273
  const DATE_PATTERN = /Today's date: .+/;
31401
32274
  for (let i = 0;i < output.system.length; i++) {
31402
32275
  const match = output.system[i].match(DATE_PATTERN);
@@ -31530,7 +32403,9 @@ function createMagicContextHook(deps) {
31530
32403
  const model = liveModelBySession.get(sessionId);
31531
32404
  return resolveModelKey(model?.providerID, model?.modelID);
31532
32405
  },
31533
- projectPath
32406
+ projectPath,
32407
+ experimentalCompactionMarkers: deps.config.experimental?.compaction_markers,
32408
+ experimentalUserMemories: deps.config.experimental?.user_memories?.enabled
31534
32409
  });
31535
32410
  const eventHandler = createEventHandler2({
31536
32411
  contextUsageMap,
@@ -31565,7 +32440,11 @@ function createMagicContextHook(deps) {
31565
32440
  client: deps.client,
31566
32441
  tasks: dreaming.tasks,
31567
32442
  taskTimeoutMinutes: dreaming.task_timeout_minutes,
31568
- 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
31569
32448
  }).catch((error48) => {
31570
32449
  log("[dreamer] scheduled queue processing failed:", error48);
31571
32450
  });
@@ -31607,7 +32486,11 @@ function createMagicContextHook(deps) {
31607
32486
  config: deps.config.dreamer,
31608
32487
  projectPath,
31609
32488
  client: deps.client,
31610
- 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
31611
32494
  } : undefined
31612
32495
  });
31613
32496
  const emergencyNudgeFired = new Set;
@@ -31619,7 +32502,8 @@ function createMagicContextHook(deps) {
31619
32502
  injectDocs: deps.config.dreamer?.inject_docs !== false,
31620
32503
  directory: deps.directory,
31621
32504
  flushedSessions,
31622
- lastHeuristicsTurnId
32505
+ lastHeuristicsTurnId,
32506
+ experimentalUserMemories: deps.config.experimental?.user_memories?.enabled
31623
32507
  });
31624
32508
  const eventHook = createEventHook({
31625
32509
  eventHandler,
@@ -31696,7 +32580,8 @@ function createSessionHooks(args) {
31696
32580
  historian_timeout_ms: pluginConfig.historian_timeout_ms,
31697
32581
  memory: pluginConfig.memory,
31698
32582
  sidekick: pluginConfig.sidekick,
31699
- dreamer: pluginConfig.dreamer
32583
+ dreamer: pluginConfig.dreamer,
32584
+ experimental: pluginConfig.experimental
31700
32585
  }
31701
32586
  })
31702
32587
  };
@@ -32079,9 +32964,9 @@ Use this for short goals, constraints, decisions, or reminders worth carrying fo
32079
32964
 
32080
32965
  Actions:
32081
32966
  - \`write\`: Append one note. Optionally provide \`surface_condition\` to create a smart note.
32082
- - \`read\`: Show current notes (session notes + ready smart notes).
32083
- - \`clear\`: Remove all session notes.
32084
- - \`dismiss\`: Dismiss a ready smart note by \`note_id\`.
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\`.
32085
32970
 
32086
32971
  **Smart Notes**: When \`surface_condition\` is provided with \`write\`, the note becomes a project-scoped smart note.
32087
32972
  The dreamer evaluates smart note conditions during nightly runs and surfaces them when conditions are met.
@@ -32090,14 +32975,79 @@ Example: \`ctx_note(action="write", content="Implement X because Y", surface_con
32090
32975
  Historian reads these notes, deduplicates them, and rewrites the remaining useful notes over time.`;
32091
32976
  // src/tools/ctx-note/tools.ts
32092
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
+ }
32093
33042
  function createCtxNoteTool(deps) {
32094
33043
  return tool3({
32095
33044
  description: CTX_NOTE_DESCRIPTION,
32096
33045
  args: {
32097
- action: tool3.schema.enum(["write", "read", "clear", "dismiss"]).optional().describe("Operation to perform. Defaults to 'write' when content is provided, otherwise 'read'."),
33046
+ action: tool3.schema.enum(["write", "read", "dismiss", "update"]).optional().describe("Operation to perform. Defaults to 'write' when content is provided, otherwise 'read'."),
32098
33047
  content: tool3.schema.string().optional().describe("Note text to store when action is 'write'."),
32099
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."),
32100
- note_id: tool3.schema.number().optional().describe("Smart note ID to dismiss (required for 'dismiss' action).")
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).")
32101
33051
  },
32102
33052
  async execute(args, toolContext) {
32103
33053
  const sessionId = toolContext.sessionID;
@@ -32114,48 +33064,59 @@ function createCtxNoteTool(deps) {
32114
33064
  if (!deps.projectIdentity) {
32115
33065
  return "Error: Could not resolve project identity for smart note.";
32116
33066
  }
32117
- const note = addSmartNote(deps.db, deps.projectIdentity, content, args.surface_condition.trim(), sessionId);
32118
- return `Created smart note #${note.id}. Dreamer will evaluate the condition during nightly runs:
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:
32119
33074
  - Content: ${content}
32120
33075
  - Condition: ${args.surface_condition.trim()}`;
32121
33076
  }
32122
- addSessionNote(deps.db, sessionId, content);
32123
- const total = getSessionNotes(deps.db, sessionId).length;
32124
- 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.`;
32125
33079
  }
32126
33080
  if (action === "dismiss") {
32127
33081
  const noteId = args.note_id;
32128
33082
  if (typeof noteId !== "number") {
32129
33083
  return "Error: 'note_id' is required when action is 'dismiss'.";
32130
33084
  }
32131
- const dismissed = dismissSmartNote(deps.db, noteId);
32132
- return dismissed ? `Smart note #${noteId} dismissed.` : `Smart note #${noteId} not found or already dismissed.`;
32133
- }
32134
- if (action === "clear") {
32135
- const existing = getSessionNotes(deps.db, sessionId);
32136
- clearSessionNotes(deps.db, sessionId);
32137
- return existing.length === 0 ? "Session notes were already empty." : `Cleared ${existing.length} session note${existing.length === 1 ? "" : "s"}.`;
32138
- }
32139
- const notes = getSessionNotes(deps.db, sessionId);
32140
- const readySmartNotes = deps.projectIdentity ? getReadySmartNotes(deps.db, deps.projectIdentity) : [];
32141
- const sections = [];
32142
- if (notes.length > 0) {
32143
- const lines = notes.map((note, index) => `${index + 1}. ${note.content}`);
32144
- sections.push(`## Session Notes
32145
-
32146
- ${lines.join(`
32147
- `)}`);
32148
- }
32149
- if (readySmartNotes.length > 0) {
32150
- const lines = readySmartNotes.map((n) => `- **#${n.id}**: ${n.content}
32151
- Condition met: ${n.readyReason ?? n.surfaceCondition}
32152
- _(dismiss with \`ctx_note(action="dismiss", note_id=${n.id})\`)_`);
32153
- sections.push(`## \uD83D\uDD14 Ready Smart Notes
32154
-
32155
- ${lines.join(`
32156
-
32157
- `)}`);
33085
+ const dismissed = dismissNote(deps.db, noteId);
33086
+ return dismissed ? `Note #${noteId} dismissed.` : `Note #${noteId} not found or already dismissed.`;
32158
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
+ });
32159
33120
  if (sections.length === 0) {
32160
33121
  return `## Notes
32161
33122
 
@@ -32789,6 +33750,115 @@ function createToolRegistry(args) {
32789
33750
  return allTools;
32790
33751
  }
32791
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
+
32792
33862
  // src/index.ts
32793
33863
  init_conflict_detector();
32794
33864
  init_logger();
@@ -32818,7 +33888,16 @@ var plugin = async (ctx) => {
32818
33888
  client: ctx.client,
32819
33889
  dreamerConfig: pluginConfig.dreamer,
32820
33890
  embeddingConfig: pluginConfig.embedding,
32821
- 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
32822
33901
  });
32823
33902
  }
32824
33903
  if (conflictResult?.hasConflict) {
@@ -32898,7 +33977,7 @@ var plugin = async (ctx) => {
32898
33977
  config2.agent = {
32899
33978
  ...config2.agent ?? {},
32900
33979
  [DREAMER_AGENT]: buildHiddenAgentConfig(DREAMER_AGENT, DREAMER_SYSTEM_PROMPT, dreamerAgentOverrides),
32901
- [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),
32902
33981
  [SIDEKICK_AGENT]: buildHiddenAgentConfig(SIDEKICK_AGENT, SIDEKICK_SYSTEM_PROMPT, sidekickAgentOverrides)
32903
33982
  };
32904
33983
  }