@cortexkit/opencode-magic-context 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) 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/memory/storage-memory.d.ts.map +1 -1
  13. package/dist/features/magic-context/migrations.d.ts +8 -0
  14. package/dist/features/magic-context/migrations.d.ts.map +1 -0
  15. package/dist/features/magic-context/plugin-messages.d.ts +75 -0
  16. package/dist/features/magic-context/plugin-messages.d.ts.map +1 -0
  17. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  18. package/dist/features/magic-context/storage-meta-persisted.d.ts +10 -0
  19. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  21. package/dist/features/magic-context/storage-notes.d.ts +51 -5
  22. package/dist/features/magic-context/storage-notes.d.ts.map +1 -1
  23. package/dist/features/magic-context/storage.d.ts +1 -2
  24. package/dist/features/magic-context/storage.d.ts.map +1 -1
  25. package/dist/features/magic-context/user-memory/review-user-memories.d.ts +20 -0
  26. package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -0
  27. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +33 -0
  28. package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -0
  29. package/dist/hooks/magic-context/command-handler.d.ts +4 -0
  30. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/compaction-marker-manager.d.ts +27 -0
  32. package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -0
  33. package/dist/hooks/magic-context/compartment-parser.d.ts +1 -0
  34. package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
  35. package/dist/hooks/magic-context/compartment-prompt.d.ts +4 -1
  36. package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
  37. package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
  40. package/dist/hooks/magic-context/compartment-runner-types.d.ts +5 -0
  41. package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
  42. package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/hook.d.ts +7 -0
  45. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/note-nudger.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/system-prompt-hash.d.ts +2 -0
  50. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  51. package/dist/hooks/magic-context/transform-compartment-phase.d.ts +4 -0
  52. package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
  53. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  54. package/dist/hooks/magic-context/transform.d.ts +2 -0
  55. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1239 -159
  58. package/dist/plugin/dream-timer.d.ts +4 -0
  59. package/dist/plugin/dream-timer.d.ts.map +1 -1
  60. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  61. package/dist/plugin/tui-action-consumer.d.ts +13 -0
  62. package/dist/plugin/tui-action-consumer.d.ts.map +1 -0
  63. package/dist/tools/ctx-note/constants.d.ts +1 -1
  64. package/dist/tools/ctx-note/constants.d.ts.map +1 -1
  65. package/dist/tools/ctx-note/tools.d.ts.map +1 -1
  66. package/dist/tools/ctx-note/types.d.ts +3 -1
  67. package/dist/tools/ctx-note/types.d.ts.map +1 -1
  68. package/dist/tui/data/context-db.d.ts +20 -0
  69. package/dist/tui/data/context-db.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/src/tui/data/context-db.ts +114 -6
  72. package/src/tui/index.tsx +77 -2
  73. package/src/tui/slots/sidebar-content.tsx +3 -2
  74. package/dist/features/magic-context/storage-smart-notes.d.ts +0 -24
  75. 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
  }
@@ -23936,7 +23965,10 @@ function getMemoryCountsByStatus(db, projectPath) {
23936
23965
  };
23937
23966
  for (const row of rows) {
23938
23967
  counts.ids.push(row.id);
23939
- if (row.status === "active") {
23968
+ if (typeof row.superseded_by_memory_id === "number") {
23969
+ counts.merged += 1;
23970
+ counts.mergedIds.push(row.id);
23971
+ } else if (row.status === "active") {
23940
23972
  counts.active += 1;
23941
23973
  } else if (row.status === "permanent") {
23942
23974
  counts.permanent += 1;
@@ -23944,10 +23976,6 @@ function getMemoryCountsByStatus(db, projectPath) {
23944
23976
  counts.archived += 1;
23945
23977
  counts.archivedIds.push(row.id);
23946
23978
  }
23947
- if (typeof row.superseded_by_memory_id === "number") {
23948
- counts.merged += 1;
23949
- counts.mergedIds.push(row.id);
23950
- }
23951
23979
  }
23952
23980
  counts.ids.sort((left, right) => left - right);
23953
23981
  counts.archivedIds.sort((left, right) => left - right);
@@ -23955,40 +23983,96 @@ function getMemoryCountsByStatus(db, projectPath) {
23955
23983
  return counts;
23956
23984
  }
23957
23985
 
23958
- // src/features/magic-context/storage-smart-notes.ts
23959
- function isSmartNoteRow(row) {
23986
+ // src/features/magic-context/storage-notes.ts
23987
+ var NOTE_TYPES = new Set(["session", "smart"]);
23988
+ var NOTE_STATUSES = new Set(["active", "pending", "ready", "dismissed"]);
23989
+ var DEFAULT_SMART_STATUSES = ["pending", "ready"];
23990
+ function toNullableString(value) {
23991
+ return typeof value === "string" && value.length > 0 ? value : null;
23992
+ }
23993
+ function toNullableNumber(value) {
23994
+ return typeof value === "number" ? value : null;
23995
+ }
23996
+ function isNoteRow(row) {
23960
23997
  if (row === null || typeof row !== "object")
23961
23998
  return false;
23962
- const 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";
23999
+ const candidate = row;
24000
+ return typeof candidate.id === "number" && typeof candidate.type === "string" && NOTE_TYPES.has(candidate.type) && typeof candidate.status === "string" && NOTE_STATUSES.has(candidate.status) && typeof candidate.content === "string" && (candidate.session_id === null || typeof candidate.session_id === "string") && (candidate.project_path === null || typeof candidate.project_path === "string") && (candidate.surface_condition === null || typeof candidate.surface_condition === "string") && typeof candidate.created_at === "number" && typeof candidate.updated_at === "number" && (candidate.last_checked_at === null || typeof candidate.last_checked_at === "number") && (candidate.ready_at === null || typeof candidate.ready_at === "number") && (candidate.ready_reason === null || typeof candidate.ready_reason === "string");
23964
24001
  }
23965
- function toSmartNote(row) {
24002
+ function toNote(row) {
23966
24003
  return {
23967
24004
  id: row.id,
23968
- projectPath: row.project_path,
23969
- content: row.content,
23970
- surfaceCondition: row.surface_condition,
24005
+ type: row.type,
23971
24006
  status: row.status,
23972
- createdSessionId: row.created_session_id && row.created_session_id.length > 0 ? row.created_session_id : null,
24007
+ content: row.content,
24008
+ sessionId: toNullableString(row.session_id),
24009
+ projectPath: toNullableString(row.project_path),
24010
+ surfaceCondition: toNullableString(row.surface_condition),
23973
24011
  createdAt: row.created_at,
23974
24012
  updatedAt: row.updated_at,
23975
- lastCheckedAt: row.last_checked_at,
23976
- readyAt: row.ready_at,
23977
- readyReason: row.ready_reason && row.ready_reason.length > 0 ? row.ready_reason : null
24013
+ lastCheckedAt: toNullableNumber(row.last_checked_at),
24014
+ readyAt: toNullableNumber(row.ready_at),
24015
+ readyReason: toNullableString(row.ready_reason)
24016
+ };
24017
+ }
24018
+ function getNoteById(db, noteId) {
24019
+ const row = db.prepare("SELECT * FROM notes WHERE id = ?").get(noteId);
24020
+ return isNoteRow(row) ? toNote(row) : null;
24021
+ }
24022
+ function buildStatusClause(status) {
24023
+ if (status === undefined) {
24024
+ return null;
24025
+ }
24026
+ const statuses = Array.isArray(status) ? status : [status];
24027
+ if (statuses.length === 0) {
24028
+ return null;
24029
+ }
24030
+ const placeholders = statuses.map(() => "?").join(", ");
24031
+ return {
24032
+ sql: `status IN (${placeholders})`,
24033
+ params: statuses
23978
24034
  };
23979
24035
  }
23980
- function addSmartNote(db, projectPath, content, surfaceCondition, sessionId) {
24036
+ function getNotes(db, options = {}) {
24037
+ const clauses = [];
24038
+ const params = [];
24039
+ if (options.sessionId !== undefined) {
24040
+ clauses.push("session_id = ?");
24041
+ params.push(options.sessionId);
24042
+ }
24043
+ if (options.projectPath !== undefined) {
24044
+ clauses.push("project_path = ?");
24045
+ params.push(options.projectPath);
24046
+ }
24047
+ if (options.type !== undefined) {
24048
+ clauses.push("type = ?");
24049
+ params.push(options.type);
24050
+ }
24051
+ const statusClause = buildStatusClause(options.status);
24052
+ if (statusClause) {
24053
+ clauses.push(statusClause.sql);
24054
+ params.push(...statusClause.params);
24055
+ }
24056
+ const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
24057
+ return db.prepare(`SELECT * FROM notes${where} ORDER BY created_at ASC, id ASC`).all(...params).filter(isNoteRow).map(toNote);
24058
+ }
24059
+ function addNote(db, type, options) {
23981
24060
  const now = Date.now();
23982
- const result = db.prepare("INSERT INTO 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");
24061
+ const result = type === "session" ? db.prepare("INSERT INTO notes (type, status, content, session_id, created_at, updated_at) VALUES ('session', 'active', ?, ?, ?, ?) RETURNING *").get(options.content, options.sessionId, now, now) : db.prepare("INSERT INTO notes (type, status, content, session_id, project_path, surface_condition, created_at, updated_at) VALUES ('smart', 'pending', ?, ?, ?, ?, ?, ?) RETURNING *").get(options.content, options.sessionId ?? null, options.projectPath, options.surfaceCondition, now, now);
24062
+ if (!isNoteRow(result)) {
24063
+ throw new Error("[notes] failed to insert note");
23985
24064
  }
23986
- return toSmartNote(result);
24065
+ return toNote(result);
24066
+ }
24067
+ function getSessionNotes(db, sessionId) {
24068
+ return getNotes(db, { sessionId, type: "session", status: "active" });
23987
24069
  }
23988
24070
  function getSmartNotes(db, projectPath, status) {
23989
- 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);
24071
+ return getNotes(db, {
24072
+ projectPath,
24073
+ type: "smart",
24074
+ status: status ?? DEFAULT_SMART_STATUSES
24075
+ });
23992
24076
  }
23993
24077
  function getPendingSmartNotes(db, projectPath) {
23994
24078
  return getSmartNotes(db, projectPath, "pending");
@@ -23996,17 +24080,294 @@ function getPendingSmartNotes(db, projectPath) {
23996
24080
  function getReadySmartNotes(db, projectPath) {
23997
24081
  return getSmartNotes(db, projectPath, "ready");
23998
24082
  }
23999
- function markSmartNoteReady(db, noteId, readyReason) {
24083
+ function updateNote(db, noteId, updates) {
24084
+ const existing = getNoteById(db, noteId);
24085
+ if (!existing) {
24086
+ return null;
24087
+ }
24000
24088
  const now = Date.now();
24001
- 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);
24089
+ const sets = ["updated_at = ?"];
24090
+ const params = [now];
24091
+ if (updates.content !== undefined) {
24092
+ sets.push("content = ?");
24093
+ params.push(updates.content);
24094
+ }
24095
+ if (updates.sessionId !== undefined) {
24096
+ sets.push("session_id = ?");
24097
+ params.push(updates.sessionId);
24098
+ }
24099
+ if (updates.status !== undefined) {
24100
+ sets.push("status = ?");
24101
+ params.push(updates.status);
24102
+ }
24103
+ if (existing.type === "smart") {
24104
+ if (updates.projectPath !== undefined) {
24105
+ sets.push("project_path = ?");
24106
+ params.push(updates.projectPath);
24107
+ }
24108
+ if (updates.surfaceCondition !== undefined) {
24109
+ sets.push("surface_condition = ?");
24110
+ params.push(updates.surfaceCondition);
24111
+ }
24112
+ if (updates.lastCheckedAt !== undefined) {
24113
+ sets.push("last_checked_at = ?");
24114
+ params.push(updates.lastCheckedAt);
24115
+ }
24116
+ if (updates.readyAt !== undefined) {
24117
+ sets.push("ready_at = ?");
24118
+ params.push(updates.readyAt);
24119
+ }
24120
+ if (updates.readyReason !== undefined) {
24121
+ sets.push("ready_reason = ?");
24122
+ params.push(updates.readyReason);
24123
+ }
24124
+ }
24125
+ if (sets.length === 1) {
24126
+ return null;
24127
+ }
24128
+ params.push(noteId);
24129
+ const result = db.prepare(`UPDATE notes SET ${sets.join(", ")} WHERE id = ? RETURNING *`).get(...params);
24130
+ return isNoteRow(result) ? toNote(result) : null;
24002
24131
  }
24003
- function markSmartNoteChecked(db, noteId) {
24132
+ function dismissNote(db, noteId) {
24133
+ const result = db.prepare("UPDATE notes SET status = 'dismissed', updated_at = ? WHERE id = ? AND status != 'dismissed'").run(Date.now(), noteId);
24134
+ return result.changes > 0;
24135
+ }
24136
+ function markNoteReady(db, noteId, reason) {
24004
24137
  const now = Date.now();
24005
- db.prepare("UPDATE smart_notes SET last_checked_at = ?, updated_at = ? WHERE id = ?").run(now, now, noteId);
24138
+ db.prepare("UPDATE notes SET status = 'ready', ready_at = ?, ready_reason = ?, updated_at = ?, last_checked_at = ? WHERE id = ? AND type = 'smart'").run(now, reason ?? null, now, now, noteId);
24006
24139
  }
24007
- function 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;
24140
+ function markNoteChecked(db, noteId) {
24141
+ const now = Date.now();
24142
+ db.prepare("UPDATE notes SET last_checked_at = ?, updated_at = ? WHERE id = ? AND type = 'smart'").run(now, now, noteId);
24143
+ }
24144
+
24145
+ // src/features/magic-context/user-memory/review-user-memories.ts
24146
+ init_logger();
24147
+
24148
+ // src/features/magic-context/user-memory/storage-user-memory.ts
24149
+ function insertUserMemoryCandidates(db, candidates) {
24150
+ if (candidates.length === 0)
24151
+ return;
24152
+ const now = Date.now();
24153
+ const stmt = db.prepare("INSERT INTO user_memory_candidates (content, session_id, source_compartment_start, source_compartment_end, created_at) VALUES (?, ?, ?, ?, ?)");
24154
+ db.transaction(() => {
24155
+ for (const c of candidates) {
24156
+ stmt.run(c.content, c.sessionId, c.sourceCompartmentStart ?? null, c.sourceCompartmentEnd ?? null, now);
24157
+ }
24158
+ })();
24159
+ }
24160
+ function getUserMemoryCandidates(db) {
24161
+ const rows = db.prepare("SELECT id, content, session_id, source_compartment_start, source_compartment_end, created_at FROM user_memory_candidates ORDER BY created_at ASC").all();
24162
+ return rows.map((r) => ({
24163
+ id: r.id,
24164
+ content: r.content,
24165
+ sessionId: r.session_id,
24166
+ sourceCompartmentStart: r.source_compartment_start,
24167
+ sourceCompartmentEnd: r.source_compartment_end,
24168
+ createdAt: r.created_at
24169
+ }));
24170
+ }
24171
+ function deleteUserMemoryCandidates(db, ids) {
24172
+ if (ids.length === 0)
24173
+ return;
24174
+ const placeholders = ids.map(() => "?").join(",");
24175
+ db.prepare(`DELETE FROM user_memory_candidates WHERE id IN (${placeholders})`).run(...ids);
24176
+ }
24177
+ function insertUserMemory(db, content, sourceCandidateIds) {
24178
+ const now = Date.now();
24179
+ const result = db.prepare("INSERT INTO user_memories (content, status, promoted_at, source_candidate_ids, created_at, updated_at) VALUES (?, 'active', ?, ?, ?, ?)").run(content, now, JSON.stringify(sourceCandidateIds), now, now);
24180
+ return Number(result.lastInsertRowid);
24181
+ }
24182
+ function getActiveUserMemories(db) {
24183
+ const rows = db.prepare("SELECT id, content, status, promoted_at, source_candidate_ids, created_at, updated_at FROM user_memories WHERE status = 'active' ORDER BY promoted_at ASC").all();
24184
+ return rows.map(parseUserMemoryRow);
24185
+ }
24186
+ function updateUserMemoryContent(db, id, content) {
24187
+ db.prepare("UPDATE user_memories SET content = ?, updated_at = ? WHERE id = ?").run(content, Date.now(), id);
24188
+ }
24189
+ function dismissUserMemory(db, id) {
24190
+ db.prepare("UPDATE user_memories SET status = 'dismissed', updated_at = ? WHERE id = ?").run(Date.now(), id);
24191
+ }
24192
+ function parseUserMemoryRow(row) {
24193
+ let candidateIds = [];
24194
+ try {
24195
+ candidateIds = JSON.parse(row.source_candidate_ids);
24196
+ } catch {}
24197
+ return {
24198
+ id: row.id,
24199
+ content: row.content,
24200
+ status: row.status === "dismissed" ? "dismissed" : "active",
24201
+ promotedAt: row.promoted_at,
24202
+ sourceCandidateIds: candidateIds,
24203
+ createdAt: row.created_at,
24204
+ updatedAt: row.updated_at
24205
+ };
24206
+ }
24207
+
24208
+ // src/features/magic-context/user-memory/review-user-memories.ts
24209
+ async function reviewUserMemories(args) {
24210
+ const result = { promoted: 0, merged: 0, dismissed: 0, candidatesConsumed: 0 };
24211
+ const candidates = getUserMemoryCandidates(args.db);
24212
+ if (candidates.length < args.promotionThreshold) {
24213
+ log(`[dreamer] user-memories: ${candidates.length} candidate(s), need ${args.promotionThreshold} \u2014 skipping`);
24214
+ return result;
24215
+ }
24216
+ const stableMemories = getActiveUserMemories(args.db);
24217
+ log(`[dreamer] user-memories: reviewing ${candidates.length} candidate(s) against ${stableMemories.length} stable memorie(s)`);
24218
+ const candidateList = candidates.map((c) => `- Candidate #${c.id} [session ${c.sessionId.slice(0, 12)}]: "${c.content}"`).join(`
24219
+ `);
24220
+ const stableList = stableMemories.length > 0 ? stableMemories.map((m) => `- Memory #${m.id}: "${m.content}"`).join(`
24221
+ `) : "(none)";
24222
+ const prompt = `## Task: Review User Memory Candidates
24223
+
24224
+ You are reviewing behavioral observations about a human user to decide which patterns are real and persistent.
24225
+
24226
+ ### Current Stable User Memories
24227
+ ${stableList}
24228
+
24229
+ ### Candidate Observations (from recent historian runs)
24230
+ ${candidateList}
24231
+
24232
+ ### Instructions
24233
+
24234
+ 1. Look for **recurring patterns** across multiple candidates \u2014 observations that appear independently from different sessions or historian runs indicate a real user trait.
24235
+ 2. A candidate must appear in at least ${args.promotionThreshold} semantically similar variants before promotion.
24236
+ 3. Only promote **truly universal** user traits \u2014 communication style, expertise level, review focus, decision-making patterns, working habits.
24237
+ 4. Do NOT promote: project-specific preferences, framework choices, one-off moods, task-local frustrations.
24238
+ 5. If a candidate is semantically equivalent to an existing stable memory, mark it as already covered.
24239
+ 6. If multiple candidates describe the same trait, merge them into one clean statement.
24240
+ 7. If an existing stable memory should be updated based on new evidence, include the update.
24241
+
24242
+ ### Output Format
24243
+
24244
+ Return valid JSON (no markdown fencing):
24245
+
24246
+ {
24247
+ "promote": [
24248
+ { "content": "Clean universal observation text", "candidate_ids": [1, 3, 7] }
24249
+ ],
24250
+ "update_existing": [
24251
+ { "memory_id": 5, "content": "Updated text incorporating new evidence", "candidate_ids": [2] }
24252
+ ],
24253
+ "dismiss_existing": [
24254
+ { "memory_id": 3, "reason": "No longer supported by recent observations" }
24255
+ ],
24256
+ "consume_candidate_ids": [1, 2, 3, 4, 5, 7, 8]
24257
+ }
24258
+
24259
+ - \`promote\`: new stable memories to create from candidates
24260
+ - \`update_existing\`: existing stable memories to rewrite with new evidence
24261
+ - \`dismiss_existing\`: existing stable memories that are no longer valid
24262
+ - \`consume_candidate_ids\`: ALL candidate IDs that were reviewed (promoted, merged, or rejected) \u2014 they will be deleted from the candidate pool
24263
+
24264
+ If no promotions are warranted, return empty arrays. Always consume reviewed candidates so they don't accumulate indefinitely.`;
24265
+ let agentSessionId = null;
24266
+ const abortController = new AbortController;
24267
+ const leaseInterval = setInterval(() => {
24268
+ try {
24269
+ if (!renewLease(args.db, args.holderId)) {
24270
+ log("[dreamer] user-memories: lease renewal failed \u2014 aborting");
24271
+ abortController.abort();
24272
+ }
24273
+ } catch {
24274
+ abortController.abort();
24275
+ }
24276
+ }, 60000);
24277
+ try {
24278
+ const createResponse = await args.client.session.create({
24279
+ body: {
24280
+ ...args.parentSessionId ? { parentID: args.parentSessionId } : {},
24281
+ title: "magic-context-dream-user-memories"
24282
+ },
24283
+ query: { directory: args.sessionDirectory }
24284
+ });
24285
+ const created = normalizeSDKResponse(createResponse, null, { preferResponseOnMissingData: true });
24286
+ agentSessionId = typeof created?.id === "string" ? created.id : null;
24287
+ if (!agentSessionId)
24288
+ throw new Error("Could not create user memory review session.");
24289
+ log(`[dreamer] user-memories: child session created ${agentSessionId}`);
24290
+ const remainingMs = Math.max(0, args.deadline - Date.now());
24291
+ await promptSyncWithModelSuggestionRetry(args.client, {
24292
+ path: { id: agentSessionId },
24293
+ query: { directory: args.sessionDirectory },
24294
+ body: {
24295
+ agent: DREAMER_AGENT,
24296
+ system: DREAMER_SYSTEM_PROMPT,
24297
+ parts: [{ type: "text", text: prompt }]
24298
+ }
24299
+ }, { timeoutMs: Math.min(remainingMs, 5 * 60 * 1000), signal: abortController.signal });
24300
+ const messagesResponse = await args.client.session.messages({
24301
+ path: { id: agentSessionId },
24302
+ query: { directory: args.sessionDirectory }
24303
+ });
24304
+ const messages = normalizeSDKResponse(messagesResponse, [], {
24305
+ preferResponseOnMissingData: true
24306
+ });
24307
+ const responseText = extractLatestAssistantText(messages);
24308
+ if (!responseText) {
24309
+ log("[dreamer] user-memories: no response from review agent");
24310
+ return result;
24311
+ }
24312
+ const jsonMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)```/) ?? responseText.match(/(\{[\s\S]*\})/);
24313
+ if (!jsonMatch) {
24314
+ log("[dreamer] user-memories: could not parse JSON from response");
24315
+ return result;
24316
+ }
24317
+ let parsed;
24318
+ try {
24319
+ parsed = JSON.parse(jsonMatch[1]);
24320
+ } catch {
24321
+ log("[dreamer] user-memories: JSON parse failed");
24322
+ return result;
24323
+ }
24324
+ if (parsed.promote) {
24325
+ for (const p of parsed.promote) {
24326
+ if (p.content?.trim()) {
24327
+ insertUserMemory(args.db, p.content.trim(), p.candidate_ids ?? []);
24328
+ result.promoted++;
24329
+ log(`[dreamer] user-memories: promoted "${p.content.trim().slice(0, 60)}..."`);
24330
+ }
24331
+ }
24332
+ }
24333
+ if (parsed.update_existing) {
24334
+ for (const u of parsed.update_existing) {
24335
+ if (u.memory_id && u.content?.trim()) {
24336
+ updateUserMemoryContent(args.db, u.memory_id, u.content.trim());
24337
+ result.merged++;
24338
+ log(`[dreamer] user-memories: updated memory #${u.memory_id}`);
24339
+ }
24340
+ }
24341
+ }
24342
+ if (parsed.dismiss_existing) {
24343
+ for (const d of parsed.dismiss_existing) {
24344
+ if (d.memory_id) {
24345
+ dismissUserMemory(args.db, d.memory_id);
24346
+ result.dismissed++;
24347
+ log(`[dreamer] user-memories: dismissed memory #${d.memory_id} \u2014 ${d.reason ?? "no reason"}`);
24348
+ }
24349
+ }
24350
+ }
24351
+ if (parsed.consume_candidate_ids && parsed.consume_candidate_ids.length > 0) {
24352
+ deleteUserMemoryCandidates(args.db, parsed.consume_candidate_ids);
24353
+ result.candidatesConsumed = parsed.consume_candidate_ids.length;
24354
+ log(`[dreamer] user-memories: consumed ${result.candidatesConsumed} candidate(s)`);
24355
+ }
24356
+ return result;
24357
+ } catch (error48) {
24358
+ log(`[dreamer] user-memories: review failed: ${getErrorMessage(error48)}`);
24359
+ return result;
24360
+ } finally {
24361
+ clearInterval(leaseInterval);
24362
+ if (agentSessionId) {
24363
+ await args.client.session.delete({
24364
+ path: { id: agentSessionId },
24365
+ query: { directory: args.sessionDirectory }
24366
+ }).catch((e) => {
24367
+ log(`[dreamer] user-memories: session cleanup failed: ${getErrorMessage(e)}`);
24368
+ });
24369
+ }
24370
+ }
24010
24371
  }
24011
24372
 
24012
24373
  // src/features/magic-context/dreamer/storage-dream-runs.ts
@@ -24186,6 +24547,24 @@ async function runDream(args) {
24186
24547
  }
24187
24548
  }
24188
24549
  }
24550
+ if (args.experimentalUserMemories?.enabled && Date.now() <= deadline) {
24551
+ try {
24552
+ const reviewResult = await reviewUserMemories({
24553
+ db: args.db,
24554
+ client: args.client,
24555
+ parentSessionId,
24556
+ sessionDirectory: args.sessionDirectory,
24557
+ holderId,
24558
+ deadline,
24559
+ promotionThreshold: args.experimentalUserMemories.promotionThreshold
24560
+ });
24561
+ if (reviewResult.promoted > 0 || reviewResult.merged > 0 || reviewResult.dismissed > 0) {
24562
+ log(`[dreamer] user-memories: promoted=${reviewResult.promoted} merged=${reviewResult.merged} dismissed=${reviewResult.dismissed} consumed=${reviewResult.candidatesConsumed}`);
24563
+ }
24564
+ } catch (error48) {
24565
+ log(`[dreamer] user-memory review failed: ${getErrorMessage(error48)}`);
24566
+ }
24567
+ }
24189
24568
  if (Date.now() <= deadline) {
24190
24569
  try {
24191
24570
  await evaluateSmartNotes({
@@ -24326,7 +24705,7 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
24326
24705
  if (!jsonMatch) {
24327
24706
  log("[dreamer] smart notes: no JSON array found in output, skipping");
24328
24707
  for (const note of pendingNotes)
24329
- markSmartNoteChecked(args.db, note.id);
24708
+ markNoteChecked(args.db, note.id);
24330
24709
  throw new Error("Smart note evaluation returned no JSON array.");
24331
24710
  }
24332
24711
  let evaluations;
@@ -24335,7 +24714,7 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
24335
24714
  } catch {
24336
24715
  log(`[dreamer] smart notes: failed to parse JSON from LLM output, marking all checked`);
24337
24716
  for (const note of pendingNotes)
24338
- markSmartNoteChecked(args.db, note.id);
24717
+ markNoteChecked(args.db, note.id);
24339
24718
  throw new Error("Smart note evaluation returned invalid JSON.");
24340
24719
  }
24341
24720
  let surfaced = 0;
@@ -24346,16 +24725,16 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
24346
24725
  if (!note)
24347
24726
  continue;
24348
24727
  if (evaluation.met) {
24349
- markSmartNoteReady(args.db, note.id, evaluation.reason);
24728
+ markNoteReady(args.db, note.id, evaluation.reason);
24350
24729
  surfaced++;
24351
24730
  log(`[dreamer] smart notes: #${note.id} condition MET \u2014 "${evaluation.reason ?? "condition satisfied"}"`);
24352
24731
  } else {
24353
- markSmartNoteChecked(args.db, note.id);
24732
+ markNoteChecked(args.db, note.id);
24354
24733
  }
24355
24734
  }
24356
24735
  for (const note of pendingNotes) {
24357
24736
  if (!evaluations.some((e) => e.id === note.id)) {
24358
- markSmartNoteChecked(args.db, note.id);
24737
+ markNoteChecked(args.db, note.id);
24359
24738
  }
24360
24739
  }
24361
24740
  const durationMs = Date.now() - taskStartedAt;
@@ -24409,7 +24788,8 @@ async function processDreamQueue(args) {
24409
24788
  tasks: args.tasks,
24410
24789
  taskTimeoutMinutes: args.taskTimeoutMinutes,
24411
24790
  maxRuntimeMinutes: args.maxRuntimeMinutes,
24412
- sessionDirectory: projectDirectory
24791
+ sessionDirectory: projectDirectory,
24792
+ experimentalUserMemories: args.experimentalUserMemories
24413
24793
  });
24414
24794
  } catch (error48) {
24415
24795
  log(`[dreamer] runDream threw for ${entry.projectIdentity}: ${getErrorMessage(error48)}`);
@@ -24461,7 +24841,8 @@ function isInScheduleWindow(schedule, now = new Date) {
24461
24841
  function findProjectsNeedingDream(db) {
24462
24842
  const projectRows = db.query(`SELECT DISTINCT project_path FROM memories WHERE status = 'active'
24463
24843
  UNION
24464
- SELECT DISTINCT project_path FROM smart_notes WHERE status = 'pending'
24844
+ SELECT DISTINCT project_path FROM notes
24845
+ WHERE type = 'smart' AND status = 'pending' AND project_path IS NOT NULL
24465
24846
  ORDER BY project_path`).all();
24466
24847
  const projects = [];
24467
24848
  for (const row of projectRows) {
@@ -24470,8 +24851,8 @@ function findProjectsNeedingDream(db) {
24470
24851
  const lastDreamAt = Number(lastDreamAtStr ?? fallbackStr ?? "0") || 0;
24471
24852
  const updatedMemories = db.query(`SELECT COUNT(*) as cnt FROM memories
24472
24853
  WHERE project_path = ? AND status = 'active' AND updated_at > ?`).get(row.project_path, lastDreamAt);
24473
- const pendingSmartNotes = db.query(`SELECT COUNT(*) as cnt FROM smart_notes
24474
- WHERE project_path = ? AND status = 'pending'`).get(row.project_path);
24854
+ const pendingSmartNotes = db.query(`SELECT COUNT(*) as cnt FROM notes
24855
+ WHERE project_path = ? AND type = 'smart' AND status = 'pending'`).get(row.project_path);
24475
24856
  if (updatedMemories && updatedMemories.cnt > 0 || pendingSmartNotes && pendingSmartNotes.cnt > 0) {
24476
24857
  projects.push(row.project_path);
24477
24858
  }
@@ -24518,6 +24899,22 @@ function cosineSimilarity(a, b) {
24518
24899
 
24519
24900
  // src/features/magic-context/memory/embedding-local.ts
24520
24901
  init_logger();
24902
+ async function withQuietConsole(fn) {
24903
+ const origWarn = console.warn;
24904
+ const origError = console.error;
24905
+ const redirect = (...args) => {
24906
+ const message = args.map((a) => typeof a === "string" ? a : String(a)).join(" ");
24907
+ log(`[transformers] ${message}`);
24908
+ };
24909
+ console.warn = redirect;
24910
+ console.error = redirect;
24911
+ try {
24912
+ return await fn();
24913
+ } finally {
24914
+ console.warn = origWarn;
24915
+ console.error = origError;
24916
+ }
24917
+ }
24521
24918
  function isArrayLikeNumber(value) {
24522
24919
  if (typeof value !== "object" || value === null || !("length" in value)) {
24523
24920
  return false;
@@ -24573,10 +24970,16 @@ class LocalEmbeddingProvider {
24573
24970
  this.initPromise = (async () => {
24574
24971
  try {
24575
24972
  const transformersModule = await import("@huggingface/transformers");
24973
+ const env = transformersModule.env;
24974
+ const LogLevel = transformersModule.LogLevel;
24975
+ if (LogLevel && "ERROR" in LogLevel) {
24976
+ env.logLevel = LogLevel.ERROR;
24977
+ }
24576
24978
  const createPipeline = transformersModule.pipeline;
24577
- this.pipeline = await createPipeline("feature-extraction", this.model, {
24578
- quantized: true
24579
- });
24979
+ this.pipeline = await withQuietConsole(() => createPipeline("feature-extraction", this.model, {
24980
+ quantized: true,
24981
+ dtype: "fp32"
24982
+ }));
24580
24983
  log(`[magic-context] embedding model loaded: ${this.model}`);
24581
24984
  } catch (error48) {
24582
24985
  log("[magic-context] embedding model failed to load:", error48);
@@ -24593,13 +24996,14 @@ class LocalEmbeddingProvider {
24593
24996
  return null;
24594
24997
  }
24595
24998
  try {
24596
- if (!this.pipeline) {
24999
+ const pipeline = this.pipeline;
25000
+ if (!pipeline) {
24597
25001
  return null;
24598
25002
  }
24599
- const result = await this.pipeline(text, {
25003
+ const result = await withQuietConsole(() => pipeline(text, {
24600
25004
  pooling: "mean",
24601
25005
  normalize: true
24602
- });
25006
+ }));
24603
25007
  return extractBatchEmbeddings(result, 1)[0] ?? null;
24604
25008
  } catch (error48) {
24605
25009
  log("[magic-context] embedding failed:", error48);
@@ -24614,13 +25018,14 @@ class LocalEmbeddingProvider {
24614
25018
  return Array.from({ length: texts.length }, () => null);
24615
25019
  }
24616
25020
  try {
24617
- if (!this.pipeline) {
25021
+ const pipeline = this.pipeline;
25022
+ if (!pipeline) {
24618
25023
  return Array.from({ length: texts.length }, () => null);
24619
25024
  }
24620
- const result = await this.pipeline(texts, {
25025
+ const result = await withQuietConsole(() => pipeline(texts, {
24621
25026
  pooling: "mean",
24622
25027
  normalize: true
24623
- });
25028
+ }));
24624
25029
  return extractBatchEmbeddings(result, texts.length);
24625
25030
  } catch (error48) {
24626
25031
  log("[magic-context] embedding batch failed:", error48);
@@ -25220,7 +25625,11 @@ function readRawSessionMessagesFromDb(db, sessionId) {
25220
25625
  list.push(parseJsonUnknown(part.data));
25221
25626
  partsByMessageId.set(part.message_id, list);
25222
25627
  }
25223
- return messageRows.flatMap((row, index) => {
25628
+ const filtered = messageRows.filter((row) => {
25629
+ const info = parseJsonRecord(row.data);
25630
+ return !(info?.summary === true && info?.finish === "stop");
25631
+ });
25632
+ return filtered.flatMap((row, index) => {
25224
25633
  const info = parseJsonRecord(row.data);
25225
25634
  if (!info)
25226
25635
  return [];
@@ -25594,6 +26003,158 @@ import { Database as Database2 } from "bun:sqlite";
25594
26003
  import { mkdirSync } from "fs";
25595
26004
  import { join as join9 } from "path";
25596
26005
  init_logger();
26006
+
26007
+ // src/features/magic-context/migrations.ts
26008
+ init_logger();
26009
+ var MIGRATIONS = [
26010
+ {
26011
+ version: 1,
26012
+ description: "Merge session_notes + smart_notes into unified notes table",
26013
+ up: (db) => {
26014
+ db.exec(`
26015
+ CREATE TABLE IF NOT EXISTS notes (
26016
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26017
+ type TEXT NOT NULL DEFAULT 'session',
26018
+ status TEXT NOT NULL DEFAULT 'active',
26019
+ content TEXT NOT NULL,
26020
+ session_id TEXT,
26021
+ project_path TEXT,
26022
+ surface_condition TEXT,
26023
+ created_at INTEGER NOT NULL,
26024
+ updated_at INTEGER NOT NULL,
26025
+ last_checked_at INTEGER,
26026
+ ready_at INTEGER,
26027
+ ready_reason TEXT
26028
+ );
26029
+ CREATE INDEX IF NOT EXISTS idx_notes_session_status ON notes(session_id, status);
26030
+ CREATE INDEX IF NOT EXISTS idx_notes_project_status ON notes(project_path, status);
26031
+ CREATE INDEX IF NOT EXISTS idx_notes_type_status ON notes(type, status);
26032
+ `);
26033
+ const hasSessionNotes = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='session_notes'").get();
26034
+ if (hasSessionNotes) {
26035
+ db.exec(`
26036
+ INSERT INTO notes (type, status, content, session_id, created_at, updated_at)
26037
+ SELECT 'session', 'active', content, session_id, created_at, created_at
26038
+ FROM session_notes
26039
+ `);
26040
+ }
26041
+ const hasSmartNotes = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='smart_notes'").get();
26042
+ if (hasSmartNotes) {
26043
+ db.exec(`
26044
+ INSERT INTO notes (type, status, content, session_id, project_path, surface_condition,
26045
+ created_at, updated_at, last_checked_at, ready_at, ready_reason)
26046
+ SELECT 'smart', status, content, created_session_id, project_path, surface_condition,
26047
+ created_at, updated_at, last_checked_at, ready_at, ready_reason
26048
+ FROM smart_notes
26049
+ `);
26050
+ }
26051
+ if (hasSessionNotes) {
26052
+ const sourceCount = db.prepare("SELECT COUNT(*) as c FROM session_notes").get().c;
26053
+ const migratedCount = db.prepare("SELECT COUNT(*) as c FROM notes WHERE type = 'session'").get().c;
26054
+ if (migratedCount >= sourceCount) {
26055
+ db.exec("DROP TABLE session_notes");
26056
+ } else {
26057
+ throw new Error(`session_notes migration verification failed: expected ${sourceCount} rows, got ${migratedCount}`);
26058
+ }
26059
+ }
26060
+ if (hasSmartNotes) {
26061
+ const sourceCount = db.prepare("SELECT COUNT(*) as c FROM smart_notes").get().c;
26062
+ const migratedCount = db.prepare("SELECT COUNT(*) as c FROM notes WHERE type = 'smart'").get().c;
26063
+ if (migratedCount >= sourceCount) {
26064
+ db.exec("DROP TABLE smart_notes");
26065
+ } else {
26066
+ throw new Error(`smart_notes migration verification failed: expected ${sourceCount} rows, got ${migratedCount}`);
26067
+ }
26068
+ }
26069
+ }
26070
+ },
26071
+ {
26072
+ version: 2,
26073
+ description: "Add plugin_messages table for TUI \u2194 server communication",
26074
+ up: (db) => {
26075
+ db.exec(`
26076
+ CREATE TABLE IF NOT EXISTS plugin_messages (
26077
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26078
+ direction TEXT NOT NULL,
26079
+ type TEXT NOT NULL,
26080
+ payload TEXT NOT NULL DEFAULT '{}',
26081
+ session_id TEXT,
26082
+ created_at INTEGER NOT NULL,
26083
+ consumed_at INTEGER
26084
+ );
26085
+ CREATE INDEX IF NOT EXISTS idx_plugin_messages_direction_consumed
26086
+ ON plugin_messages(direction, consumed_at);
26087
+ CREATE INDEX IF NOT EXISTS idx_plugin_messages_created
26088
+ ON plugin_messages(created_at);
26089
+ `);
26090
+ }
26091
+ },
26092
+ {
26093
+ version: 3,
26094
+ description: "Add user_memory_candidates and user_memories tables",
26095
+ up: (db) => {
26096
+ db.exec(`
26097
+ CREATE TABLE IF NOT EXISTS user_memory_candidates (
26098
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26099
+ content TEXT NOT NULL,
26100
+ session_id TEXT NOT NULL,
26101
+ source_compartment_start INTEGER,
26102
+ source_compartment_end INTEGER,
26103
+ created_at INTEGER NOT NULL
26104
+ );
26105
+ CREATE INDEX IF NOT EXISTS idx_umc_created ON user_memory_candidates(created_at);
26106
+
26107
+ CREATE TABLE IF NOT EXISTS user_memories (
26108
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26109
+ content TEXT NOT NULL,
26110
+ status TEXT NOT NULL DEFAULT 'active',
26111
+ promoted_at INTEGER NOT NULL,
26112
+ source_candidate_ids TEXT DEFAULT '[]',
26113
+ created_at INTEGER NOT NULL,
26114
+ updated_at INTEGER NOT NULL
26115
+ );
26116
+ CREATE INDEX IF NOT EXISTS idx_um_status ON user_memories(status);
26117
+ `);
26118
+ }
26119
+ }
26120
+ ];
26121
+ function ensureMigrationsTable(db) {
26122
+ db.exec(`
26123
+ CREATE TABLE IF NOT EXISTS schema_migrations (
26124
+ version INTEGER PRIMARY KEY,
26125
+ description TEXT NOT NULL,
26126
+ applied_at INTEGER NOT NULL
26127
+ )
26128
+ `);
26129
+ }
26130
+ function getCurrentVersion(db) {
26131
+ const row = db.prepare("SELECT MAX(version) as version FROM schema_migrations").get();
26132
+ return row?.version ?? 0;
26133
+ }
26134
+ function runMigrations(db) {
26135
+ ensureMigrationsTable(db);
26136
+ const currentVersion = getCurrentVersion(db);
26137
+ const pendingMigrations = MIGRATIONS.filter((m) => m.version > currentVersion);
26138
+ if (pendingMigrations.length === 0) {
26139
+ return;
26140
+ }
26141
+ log(`[migrations] current schema version: ${currentVersion}, applying ${pendingMigrations.length} migration(s)`);
26142
+ for (const migration of pendingMigrations) {
26143
+ try {
26144
+ db.transaction(() => {
26145
+ migration.up(db);
26146
+ db.prepare("INSERT INTO schema_migrations (version, description, applied_at) VALUES (?, ?, ?)").run(migration.version, migration.description, Date.now());
26147
+ })();
26148
+ log(`[migrations] applied v${migration.version}: ${migration.description}`);
26149
+ } catch (error48) {
26150
+ log(`[migrations] FAILED v${migration.version}: ${migration.description} \u2014 ${error48 instanceof Error ? error48.message : String(error48)}`);
26151
+ throw new Error(`Migration v${migration.version} failed: ${error48 instanceof Error ? error48.message : String(error48)}. Database may need manual repair.`);
26152
+ }
26153
+ }
26154
+ log(`[migrations] schema version now: ${MIGRATIONS[MIGRATIONS.length - 1].version}`);
26155
+ }
26156
+
26157
+ // src/features/magic-context/storage-db.ts
25597
26158
  var databases = new Map;
25598
26159
  var FALLBACK_DATABASE_KEY = "__fallback__:memory:";
25599
26160
  var persistenceByDatabase = new WeakMap;
@@ -25873,6 +26434,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
25873
26434
  ensureColumn(db, "dream_queue", "retry_count", "INTEGER DEFAULT 0");
25874
26435
  ensureColumn(db, "tags", "reasoning_byte_size", "INTEGER DEFAULT 0");
25875
26436
  ensureColumn(db, "session_meta", "system_prompt_tokens", "INTEGER DEFAULT 0");
26437
+ ensureColumn(db, "session_meta", "compaction_marker_state", "TEXT DEFAULT ''");
25876
26438
  }
25877
26439
  function ensureColumn(db, table, column, definition) {
25878
26440
  if (!/^[a-z_]+$/.test(table) || !/^[a-z_]+$/.test(column) || !/^[A-Z0-9_'(),\s]+$/i.test(definition)) {
@@ -25888,6 +26450,7 @@ function createFallbackDatabase() {
25888
26450
  try {
25889
26451
  const fallback = new Database2(":memory:");
25890
26452
  initializeDatabase(fallback);
26453
+ runMigrations(fallback);
25891
26454
  return fallback;
25892
26455
  } catch (error48) {
25893
26456
  throw new Error(`[magic-context] storage fatal: failed to initialize fallback database: ${getErrorMessage(error48)}`);
@@ -25906,6 +26469,7 @@ function openDatabase() {
25906
26469
  mkdirSync(dbDir, { recursive: true });
25907
26470
  const db = new Database2(dbPath);
25908
26471
  initializeDatabase(db);
26472
+ runMigrations(db);
25909
26473
  databases.set(dbPath, db);
25910
26474
  persistenceByDatabase.set(db, true);
25911
26475
  persistenceErrorByDatabase.delete(db);
@@ -26138,6 +26702,24 @@ function setPersistedDeliveredNoteNudge(db, sessionId, text, messageId = "") {
26138
26702
  function clearPersistedNoteNudge(db, sessionId) {
26139
26703
  db.prepare("UPDATE session_meta SET note_nudge_trigger_pending = 0, note_nudge_trigger_message_id = '', note_nudge_sticky_text = '', note_nudge_sticky_message_id = '' WHERE session_id = ?").run(sessionId);
26140
26704
  }
26705
+ function getPersistedCompactionMarkerState(db, sessionId) {
26706
+ const row = db.prepare("SELECT compaction_marker_state FROM session_meta WHERE session_id = ?").get(sessionId);
26707
+ const raw = row?.compaction_marker_state;
26708
+ if (!raw || raw.length === 0)
26709
+ return null;
26710
+ try {
26711
+ const parsed = JSON.parse(raw);
26712
+ if (parsed && typeof parsed === "object" && typeof parsed.boundaryMessageId === "string" && typeof parsed.summaryMessageId === "string" && typeof parsed.compactionPartId === "string" && typeof parsed.summaryPartId === "string" && typeof parsed.boundaryOrdinal === "number") {
26713
+ return parsed;
26714
+ }
26715
+ } catch {}
26716
+ return null;
26717
+ }
26718
+ function setPersistedCompactionMarkerState(db, sessionId, state) {
26719
+ ensureSessionMetaRow(db, sessionId);
26720
+ const json2 = state ? JSON.stringify(state) : "";
26721
+ db.prepare("UPDATE session_meta SET compaction_marker_state = ? WHERE session_id = ?").run(json2, sessionId);
26722
+ }
26141
26723
  function getStrippedPlaceholderIds(db, sessionId) {
26142
26724
  const row = db.prepare("SELECT stripped_placeholder_ids FROM session_meta WHERE session_id = ?").get(sessionId);
26143
26725
  const raw = row?.stripped_placeholder_ids;
@@ -26208,37 +26790,13 @@ function clearSession(db, sessionId) {
26208
26790
  db.prepare("DELETE FROM compartments WHERE session_id = ?").run(sessionId);
26209
26791
  clearCompressionDepth(db, sessionId);
26210
26792
  db.prepare("DELETE FROM session_facts WHERE session_id = ?").run(sessionId);
26211
- db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
26793
+ db.prepare("DELETE FROM notes WHERE session_id = ? AND type = 'session'").run(sessionId);
26212
26794
  db.prepare("DELETE FROM recomp_compartments WHERE session_id = ?").run(sessionId);
26213
26795
  db.prepare("DELETE FROM recomp_facts WHERE session_id = ?").run(sessionId);
26796
+ db.prepare("DELETE FROM user_memory_candidates WHERE session_id = ?").run(sessionId);
26214
26797
  clearIndexedMessages(db, sessionId);
26215
26798
  })();
26216
26799
  }
26217
- // src/features/magic-context/storage-notes.ts
26218
- function isSessionNoteRow(row) {
26219
- if (row === null || typeof row !== "object")
26220
- return false;
26221
- const candidate = row;
26222
- return typeof candidate.id === "number" && typeof candidate.session_id === "string" && typeof candidate.content === "string" && typeof candidate.created_at === "number";
26223
- }
26224
- function toSessionNote(row) {
26225
- return {
26226
- id: row.id,
26227
- sessionId: row.session_id,
26228
- content: row.content,
26229
- createdAt: row.created_at
26230
- };
26231
- }
26232
- function getSessionNotes(db, sessionId) {
26233
- const rows = db.prepare("SELECT * FROM session_notes WHERE session_id = ? ORDER BY id ASC").all(sessionId).filter(isSessionNoteRow);
26234
- return rows.map(toSessionNote);
26235
- }
26236
- function addSessionNote(db, sessionId, content) {
26237
- db.prepare("INSERT INTO session_notes (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, Date.now());
26238
- }
26239
- function clearSessionNotes(db, sessionId) {
26240
- db.prepare("DELETE FROM session_notes WHERE session_id = ?").run(sessionId);
26241
- }
26242
26800
  // src/features/magic-context/storage-ops.ts
26243
26801
  init_logger();
26244
26802
  var queuePendingOpStatements = new WeakMap;
@@ -26455,7 +27013,14 @@ function getTopNBySize(db, sessionId, n) {
26455
27013
  init_logger();
26456
27014
  var DREAM_TIMER_INTERVAL_MS = 15 * 60 * 1000;
26457
27015
  function startDreamScheduleTimer(args) {
26458
- const { 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
  }
@@ -30517,11 +31343,9 @@ function applyHeuristicCleanup(sessionId, db, targets, messageTagNumbers, config
30517
31343
  return { droppedTools, deduplicatedTools, droppedInjections };
30518
31344
  }
30519
31345
  function extractToolInfo(part) {
30520
- if (part.type === "tool" && typeof part.state === "object" && part.state !== null) {
30521
- const state = part.state;
30522
- if (typeof state.tool === "string" && DEDUP_SAFE_TOOLS.has(state.tool)) {
30523
- return { toolName: state.tool, args: state.input ?? {} };
30524
- }
31346
+ if (part.type === "tool" && typeof part.tool === "string" && DEDUP_SAFE_TOOLS.has(part.tool)) {
31347
+ const state = typeof part.state === "object" && part.state !== null ? part.state : {};
31348
+ return { toolName: part.tool, args: state.input ?? {} };
30525
31349
  }
30526
31350
  if (part.type === "tool-invocation" && typeof part.toolName === "string" && DEDUP_SAFE_TOOLS.has(part.toolName)) {
30527
31351
  return { toolName: part.toolName, args: part.args ?? {} };
@@ -30744,7 +31568,18 @@ function runPostTransformPhase(args) {
30744
31568
  if (pendingUserTurnReminder.messageId) {
30745
31569
  const reinjected = appendReminderToUserMessageById(args.messages, pendingUserTurnReminder.messageId, pendingUserTurnReminder.text);
30746
31570
  if (!reinjected) {
30747
- sessionLog(args.sessionId, `preserving sticky turn reminder anchor to avoid cache bust: messageId=${pendingUserTurnReminder.messageId}`);
31571
+ if (isCacheBustingPass) {
31572
+ const newAnchorId = appendReminderToLatestUserMessage(args.messages, pendingUserTurnReminder.text);
31573
+ if (newAnchorId) {
31574
+ setPersistedStickyTurnReminder(args.db, args.sessionId, pendingUserTurnReminder.text, newAnchorId);
31575
+ sessionLog(args.sessionId, `sticky turn reminder re-anchored: ${pendingUserTurnReminder.messageId} \u2192 ${newAnchorId}`);
31576
+ } else {
31577
+ clearPersistedStickyTurnReminder(args.db, args.sessionId);
31578
+ sessionLog(args.sessionId, `sticky turn reminder cleared \u2014 anchor ${pendingUserTurnReminder.messageId} gone and no user message visible`);
31579
+ }
31580
+ } else {
31581
+ sessionLog(args.sessionId, `preserving sticky turn reminder anchor to avoid cache bust: messageId=${pendingUserTurnReminder.messageId}`);
31582
+ }
30748
31583
  }
30749
31584
  } else {
30750
31585
  const anchoredMessageId = appendReminderToLatestUserMessage(args.messages, pendingUserTurnReminder.text);
@@ -30781,7 +31616,17 @@ function runPostTransformPhase(args) {
30781
31616
  if (stickyNoteNudge) {
30782
31617
  const reinjected = appendReminderToUserMessageById(args.messages, stickyNoteNudge.messageId, stickyNoteNudge.text);
30783
31618
  if (!reinjected) {
30784
- sessionLog(args.sessionId, `preserving sticky note nudge anchor to avoid cache bust: messageId=${stickyNoteNudge.messageId}`);
31619
+ if (isCacheBustingPass) {
31620
+ const newAnchorId = appendReminderToLatestUserMessage(args.messages, stickyNoteNudge.text);
31621
+ if (newAnchorId) {
31622
+ markNoteNudgeDelivered(args.db, args.sessionId, stickyNoteNudge.text, newAnchorId);
31623
+ sessionLog(args.sessionId, `sticky note nudge re-anchored: ${stickyNoteNudge.messageId} \u2192 ${newAnchorId}`);
31624
+ } else {
31625
+ sessionLog(args.sessionId, `sticky note nudge anchor ${stickyNoteNudge.messageId} gone \u2014 no user message visible to re-anchor`);
31626
+ }
31627
+ } else {
31628
+ sessionLog(args.sessionId, `preserving sticky note nudge anchor to avoid cache bust: messageId=${stickyNoteNudge.messageId}`);
31629
+ }
30785
31630
  }
30786
31631
  }
30787
31632
  const deferredNoteText = peekNoteNudgeText(args.db, args.sessionId, args.currentTurnId, args.projectPath);
@@ -30856,7 +31701,7 @@ function createTransform(deps) {
30856
31701
  const canRunCompartments = fullFeatureMode && deps.client !== undefined && compartmentDirectory.length > 0;
30857
31702
  const contextUsageEarly = loadContextUsage(deps.contextUsageMap, db, sessionId);
30858
31703
  const schedulerDecisionEarly = resolveSchedulerDecision(deps.scheduler, sessionMeta, contextUsageEarly, sessionId, deps.getModelKey?.(sessionId));
30859
- const isCacheBusting = deps.flushedSessions.has(sessionId) || schedulerDecisionEarly === "execute";
31704
+ const isCacheBusting = deps.flushedSessions.has(sessionId);
30860
31705
  let pendingCompartmentInjection = null;
30861
31706
  if (fullFeatureMode) {
30862
31707
  const projectPath = deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined;
@@ -30947,7 +31792,9 @@ function createTransform(deps) {
30947
31792
  projectPath: deps.memoryConfig?.enabled ? resolveProjectIdentity(deps.directory ?? process.cwd()) : undefined,
30948
31793
  injectionBudgetTokens: deps.memoryConfig?.injectionBudgetTokens,
30949
31794
  getNotificationParams: rawGetNotifParams ? () => rawGetNotifParams(sessionId) : undefined,
30950
- cacheAlreadyBusting: isCacheBusting
31795
+ cacheAlreadyBusting: isCacheBusting || schedulerDecisionEarly === "execute",
31796
+ experimentalCompactionMarkers: deps.experimentalCompactionMarkers,
31797
+ experimentalUserMemories: deps.experimentalUserMemories
30951
31798
  });
30952
31799
  pendingCompartmentInjection = compartmentPhase.pendingCompartmentInjection;
30953
31800
  const awaitedCompartmentRun = compartmentPhase.awaitedCompartmentRun;
@@ -31137,7 +31984,7 @@ function createToolExecuteAfterHook(args) {
31137
31984
 
31138
31985
  // src/hooks/magic-context/system-prompt-hash.ts
31139
31986
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
31140
- import { join as join11 } from "path";
31987
+ import { join as join12 } from "path";
31141
31988
 
31142
31989
  // src/agents/magic-context-prompt.ts
31143
31990
  var BASE_INTRO = (protectedTags) => `Messages and tool outputs are tagged with \xA7N\xA7 identifiers (e.g., \xA71\xA7, \xA742\xA7).
@@ -31338,11 +32185,13 @@ Prefer many small targeted operations over one large blanket operation. Compress
31338
32185
  init_logger();
31339
32186
  var MAGIC_CONTEXT_MARKER = "## Magic Context";
31340
32187
  var PROJECT_DOCS_MARKER = "<project-docs>";
32188
+ var USER_PROFILE_MARKER = "<user-profile>";
32189
+ var cachedUserProfileBySession = new Map;
31341
32190
  var DOC_FILES = ["ARCHITECTURE.md", "STRUCTURE.md"];
31342
32191
  function readProjectDocs(directory) {
31343
32192
  const sections = [];
31344
32193
  for (const filename of DOC_FILES) {
31345
- const filePath = join11(directory, filename);
32194
+ const filePath = join12(directory, filename);
31346
32195
  try {
31347
32196
  if (existsSync5(filePath)) {
31348
32197
  const content = readFileSync4(filePath, "utf-8").trim();
@@ -31397,6 +32246,28 @@ function createSystemPromptHashHandler(deps) {
31397
32246
  output.system.push(docsBlock);
31398
32247
  }
31399
32248
  }
32249
+ if (deps.experimentalUserMemories) {
32250
+ const hasCachedProfile = cachedUserProfileBySession.has(sessionId);
32251
+ if (!hasCachedProfile || isCacheBusting) {
32252
+ const memories = getActiveUserMemories(deps.db);
32253
+ if (memories.length > 0) {
32254
+ const items = memories.map((m) => `- ${m.content}`).join(`
32255
+ `);
32256
+ cachedUserProfileBySession.set(sessionId, `${USER_PROFILE_MARKER}
32257
+ ${items}
32258
+ </user-profile>`);
32259
+ if (!hasCachedProfile) {
32260
+ sessionLog(sessionId, `loaded ${memories.length} user profile memorie(s)`);
32261
+ }
32262
+ } else {
32263
+ cachedUserProfileBySession.set(sessionId, null);
32264
+ }
32265
+ }
32266
+ const profileBlock = cachedUserProfileBySession.get(sessionId);
32267
+ if (profileBlock && !fullPrompt.includes(USER_PROFILE_MARKER)) {
32268
+ output.system.push(profileBlock);
32269
+ }
32270
+ }
31400
32271
  const DATE_PATTERN = /Today's date: .+/;
31401
32272
  for (let i = 0;i < output.system.length; i++) {
31402
32273
  const match = output.system[i].match(DATE_PATTERN);
@@ -31530,7 +32401,9 @@ function createMagicContextHook(deps) {
31530
32401
  const model = liveModelBySession.get(sessionId);
31531
32402
  return resolveModelKey(model?.providerID, model?.modelID);
31532
32403
  },
31533
- projectPath
32404
+ projectPath,
32405
+ experimentalCompactionMarkers: deps.config.experimental?.compaction_markers,
32406
+ experimentalUserMemories: deps.config.experimental?.user_memories?.enabled
31534
32407
  });
31535
32408
  const eventHandler = createEventHandler2({
31536
32409
  contextUsageMap,
@@ -31565,7 +32438,11 @@ function createMagicContextHook(deps) {
31565
32438
  client: deps.client,
31566
32439
  tasks: dreaming.tasks,
31567
32440
  taskTimeoutMinutes: dreaming.task_timeout_minutes,
31568
- maxRuntimeMinutes: dreaming.max_runtime_minutes
32441
+ maxRuntimeMinutes: dreaming.max_runtime_minutes,
32442
+ experimentalUserMemories: deps.config.experimental?.user_memories?.enabled ? {
32443
+ enabled: true,
32444
+ promotionThreshold: deps.config.experimental.user_memories?.promotion_threshold
32445
+ } : undefined
31569
32446
  }).catch((error48) => {
31570
32447
  log("[dreamer] scheduled queue processing failed:", error48);
31571
32448
  });
@@ -31607,7 +32484,11 @@ function createMagicContextHook(deps) {
31607
32484
  config: deps.config.dreamer,
31608
32485
  projectPath,
31609
32486
  client: deps.client,
31610
- directory: deps.directory
32487
+ directory: deps.directory,
32488
+ experimentalUserMemories: deps.config.experimental?.user_memories?.enabled ? {
32489
+ enabled: true,
32490
+ promotionThreshold: deps.config.experimental.user_memories?.promotion_threshold
32491
+ } : undefined
31611
32492
  } : undefined
31612
32493
  });
31613
32494
  const emergencyNudgeFired = new Set;
@@ -31619,7 +32500,8 @@ function createMagicContextHook(deps) {
31619
32500
  injectDocs: deps.config.dreamer?.inject_docs !== false,
31620
32501
  directory: deps.directory,
31621
32502
  flushedSessions,
31622
- lastHeuristicsTurnId
32503
+ lastHeuristicsTurnId,
32504
+ experimentalUserMemories: deps.config.experimental?.user_memories?.enabled
31623
32505
  });
31624
32506
  const eventHook = createEventHook({
31625
32507
  eventHandler,
@@ -31696,7 +32578,8 @@ function createSessionHooks(args) {
31696
32578
  historian_timeout_ms: pluginConfig.historian_timeout_ms,
31697
32579
  memory: pluginConfig.memory,
31698
32580
  sidekick: pluginConfig.sidekick,
31699
- dreamer: pluginConfig.dreamer
32581
+ dreamer: pluginConfig.dreamer,
32582
+ experimental: pluginConfig.experimental
31700
32583
  }
31701
32584
  })
31702
32585
  };
@@ -32079,9 +32962,9 @@ Use this for short goals, constraints, decisions, or reminders worth carrying fo
32079
32962
 
32080
32963
  Actions:
32081
32964
  - \`write\`: Append one note. Optionally provide \`surface_condition\` to create a smart note.
32082
- - \`read\`: Show current notes (session notes + ready smart notes).
32083
- - \`clear\`: Remove all session notes.
32084
- - \`dismiss\`: Dismiss a ready smart note by \`note_id\`.
32965
+ - \`read\`: Show current notes. Defaults to active session notes + ready smart notes; use \`filter\` to inspect all, pending, ready, active, or dismissed notes.
32966
+ - \`dismiss\`: Dismiss a note by \`note_id\`.
32967
+ - \`update\`: Update a note by \`note_id\`.
32085
32968
 
32086
32969
  **Smart Notes**: When \`surface_condition\` is provided with \`write\`, the note becomes a project-scoped smart note.
32087
32970
  The dreamer evaluates smart note conditions during nightly runs and surfaces them when conditions are met.
@@ -32090,14 +32973,79 @@ Example: \`ctx_note(action="write", content="Implement X because Y", surface_con
32090
32973
  Historian reads these notes, deduplicates them, and rewrites the remaining useful notes over time.`;
32091
32974
  // src/tools/ctx-note/tools.ts
32092
32975
  import { tool as tool3 } from "@opencode-ai/plugin";
32976
+ function formatNoteLine(note) {
32977
+ const statusSuffix = note.status === "active" ? "" : ` (${note.status})`;
32978
+ const dismissHint = note.status === "dismissed" ? "" : ` _(dismiss with \`ctx_note(action="dismiss", note_id=${note.id})\`)_`;
32979
+ if (note.type === "session") {
32980
+ return `- **#${note.id}**${statusSuffix}: ${note.content}${dismissHint}`;
32981
+ }
32982
+ const conditionText = note.status === "ready" ? note.readyReason ?? note.surfaceCondition ?? "Condition satisfied" : note.surfaceCondition ?? "No condition recorded";
32983
+ const conditionLabel = note.status === "ready" ? "Condition met" : "Condition";
32984
+ return `- **#${note.id}**${statusSuffix}: ${note.content}
32985
+ ${conditionLabel}: ${conditionText}${dismissHint}`;
32986
+ }
32987
+ function buildReadSections(args) {
32988
+ if (args.filter === undefined) {
32989
+ const sessionNotes2 = getSessionNotes(args.db, args.sessionId);
32990
+ const readySmartNotes = args.projectIdentity ? getReadySmartNotes(args.db, args.projectIdentity) : [];
32991
+ const sections2 = [];
32992
+ if (sessionNotes2.length > 0) {
32993
+ sections2.push(`## Session Notes
32994
+
32995
+ ${sessionNotes2.map((note) => formatNoteLine(note)).join(`
32996
+ `)}`);
32997
+ }
32998
+ if (readySmartNotes.length > 0) {
32999
+ sections2.push(`## \uD83D\uDD14 Ready Smart Notes
33000
+
33001
+ ${readySmartNotes.map((note) => formatNoteLine(note)).join(`
33002
+
33003
+ `)}`);
33004
+ }
33005
+ return sections2;
33006
+ }
33007
+ const statusByFilter = {
33008
+ active: "active",
33009
+ all: ["active", "pending", "ready", "dismissed"],
33010
+ dismissed: "dismissed",
33011
+ pending: "pending",
33012
+ ready: "ready"
33013
+ };
33014
+ const sessionNotes = getNotes(args.db, {
33015
+ sessionId: args.sessionId,
33016
+ type: "session",
33017
+ status: statusByFilter[args.filter]
33018
+ });
33019
+ const smartNotes = args.projectIdentity ? getNotes(args.db, {
33020
+ projectPath: args.projectIdentity,
33021
+ type: "smart",
33022
+ status: statusByFilter[args.filter]
33023
+ }) : [];
33024
+ const sections = [];
33025
+ if (sessionNotes.length > 0) {
33026
+ sections.push(`## Session Notes
33027
+
33028
+ ${sessionNotes.map((note) => formatNoteLine(note)).join(`
33029
+ `)}`);
33030
+ }
33031
+ if (smartNotes.length > 0) {
33032
+ sections.push(`## Smart Notes
33033
+
33034
+ ${smartNotes.map((note) => formatNoteLine(note)).join(`
33035
+
33036
+ `)}`);
33037
+ }
33038
+ return sections;
33039
+ }
32093
33040
  function createCtxNoteTool(deps) {
32094
33041
  return tool3({
32095
33042
  description: CTX_NOTE_DESCRIPTION,
32096
33043
  args: {
32097
- action: tool3.schema.enum(["write", "read", "clear", "dismiss"]).optional().describe("Operation to perform. Defaults to 'write' when content is provided, otherwise 'read'."),
33044
+ action: tool3.schema.enum(["write", "read", "dismiss", "update"]).optional().describe("Operation to perform. Defaults to 'write' when content is provided, otherwise 'read'."),
32098
33045
  content: tool3.schema.string().optional().describe("Note text to store when action is 'write'."),
32099
33046
  surface_condition: tool3.schema.string().optional().describe("Open-ended condition for smart notes. When provided, creates a project-scoped smart note that the dreamer evaluates nightly. The note surfaces when the condition is met."),
32100
- note_id: tool3.schema.number().optional().describe("Smart note ID to dismiss (required for 'dismiss' action).")
33047
+ filter: tool3.schema.enum(["all", "active", "pending", "ready", "dismissed"]).optional().describe("Optional read filter. Defaults to active session notes + ready smart notes. Use 'all' to inspect every status or 'pending' to inspect unsurfaced smart notes."),
33048
+ note_id: tool3.schema.number().optional().describe("Note ID (required for 'dismiss' and 'update' actions).")
32101
33049
  },
32102
33050
  async execute(args, toolContext) {
32103
33051
  const sessionId = toolContext.sessionID;
@@ -32114,48 +33062,59 @@ function createCtxNoteTool(deps) {
32114
33062
  if (!deps.projectIdentity) {
32115
33063
  return "Error: Could not resolve project identity for smart note.";
32116
33064
  }
32117
- const 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:
33065
+ const note2 = addNote(deps.db, "smart", {
33066
+ content,
33067
+ projectPath: deps.projectIdentity,
33068
+ sessionId,
33069
+ surfaceCondition: args.surface_condition.trim()
33070
+ });
33071
+ return `Created smart note #${note2.id}. Dreamer will evaluate the condition during nightly runs:
32119
33072
  - Content: ${content}
32120
33073
  - Condition: ${args.surface_condition.trim()}`;
32121
33074
  }
32122
- 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.`;
33075
+ const note = addNote(deps.db, "session", { sessionId, content });
33076
+ return `Saved session note #${note.id}. Historian will rewrite or deduplicate notes as needed.`;
32125
33077
  }
32126
33078
  if (action === "dismiss") {
32127
33079
  const noteId = args.note_id;
32128
33080
  if (typeof noteId !== "number") {
32129
33081
  return "Error: 'note_id' is required when action is 'dismiss'.";
32130
33082
  }
32131
- const dismissed = 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
- `)}`);
33083
+ const dismissed = dismissNote(deps.db, noteId);
33084
+ return dismissed ? `Note #${noteId} dismissed.` : `Note #${noteId} not found or already dismissed.`;
32158
33085
  }
33086
+ if (action === "update") {
33087
+ const noteId = args.note_id;
33088
+ if (typeof noteId !== "number") {
33089
+ return "Error: 'note_id' is required when action is 'update'.";
33090
+ }
33091
+ const updates = {};
33092
+ if (args.content?.trim())
33093
+ updates.content = args.content.trim();
33094
+ if (args.surface_condition?.trim())
33095
+ updates.surfaceCondition = args.surface_condition.trim();
33096
+ if (!updates.content && !updates.surfaceCondition) {
33097
+ return "Error: Provide 'content' and/or 'surface_condition' to update.";
33098
+ }
33099
+ const updated = updateNote(deps.db, noteId, updates);
33100
+ if (!updated) {
33101
+ return `Note #${noteId} not found or has no compatible fields to update.`;
33102
+ }
33103
+ const parts = [];
33104
+ if (updates.content)
33105
+ parts.push(`Content: ${updates.content}`);
33106
+ if (updates.surfaceCondition)
33107
+ parts.push(`Condition: ${updates.surfaceCondition}`);
33108
+ return `Updated note #${noteId}:
33109
+ ${parts.join(`
33110
+ `)}`;
33111
+ }
33112
+ const sections = buildReadSections({
33113
+ db: deps.db,
33114
+ filter: args.filter,
33115
+ projectIdentity: deps.projectIdentity,
33116
+ sessionId
33117
+ });
32159
33118
  if (sections.length === 0) {
32160
33119
  return `## Notes
32161
33120
 
@@ -32789,6 +33748,118 @@ function createToolRegistry(args) {
32789
33748
  return allTools;
32790
33749
  }
32791
33750
 
33751
+ // src/features/magic-context/plugin-messages.ts
33752
+ function isPluginMessageRow(row) {
33753
+ if (row === null || typeof row !== "object")
33754
+ return false;
33755
+ const r = row;
33756
+ return typeof r.id === "number" && typeof r.direction === "string" && typeof r.type === "string" && typeof r.payload === "string" && typeof r.created_at === "number";
33757
+ }
33758
+ function toPluginMessage(row) {
33759
+ let payload = {};
33760
+ try {
33761
+ payload = JSON.parse(row.payload);
33762
+ } catch {}
33763
+ return {
33764
+ id: row.id,
33765
+ direction: row.direction,
33766
+ type: row.type,
33767
+ payload,
33768
+ sessionId: row.session_id,
33769
+ createdAt: row.created_at,
33770
+ consumedAt: row.consumed_at
33771
+ };
33772
+ }
33773
+ var CLEANUP_THRESHOLD_MS = 5 * 60 * 1000;
33774
+ function sendToTui(db, type, payload, sessionId) {
33775
+ const result = db.prepare("INSERT INTO plugin_messages (direction, type, payload, session_id, created_at) VALUES (?, ?, ?, ?, ?)").run("server_to_tui", type, JSON.stringify(payload), sessionId ?? null, Date.now());
33776
+ return Number(result.lastInsertRowid);
33777
+ }
33778
+ function consumeMessages(db, direction, options) {
33779
+ const now = Date.now();
33780
+ const conditions = ["direction = ?", "consumed_at IS NULL"];
33781
+ const params = [direction];
33782
+ if (options?.type) {
33783
+ conditions.push("type = ?");
33784
+ params.push(options.type);
33785
+ }
33786
+ if (options?.sessionId) {
33787
+ conditions.push("session_id = ?");
33788
+ params.push(options.sessionId);
33789
+ }
33790
+ const query = `SELECT * FROM plugin_messages WHERE ${conditions.join(" AND ")} ORDER BY created_at ASC`;
33791
+ const messages = db.transaction(() => {
33792
+ const rows = db.prepare(query).all(...params);
33793
+ const result = rows.filter(isPluginMessageRow).map(toPluginMessage);
33794
+ if (result.length > 0) {
33795
+ const ids = result.map((m) => m.id);
33796
+ db.prepare(`UPDATE plugin_messages SET consumed_at = ? WHERE id IN (${ids.map(() => "?").join(",")})`).run(now, ...ids);
33797
+ }
33798
+ return result;
33799
+ })();
33800
+ db.prepare("DELETE FROM plugin_messages WHERE created_at < ?").run(now - CLEANUP_THRESHOLD_MS);
33801
+ return messages;
33802
+ }
33803
+ function sendTuiToast(db, message, options) {
33804
+ return sendToTui(db, "toast", {
33805
+ message,
33806
+ variant: options?.variant ?? "info",
33807
+ duration: options?.duration ?? 5000
33808
+ }, options?.sessionId);
33809
+ }
33810
+
33811
+ // src/plugin/tui-action-consumer.ts
33812
+ init_logger();
33813
+ var DEFAULT_COMPARTMENT_TOKEN_BUDGET2 = 20000;
33814
+ var DEFAULT_HISTORIAN_TIMEOUT_MS2 = 10 * 60 * 1000;
33815
+ var TUI_ACTION_POLL_INTERVAL_MS = 2000;
33816
+ function startTuiActionConsumer(args) {
33817
+ const { client, directory, config: config2 } = args;
33818
+ const timer = setInterval(() => {
33819
+ try {
33820
+ const db = openDatabase();
33821
+ const actions = consumeMessages(db, "tui_to_server", { type: "action" });
33822
+ for (const msg of actions) {
33823
+ const command = msg.payload.command;
33824
+ const sessionId = msg.sessionId;
33825
+ if (command === "recomp" && sessionId) {
33826
+ log(`[magic-context] TUI action: recomp requested for session ${sessionId}`);
33827
+ sendTuiToast(db, "Historian recomp started", {
33828
+ variant: "info",
33829
+ sessionId
33830
+ });
33831
+ executeContextRecomp({
33832
+ client,
33833
+ db,
33834
+ sessionId,
33835
+ tokenBudget: config2.compartment_token_budget ?? DEFAULT_COMPARTMENT_TOKEN_BUDGET2,
33836
+ historianTimeoutMs: config2.historian_timeout_ms ?? DEFAULT_HISTORIAN_TIMEOUT_MS2,
33837
+ directory,
33838
+ getNotificationParams: () => ({})
33839
+ }).then((result) => {
33840
+ sendTuiToast(db, "Recomp completed", { variant: "success", sessionId });
33841
+ sendIgnoredMessage(client, sessionId, result, {}).catch(() => {});
33842
+ }).catch((error48) => {
33843
+ log("[magic-context] TUI recomp failed:", error48);
33844
+ sendTuiToast(db, `Recomp failed: ${error48 instanceof Error ? error48.message : "unknown error"}`, { variant: "error", sessionId });
33845
+ });
33846
+ } else {
33847
+ log(`[magic-context] TUI action: unknown command=${String(command)} session=${String(sessionId)}`);
33848
+ }
33849
+ }
33850
+ } catch (error48) {
33851
+ log("[magic-context] TUI action consumer error:", error48);
33852
+ }
33853
+ }, TUI_ACTION_POLL_INTERVAL_MS);
33854
+ if (typeof timer === "object" && "unref" in timer) {
33855
+ timer.unref();
33856
+ }
33857
+ log("[magic-context] started TUI action consumer (2s poll)");
33858
+ return () => {
33859
+ clearInterval(timer);
33860
+ };
33861
+ }
33862
+
32792
33863
  // src/index.ts
32793
33864
  init_conflict_detector();
32794
33865
  init_logger();
@@ -32818,7 +33889,16 @@ var plugin = async (ctx) => {
32818
33889
  client: ctx.client,
32819
33890
  dreamerConfig: pluginConfig.dreamer,
32820
33891
  embeddingConfig: pluginConfig.embedding,
32821
- memoryEnabled: pluginConfig.memory?.enabled === true
33892
+ memoryEnabled: pluginConfig.memory?.enabled === true,
33893
+ experimentalUserMemories: pluginConfig.experimental?.user_memories?.enabled ? {
33894
+ enabled: true,
33895
+ promotionThreshold: pluginConfig.experimental.user_memories?.promotion_threshold
33896
+ } : undefined
33897
+ });
33898
+ startTuiActionConsumer({
33899
+ client: ctx.client,
33900
+ directory: ctx.directory,
33901
+ config: pluginConfig
32822
33902
  });
32823
33903
  }
32824
33904
  if (conflictResult?.hasConflict) {
@@ -32898,7 +33978,7 @@ var plugin = async (ctx) => {
32898
33978
  config2.agent = {
32899
33979
  ...config2.agent ?? {},
32900
33980
  [DREAMER_AGENT]: buildHiddenAgentConfig(DREAMER_AGENT, DREAMER_SYSTEM_PROMPT, dreamerAgentOverrides),
32901
- [HISTORIAN_AGENT]: buildHiddenAgentConfig(HISTORIAN_AGENT, COMPARTMENT_AGENT_SYSTEM_PROMPT, pluginConfig.historian),
33981
+ [HISTORIAN_AGENT]: buildHiddenAgentConfig(HISTORIAN_AGENT, pluginConfig.experimental?.user_memories?.enabled ? COMPARTMENT_AGENT_SYSTEM_PROMPT + USER_OBSERVATIONS_APPENDIX : COMPARTMENT_AGENT_SYSTEM_PROMPT, pluginConfig.historian),
32902
33982
  [SIDEKICK_AGENT]: buildHiddenAgentConfig(SIDEKICK_AGENT, SIDEKICK_SYSTEM_PROMPT, sidekickAgentOverrides)
32903
33983
  };
32904
33984
  }