@wolfx/opencode-magic-context 0.24.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  2. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +8 -0
  3. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  4. package/dist/features/magic-context/memory/embedding-local.d.ts +4 -0
  5. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  6. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts +6 -0
  7. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  8. package/dist/features/magic-context/project-embedding-registry.d.ts +38 -0
  9. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  10. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  11. package/dist/features/magic-context/storage-meta-session.d.ts +1 -0
  12. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  13. package/dist/features/magic-context/storage-meta-shared.d.ts +2 -1
  14. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  15. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  16. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  17. package/dist/features/magic-context/storage-tags.d.ts +10 -0
  18. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  19. package/dist/features/magic-context/storage.d.ts +2 -2
  20. package/dist/features/magic-context/storage.d.ts.map +1 -1
  21. package/dist/features/magic-context/types.d.ts +1 -0
  22. package/dist/features/magic-context/types.d.ts.map +1 -1
  23. package/dist/hooks/magic-context/apply-operations.d.ts +3 -25
  24. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  25. package/dist/hooks/magic-context/caveman-cleanup.d.ts +1 -0
  26. package/dist/hooks/magic-context/caveman-cleanup.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/channel2-delivery.d.ts +2 -0
  28. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  29. package/dist/hooks/magic-context/command-handler.d.ts +7 -5
  30. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +7 -2
  32. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/embed-session-state.d.ts +14 -0
  34. package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -0
  35. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/format-embed-status.d.ts +9 -0
  37. package/dist/hooks/magic-context/format-embed-status.d.ts.map +1 -0
  38. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -0
  39. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  40. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  41. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  42. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +1 -1
  44. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/strip-content.d.ts +0 -1
  46. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/tag-content-primitives.d.ts +2 -0
  48. package/dist/hooks/magic-context/tag-content-primitives.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/tool-drop-target.d.ts +1 -1
  51. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  52. package/dist/hooks/magic-context/tool-reclaim.d.ts +12 -0
  53. package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -0
  54. package/dist/hooks/magic-context/transform-operations.d.ts +1 -1
  55. package/dist/hooks/magic-context/transform-operations.d.ts.map +1 -1
  56. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/transform.d.ts +2 -0
  58. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +1103 -408
  61. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  62. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  63. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  64. package/dist/shared/announcement.d.ts +1 -1
  65. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  66. package/dist/shared/rpc-types.d.ts +20 -0
  67. package/dist/shared/rpc-types.d.ts.map +1 -1
  68. package/dist/shared/sqlite.d.ts +5 -1
  69. package/dist/shared/sqlite.d.ts.map +1 -1
  70. package/dist/tools/ctx-expand/constants.d.ts +1 -1
  71. package/dist/tools/ctx-expand/constants.d.ts.map +1 -1
  72. package/dist/tools/ctx-expand/render.d.ts +43 -0
  73. package/dist/tools/ctx-expand/render.d.ts.map +1 -0
  74. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  75. package/dist/tools/ctx-expand/types.d.ts +6 -2
  76. package/dist/tools/ctx-expand/types.d.ts.map +1 -1
  77. package/dist/tools/ctx-reduce/constants.d.ts +1 -1
  78. package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
  79. package/dist/tui/data/context-db.d.ts +4 -2
  80. package/dist/tui/data/context-db.d.ts.map +1 -1
  81. package/package.json +1 -1
  82. package/src/shared/announcement.ts +6 -6
  83. package/src/shared/model-suggestion-retry.test.ts +61 -1
  84. package/src/shared/model-suggestion-retry.ts +22 -0
  85. package/src/shared/rpc-types.ts +11 -0
  86. package/src/shared/sqlite-bind-style.test.ts +82 -0
  87. package/src/shared/sqlite.ts +30 -1
  88. package/src/shared/tag-transcript.test.ts +3 -1
  89. package/src/shared/tag-transcript.ts +19 -17
  90. package/src/tui/data/context-db.ts +34 -2
  91. package/src/tui/index.tsx +58 -4
  92. package/src/tui/slots/sidebar-content.tsx +7 -2
package/dist/index.js CHANGED
@@ -15565,9 +15565,11 @@ async function promptWithTimeout(client, args, timeoutMs, signal) {
15565
15565
  });
15566
15566
  } catch (error51) {
15567
15567
  if (signal?.aborted) {
15568
+ await abortChildRun(client, args.path.id);
15568
15569
  throw new Error("prompt aborted by external signal");
15569
15570
  }
15570
15571
  if (controller.signal.aborted) {
15572
+ await abortChildRun(client, args.path.id);
15571
15573
  throw new Error(`prompt timed out after ${timeoutMs}ms`);
15572
15574
  }
15573
15575
  throw error51;
@@ -15576,6 +15578,13 @@ async function promptWithTimeout(client, args, timeoutMs, signal) {
15576
15578
  signal?.removeEventListener("abort", onExternalAbort);
15577
15579
  }
15578
15580
  }
15581
+ async function abortChildRun(client, sessionId) {
15582
+ try {
15583
+ await client.session.abort({ path: { id: sessionId } });
15584
+ } catch (error51) {
15585
+ log(`[model-retry] child session abort failed for ${sessionId}: ${String(error51)}`);
15586
+ }
15587
+ }
15579
15588
  function isNonRetryable(error51, externalSignal) {
15580
15589
  if (externalSignal?.aborted)
15581
15590
  return true;
@@ -15847,7 +15856,7 @@ function isSessionMetaRow(row) {
15847
15856
  if (row === null || typeof row !== "object")
15848
15857
  return false;
15849
15858
  const r = row;
15850
- return typeof r.session_id === "string" && typeof r.last_response_time === "number" && isStringOrNull(r.cache_ttl) && typeof r.counter === "number" && typeof r.last_nudge_tokens === "number" && isStringOrNull(r.last_nudge_band) && isStringOrNull(r.last_transform_error) && typeof r.is_subagent === "number" && typeof r.last_context_percentage === "number" && typeof r.last_input_tokens === "number" && isNumberOrNull(r.observed_safe_input_tokens) && isNumberOrNull(r.cache_alert_sent) && isNumberOrNull(r.times_execute_threshold_reached) && isNumberOrNull(r.compartment_in_progress) && (r.system_prompt_hash === null || typeof r.system_prompt_hash === "string" || typeof r.system_prompt_hash === "number") && isNumberOrNull(r.system_prompt_tokens) && isNumberOrNull(r.conversation_tokens) && isNumberOrNull(r.tool_call_tokens) && isNumberOrNull(r.cleared_reasoning_through_tag) && isStringOrNull(r.last_todo_state) && isBlobOrNull(r.cached_m0_bytes) && isBlobOrNull(r.cached_m1_bytes) && isNumberOrNull(r.cached_m0_project_memory_epoch) && isStringOrNull(r.cached_m0_workspace_fingerprint) && isNumberOrNull(r.cached_m0_project_user_profile_version) && isNumberOrNull(r.cached_m0_max_compartment_seq) && isNumberOrNull(r.cached_m0_max_memory_id) && isNumberOrNull(r.cached_m0_max_mutation_id) && isNumberOrNull(r.cached_m0_max_memory_mutation_id) && isStringOrNull(r.cached_m0_project_docs_hash) && isNumberOrNull(r.cached_m0_materialized_at) && isNumberOrNull(r.cached_m0_session_facts_version) && isStringOrNull(r.cached_m0_upgrade_state) && isStringOrNull(r.cached_m0_system_hash) && isStringOrNull(r.cached_m0_tool_set_hash) && isStringOrNull(r.cached_m0_model_key) && isStringOrNull(r.last_observed_model_key) && isNumberOrNull(r.last_usage_context_limit) && isNumberOrNull(r.prior_boundary_ordinal) && isNumberOrNull(r.protected_tail_policy_version) && isNumberOrNull(r.protected_tail_drain_window_started_at) && isNumberOrNull(r.protected_tail_drain_tokens) && isNumberOrNull(r.recovery_no_eligible_head_count) && isNumberOrNull(r.force_emergency_bypass_window_start) && isNumberOrNull(r.force_emergency_bypass_used) && isNumberOrNull(r.upgrade_reminded_at) && isNumberOrNull(r.pi_stable_id_scheme);
15859
+ return typeof r.session_id === "string" && typeof r.last_response_time === "number" && isStringOrNull(r.cache_ttl) && typeof r.counter === "number" && typeof r.last_nudge_tokens === "number" && isStringOrNull(r.last_nudge_band) && isStringOrNull(r.last_transform_error) && typeof r.is_subagent === "number" && typeof r.last_context_percentage === "number" && typeof r.last_input_tokens === "number" && isNumberOrNull(r.observed_safe_input_tokens) && isNumberOrNull(r.cache_alert_sent) && isNumberOrNull(r.times_execute_threshold_reached) && isNumberOrNull(r.compartment_in_progress) && (r.system_prompt_hash === null || typeof r.system_prompt_hash === "string" || typeof r.system_prompt_hash === "number") && isNumberOrNull(r.system_prompt_tokens) && isNumberOrNull(r.conversation_tokens) && isNumberOrNull(r.tool_call_tokens) && isNumberOrNull(r.cleared_reasoning_through_tag) && isStringOrNull(r.last_todo_state) && isBlobOrNull(r.cached_m0_bytes) && isBlobOrNull(r.cached_m1_bytes) && isNumberOrNull(r.cached_m0_project_memory_epoch) && isStringOrNull(r.cached_m0_workspace_fingerprint) && isNumberOrNull(r.cached_m0_project_user_profile_version) && isNumberOrNull(r.cached_m0_max_compartment_seq) && isNumberOrNull(r.cached_m0_max_memory_id) && isNumberOrNull(r.cached_m0_max_mutation_id) && isNumberOrNull(r.cached_m0_max_memory_mutation_id) && isStringOrNull(r.cached_m0_project_docs_hash) && isNumberOrNull(r.cached_m0_materialized_at) && isNumberOrNull(r.cached_m0_session_facts_version) && isStringOrNull(r.cached_m0_upgrade_state) && isStringOrNull(r.cached_m0_system_hash) && isStringOrNull(r.cached_m0_tool_set_hash) && isStringOrNull(r.cached_m0_model_key) && isStringOrNull(r.last_observed_model_key) && isNumberOrNull(r.last_usage_context_limit) && isNumberOrNull(r.prior_boundary_ordinal) && isNumberOrNull(r.protected_tail_policy_version) && isNumberOrNull(r.protected_tail_drain_window_started_at) && isNumberOrNull(r.protected_tail_drain_tokens) && isNumberOrNull(r.recovery_no_eligible_head_count) && isNumberOrNull(r.force_emergency_bypass_window_start) && isNumberOrNull(r.force_emergency_bypass_used) && isNumberOrNull(r.upgrade_reminded_at) && isNumberOrNull(r.pi_stable_id_scheme) && isNumberOrNull(r.tool_reclaim_watermark);
15851
15860
  }
15852
15861
  function getDefaultSessionMeta(sessionId) {
15853
15862
  return {
@@ -15870,6 +15879,7 @@ function getDefaultSessionMeta(sessionId) {
15870
15879
  conversationTokens: 0,
15871
15880
  toolCallTokens: 0,
15872
15881
  clearedReasoningThroughTag: 0,
15882
+ toolReclaimWatermark: 0,
15873
15883
  lastTodoState: "",
15874
15884
  cachedM0Bytes: null,
15875
15885
  cachedM1Bytes: null,
@@ -15933,6 +15943,7 @@ function toSessionMeta(row) {
15933
15943
  conversationTokens: numOrZero(row.conversation_tokens),
15934
15944
  toolCallTokens: numOrZero(row.tool_call_tokens),
15935
15945
  clearedReasoningThroughTag: numOrZero(row.cleared_reasoning_through_tag),
15946
+ toolReclaimWatermark: numOrZero(row.tool_reclaim_watermark),
15936
15947
  lastTodoState: lastTodoStateRaw,
15937
15948
  cachedM0Bytes: toBufferOrNull(row.cached_m0_bytes),
15938
15949
  cachedM1Bytes: toBufferOrNull(row.cached_m1_bytes),
@@ -16042,6 +16053,7 @@ var init_storage_meta_shared = __esm(() => {
16042
16053
  "conversation_tokens",
16043
16054
  "tool_call_tokens",
16044
16055
  "cleared_reasoning_through_tag",
16056
+ "tool_reclaim_watermark",
16045
16057
  "last_todo_state",
16046
16058
  "cached_m0_bytes",
16047
16059
  "cached_m1_bytes",
@@ -16090,6 +16102,7 @@ var init_storage_meta_shared = __esm(() => {
16090
16102
  conversationTokens: "conversation_tokens",
16091
16103
  toolCallTokens: "tool_call_tokens",
16092
16104
  clearedReasoningThroughTag: "cleared_reasoning_through_tag",
16105
+ toolReclaimWatermark: "tool_reclaim_watermark",
16093
16106
  lastTodoState: "last_todo_state",
16094
16107
  cachedM0Bytes: "cached_m0_bytes",
16095
16108
  cachedM1Bytes: "cached_m1_bytes",
@@ -16366,6 +16379,14 @@ function buildNodeSqliteDatabaseClass(DatabaseSync) {
16366
16379
  }
16367
16380
  super(typeof filename === "string" ? filename : ":memory:", translated);
16368
16381
  }
16382
+ prepare(sql) {
16383
+ const stmt = super.prepare(sql);
16384
+ for (const method of ["run", "get", "all"]) {
16385
+ const original = stmt[method].bind(stmt);
16386
+ stmt[method] = (...args) => args.length === 1 && Array.isArray(args[0]) ? original(...args[0]) : original(...args);
16387
+ }
16388
+ return stmt;
16389
+ }
16369
16390
  transaction(fn) {
16370
16391
  const self = this;
16371
16392
  const wrapped = function(...args) {
@@ -149342,6 +149363,9 @@ function stripCompleteTagPairsGlobally(value) {
149342
149363
  function stripMalformedTagNotationGlobally(value) {
149343
149364
  return value.replace(MALFORMED_TAG_GLOBAL_REGEX, "");
149344
149365
  }
149366
+ function stripDanglingTagNotationGlobally(value) {
149367
+ return value.replace(DANGLING_TAG_GLOBAL_REGEX, "");
149368
+ }
149345
149369
  function stripTagSectionCharacters(value) {
149346
149370
  return value.replace(STRAY_SECTION_CHAR_REGEX, "");
149347
149371
  }
@@ -149349,6 +149373,7 @@ function stripPersistedAssistantText(value) {
149349
149373
  let text = stripWellFormedLeadingTagPrefix(value);
149350
149374
  text = stripCompleteTagPairsGlobally(text);
149351
149375
  text = stripMalformedTagNotationGlobally(text);
149376
+ text = stripDanglingTagNotationGlobally(text);
149352
149377
  text = stripTagSectionCharacters(text);
149353
149378
  return text.trim();
149354
149379
  }
@@ -149361,6 +149386,7 @@ function stripTagPrefix(value) {
149361
149386
  const prev = stripped;
149362
149387
  stripped = stripped.replace(MALFORMED_TAG_PREFIX_REGEX, "");
149363
149388
  stripped = stripped.replace(TAG_PREFIX_REGEX, "");
149389
+ stripped = stripped.replace(DANGLING_TAG_PREFIX_REGEX, "");
149364
149390
  if (stripped === prev)
149365
149391
  break;
149366
149392
  }
@@ -149382,11 +149408,13 @@ function isThinkingPart(part) {
149382
149408
  const candidate = part;
149383
149409
  return candidate.type === "thinking" || candidate.type === "reasoning";
149384
149410
  }
149385
- var encoder, TAG_PREFIX_REGEX, MALFORMED_TAG_PREFIX_REGEX, COMPLETE_TAG_PAIR_GLOBAL_REGEX, MALFORMED_TAG_GLOBAL_REGEX, STRAY_SECTION_CHAR_REGEX;
149411
+ var encoder, TAG_PREFIX_REGEX, MALFORMED_TAG_PREFIX_REGEX, DANGLING_TAG_GLOBAL_REGEX, DANGLING_TAG_PREFIX_REGEX, COMPLETE_TAG_PAIR_GLOBAL_REGEX, MALFORMED_TAG_GLOBAL_REGEX, STRAY_SECTION_CHAR_REGEX;
149386
149412
  var init_tag_content_primitives = __esm(() => {
149387
149413
  encoder = new TextEncoder;
149388
149414
  TAG_PREFIX_REGEX = /^(?:§\d+§\s*)+/;
149389
149415
  MALFORMED_TAG_PREFIX_REGEX = /^(?:§\d+">§(?:\d+§)?\s*)+/;
149416
+ DANGLING_TAG_GLOBAL_REGEX = /\u00a7\d+(?!\.\d)[^\s\u00a7\w.]?/g;
149417
+ DANGLING_TAG_PREFIX_REGEX = /^(?:\u00a7\d+(?!\.\d)[^\s\u00a7\w.]?\s*)+/;
149390
149418
  COMPLETE_TAG_PAIR_GLOBAL_REGEX = /\u00a7\d+\u00a7/g;
149391
149419
  MALFORMED_TAG_GLOBAL_REGEX = /\u00a7\d+">(?:\u00a7(?:\d+\u00a7)?)?/g;
149392
149420
  STRAY_SECTION_CHAR_REGEX = /\u00a7/g;
@@ -149450,12 +149478,13 @@ function setToolContent(part, content) {
149450
149478
  part.content = content;
149451
149479
  }
149452
149480
  }
149453
- function truncateToolPart(part) {
149481
+ function truncateToolPart(part, tagId) {
149454
149482
  if (!isRecord(part))
149455
149483
  return;
149484
+ const sentinel = `[dropped §${tagId}§]`;
149456
149485
  if (part.type === "tool" && isRecord(part.state)) {
149457
149486
  const state = part.state;
149458
- state.output = "[truncated]";
149487
+ state.output = sentinel;
149459
149488
  if (isRecord(state.input)) {
149460
149489
  const inputSize = estimateInputSize(state.input);
149461
149490
  if (inputSize > 500) {
@@ -149465,7 +149494,7 @@ function truncateToolPart(part) {
149465
149494
  return;
149466
149495
  }
149467
149496
  if (part.type === "tool_result") {
149468
- part.content = "[truncated]";
149497
+ part.content = sentinel;
149469
149498
  return;
149470
149499
  }
149471
149500
  if (part.type === "tool-invocation" && isRecord(part.args)) {
@@ -149582,7 +149611,7 @@ class ToolMutationBatch {
149582
149611
  this.affectedMessages.clear();
149583
149612
  }
149584
149613
  }
149585
- function createToolDropTarget(compositeKey, thinkingParts, index, batch) {
149614
+ function createToolDropTarget(compositeKey, thinkingParts, index, batch, tagId) {
149586
149615
  const drop = () => {
149587
149616
  const entry = index.get(compositeKey);
149588
149617
  if (!entry || entry.occurrences.length === 0)
@@ -149603,7 +149632,7 @@ function createToolDropTarget(compositeKey, thinkingParts, index, batch) {
149603
149632
  if (!entry.hasResult)
149604
149633
  return "incomplete";
149605
149634
  for (const occurrence of entry.occurrences) {
149606
- truncateToolPart(occurrence.part);
149635
+ truncateToolPart(occurrence.part, tagId);
149607
149636
  }
149608
149637
  clearThinkingParts(thinkingParts);
149609
149638
  return "truncated";
@@ -151337,6 +151366,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
151337
151366
  ensureColumn(db, "session_meta", "historian_last_failure_at", "INTEGER DEFAULT NULL");
151338
151367
  ensureColumn(db, "session_meta", "system_prompt_hash", "TEXT DEFAULT ''");
151339
151368
  ensureColumn(db, "session_meta", "cleared_reasoning_through_tag", "INTEGER DEFAULT 0");
151369
+ ensureColumn(db, "session_meta", "tool_reclaim_watermark", "INTEGER DEFAULT 0");
151340
151370
  ensureColumn(db, "session_meta", "stripped_placeholder_ids", "TEXT DEFAULT ''");
151341
151371
  ensureColumn(db, "session_meta", "stale_reduce_stripped_ids", "TEXT DEFAULT ''");
151342
151372
  ensureColumn(db, "session_meta", "processed_image_stripped_ids", "TEXT DEFAULT ''");
@@ -154126,7 +154156,8 @@ var exports_storage_meta_session = {};
154126
154156
  __export(exports_storage_meta_session, {
154127
154157
  updateSessionMeta: () => updateSessionMeta,
154128
154158
  getOrCreateSessionMeta: () => getOrCreateSessionMeta,
154129
- clearSession: () => clearSession
154159
+ clearSession: () => clearSession,
154160
+ advanceToolReclaimWatermark: () => advanceToolReclaimWatermark
154130
154161
  });
154131
154162
  import { Buffer as Buffer3 } from "node:buffer";
154132
154163
  function getSessionMetaSelectColumns(db) {
@@ -154187,6 +154218,14 @@ function updateSessionMeta(db, sessionId, updates) {
154187
154218
  db.prepare(`UPDATE session_meta SET ${setClauses.join(", ")} WHERE session_id = ?`).run(...values, sessionId);
154188
154219
  })();
154189
154220
  }
154221
+ function advanceToolReclaimWatermark(db, sessionId, maxTagNumber) {
154222
+ if (maxTagNumber <= 0)
154223
+ return;
154224
+ db.transaction(() => {
154225
+ ensureSessionMetaRow(db, sessionId);
154226
+ db.prepare("UPDATE session_meta SET tool_reclaim_watermark = MAX(COALESCE(tool_reclaim_watermark, 0), ?) WHERE session_id = ?").run(maxTagNumber, sessionId);
154227
+ })();
154228
+ }
154190
154229
  function clearSession(db, sessionId) {
154191
154230
  db.transaction(() => {
154192
154231
  db.prepare("DELETE FROM pending_ops WHERE session_id = ?").run(sessionId);
@@ -154225,6 +154264,7 @@ var init_storage_meta_session = __esm(async () => {
154225
154264
  last_transform_error: "'' AS last_transform_error",
154226
154265
  system_prompt_hash: "'' AS system_prompt_hash",
154227
154266
  last_todo_state: "'' AS last_todo_state",
154267
+ tool_reclaim_watermark: "0 AS tool_reclaim_watermark",
154228
154268
  cached_m0_bytes: "NULL AS cached_m0_bytes",
154229
154269
  cached_m1_bytes: "NULL AS cached_m1_bytes",
154230
154270
  cached_m0_project_memory_epoch: "NULL AS cached_m0_project_memory_epoch",
@@ -154632,6 +154672,26 @@ function getActiveTagTokenAggregate(db, sessionId, protectedTags = 0) {
154632
154672
  nullCount: row?.null_count ?? 0
154633
154673
  };
154634
154674
  }
154675
+ function getOldestActiveUnprotectedToolTags(db, sessionId, protectedTags = 0, limit = 4) {
154676
+ if (limit <= 0)
154677
+ return [];
154678
+ const boundedLimit = Math.max(1, Math.min(10, Math.floor(limit)));
154679
+ const whereProtected = protectedTags > 0 ? `AND tag_number < (
154680
+ SELECT tag_number FROM tags
154681
+ WHERE session_id = ? AND status = 'active'
154682
+ ORDER BY tag_number DESC LIMIT 1 OFFSET ?
154683
+ )` : "";
154684
+ const params = protectedTags > 0 ? [sessionId, sessionId, protectedTags - 1, boundedLimit] : [sessionId, boundedLimit];
154685
+ const rows = db.prepare(`SELECT tag_number, tool_name
154686
+ FROM tags
154687
+ WHERE session_id = ? AND status = 'active' AND type = 'tool' ${whereProtected}
154688
+ ORDER BY tag_number ASC, id ASC
154689
+ LIMIT ?`).all(...params);
154690
+ return rows.filter((row) => typeof row.tag_number === "number").map((row) => ({
154691
+ tagNumber: row.tag_number,
154692
+ toolName: typeof row.tool_name === "string" ? row.tool_name : null
154693
+ }));
154694
+ }
154635
154695
  function getTriggerTagTokenUpperBound(db, sessionId) {
154636
154696
  const row = db.prepare(`SELECT
154637
154697
  COALESCE(SUM(COALESCE(token_count, 0) + COALESCE(input_token_count, 0) + COALESCE(reasoning_token_count, 0)), 0) AS bound,
@@ -156218,6 +156278,58 @@ var init_safe_notification_target = __esm(() => {
156218
156278
  DEFAULT_TITLE_RE = /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
156219
156279
  });
156220
156280
 
156281
+ // src/shared/rpc-notifications.ts
156282
+ var exports_rpc_notifications = {};
156283
+ __export(exports_rpc_notifications, {
156284
+ pushNotification: () => pushNotification,
156285
+ isTuiConnected: () => isTuiConnected,
156286
+ drainNotifications: () => drainNotifications
156287
+ });
156288
+ function pushNotification(type, payload, sessionId) {
156289
+ queue.push({ id: nextNotificationId++, type, payload, sessionId });
156290
+ if (queue.length > 100) {
156291
+ const newestPerSession = new Map;
156292
+ for (const n of queue) {
156293
+ const prev = newestPerSession.get(n.sessionId);
156294
+ if (prev === undefined || n.id > prev) {
156295
+ newestPerSession.set(n.sessionId, n.id);
156296
+ }
156297
+ }
156298
+ const mustKeep = new Set(newestPerSession.values());
156299
+ const byNewest = [...queue].sort((a, b) => b.id - a.id);
156300
+ const kept = [];
156301
+ for (const n of byNewest) {
156302
+ if (kept.length < 50 || mustKeep.has(n.id))
156303
+ kept.push(n);
156304
+ }
156305
+ queue = kept.sort((a, b) => a.id - b.id);
156306
+ }
156307
+ }
156308
+ function drainNotifications(lastReceivedId = 0, sessionId) {
156309
+ const now = Date.now();
156310
+ lastDrainAtAny = now;
156311
+ if (sessionId !== undefined)
156312
+ lastDrainAtBySession.set(sessionId, now);
156313
+ const matchesClient = (notification) => sessionId === undefined || notification.sessionId === undefined || notification.sessionId === sessionId;
156314
+ if (lastReceivedId > 0) {
156315
+ queue = queue.filter((notification) => !(notification.id <= lastReceivedId && matchesClient(notification)));
156316
+ }
156317
+ return queue.filter((notification) => notification.id > lastReceivedId && matchesClient(notification));
156318
+ }
156319
+ function isTuiConnected(sessionId) {
156320
+ const now = Date.now();
156321
+ if (sessionId !== undefined) {
156322
+ const at = lastDrainAtBySession.get(sessionId) ?? 0;
156323
+ return at > 0 && now - at < TUI_CONNECTED_WINDOW_MS;
156324
+ }
156325
+ return lastDrainAtAny > 0 && now - lastDrainAtAny < TUI_CONNECTED_WINDOW_MS;
156326
+ }
156327
+ var queue, nextNotificationId = 1, lastDrainAtBySession, lastDrainAtAny = 0, TUI_CONNECTED_WINDOW_MS = 3000;
156328
+ var init_rpc_notifications = __esm(() => {
156329
+ queue = [];
156330
+ lastDrainAtBySession = new Map;
156331
+ });
156332
+
156221
156333
  // src/plugin/conflict-warning-hook.ts
156222
156334
  var exports_conflict_warning_hook = {};
156223
156335
  __export(exports_conflict_warning_hook, {
@@ -156552,6 +156664,9 @@ async function sendStartupAnnouncement(client, directory, version2, features, fo
156552
156664
  if (!sessionId) {
156553
156665
  return;
156554
156666
  }
156667
+ const { isTuiConnected: isTuiConnected2 } = await Promise.resolve().then(() => (init_rpc_notifications(), exports_rpc_notifications));
156668
+ if (isTuiConnected2(sessionId) || isTuiConnected2())
156669
+ return;
156555
156670
  if (await waitForSafeNotificationTarget(client, sessionId) === "skip")
156556
156671
  return;
156557
156672
  const bullets = features.map((line) => ` • ${line}`).join(`
@@ -164531,6 +164646,20 @@ function getDistinctStoredModelIds(db, projectPath) {
164531
164646
  const rows = getDistinctStoredModelIdsStatement(db).all(projectPath);
164532
164647
  return new Set(rows.map((row) => typeof row.modelId === "string" ? row.modelId : null));
164533
164648
  }
164649
+ function getMemoryEmbedCoverage(db, projectPath, modelId) {
164650
+ const row = db.prepare(`SELECT
164651
+ COUNT(*) AS total,
164652
+ SUM(CASE WHEN EXISTS (
164653
+ SELECT 1 FROM memory_embeddings e
164654
+ WHERE e.memory_id = m.id AND e.model_id = ?
164655
+ ) THEN 1 ELSE 0 END) AS embedded
164656
+ FROM memories m
164657
+ WHERE m.project_path = ? AND m.status = 'active'`).get(modelId, projectPath);
164658
+ return {
164659
+ total: typeof row?.total === "number" ? row.total : 0,
164660
+ embedded: typeof row?.embedded === "number" ? row.embedded : 0
164661
+ };
164662
+ }
164534
164663
  var saveEmbeddingStatements, loadAllEmbeddingsStatements, deleteEmbeddingStatements, getStoredModelIdStatements, clearAllEmbeddingsStatements, getDistinctStoredModelIdsStatements;
164535
164664
  var init_storage_memory_embeddings = __esm(() => {
164536
164665
  saveEmbeddingStatements = new WeakMap;
@@ -165575,6 +165704,28 @@ function countUnembeddedSessionCompartments(db, projectPath, sessionId, modelId)
165575
165704
  )`).get(projectPath, sessionId, projectPath, modelId);
165576
165705
  return typeof row?.n === "number" ? row.n : 0;
165577
165706
  }
165707
+ function countSessionCompartmentEmbedCoverage(db, projectPath, sessionId, modelId) {
165708
+ const row = db.prepare(`SELECT
165709
+ COUNT(*) AS total,
165710
+ SUM(CASE WHEN EXISTS (
165711
+ SELECT 1 FROM compartment_chunk_embeddings e
165712
+ WHERE e.compartment_id = c.id
165713
+ AND e.project_path = ?
165714
+ AND e.model_id = ?
165715
+ ) THEN 1 ELSE 0 END) AS embedded
165716
+ FROM compartments c
165717
+ JOIN session_projects sp
165718
+ ON sp.session_id = c.session_id
165719
+ AND sp.harness = c.harness
165720
+ AND sp.project_path = ?
165721
+ WHERE c.session_id = ?
165722
+ AND c.start_message IS NOT NULL
165723
+ AND c.end_message IS NOT NULL`).get(projectPath, modelId, projectPath, sessionId);
165724
+ return {
165725
+ total: typeof row?.total === "number" ? row.total : 0,
165726
+ embedded: typeof row?.embedded === "number" ? row.embedded : 0
165727
+ };
165728
+ }
165578
165729
  var DEFAULT_COMPARTMENT_CHUNK_MAX_INPUT_TOKENS = 512, CHUNK_WINDOW_SAFETY_RATIO = 0.9, loadFtsRowsStatements, existingHashStatements, existingHashByProjectStatements, deleteByCompartmentStatements, insertEmbeddingStatements, distinctModelStatements, clearProjectStatements, clearProjectModelStatements, searchRowsStatements, searchRowsByModelStatements, backfillCandidateStatements, sessionBackfillCandidateStatements;
165579
165730
  var init_compartment_chunk_embedding = __esm(() => {
165580
165731
  init_read_session_formatting();
@@ -165732,6 +165883,18 @@ async function withQuietConsole(fn) {
165732
165883
  console.error = origError;
165733
165884
  }
165734
165885
  }
165886
+ function isNativeRuntimeMissingError(error51) {
165887
+ const message = error51 instanceof Error ? error51.message : String(error51 ?? "");
165888
+ const lower = message.toLowerCase();
165889
+ const code = error51?.code;
165890
+ const name2 = error51?.name;
165891
+ if (code === "ERR_DLOPEN_FAILED" && lower.includes("onnxruntime")) {
165892
+ return true;
165893
+ }
165894
+ if (!lower.includes("onnxruntime-node"))
165895
+ return false;
165896
+ return code === "ERR_MODULE_NOT_FOUND" || name2 === "ResolveMessage" || lower.includes("cannot find package") || lower.includes("cannot find module") || lower.includes("err_module_not_found");
165897
+ }
165735
165898
  function isTransientLoadError(error51) {
165736
165899
  const message = error51 instanceof Error ? error51.message : String(error51 ?? "");
165737
165900
  if (!message)
@@ -165796,6 +165959,9 @@ class LocalEmbeddingProvider {
165796
165959
  if (this.pipeline) {
165797
165960
  return true;
165798
165961
  }
165962
+ if (nativeRuntimeMissing) {
165963
+ return false;
165964
+ }
165799
165965
  if (this.initPromise) {
165800
165966
  await this.initPromise;
165801
165967
  return this.pipeline !== null;
@@ -165862,7 +166028,12 @@ class LocalEmbeddingProvider {
165862
166028
  await releaseLock();
165863
166029
  }
165864
166030
  } catch (error51) {
165865
- log("[magic-context] embedding model failed to load:", error51);
166031
+ if (isNativeRuntimeMissingError(error51)) {
166032
+ nativeRuntimeMissing = true;
166033
+ log("[magic-context] local embedding runtime is not installed (onnxruntime-node missing from this install). Local embeddings are disabled. Fix: reinstall the plugin (run `npx @wolfx/magic-context@latest doctor --force`), or configure an `openai-compatible`/`ollama` embedding endpoint instead. Existing memories are unaffected.");
166034
+ } else {
166035
+ log("[magic-context] embedding model failed to load:", error51);
166036
+ }
165866
166037
  this.pipeline = null;
165867
166038
  } finally {
165868
166039
  this.initPromise = null;
@@ -165973,7 +166144,7 @@ class LocalEmbeddingProvider {
165973
166144
  return this.pipeline !== null;
165974
166145
  }
165975
166146
  }
165976
- var LOCK_POLL_MS = 150, STALE_LOCK_MS, MAX_LOCK_WAIT_MS;
166147
+ var LOCK_POLL_MS = 150, STALE_LOCK_MS, MAX_LOCK_WAIT_MS, nativeRuntimeMissing = false;
165977
166148
  var init_embedding_local = __esm(() => {
165978
166149
  init_magic_context();
165979
166150
  init_data_path();
@@ -166387,6 +166558,121 @@ var init_storage_git_commit_embeddings = __esm(() => {
166387
166558
  distinctModelIdStatements = new WeakMap;
166388
166559
  });
166389
166560
 
166561
+ // src/features/magic-context/git-commits/storage-git-commits.ts
166562
+ function getInsertStatement(db) {
166563
+ let stmt = insertStatements.get(db);
166564
+ if (!stmt) {
166565
+ stmt = db.prepare(`INSERT INTO git_commits (sha, project_path, short_sha, message, author, committed_at, indexed_at)
166566
+ VALUES (?, ?, ?, ?, ?, ?, ?)
166567
+ ON CONFLICT(sha) DO UPDATE SET
166568
+ project_path = excluded.project_path,
166569
+ short_sha = excluded.short_sha,
166570
+ message = excluded.message,
166571
+ author = excluded.author,
166572
+ committed_at = excluded.committed_at,
166573
+ indexed_at = excluded.indexed_at
166574
+ WHERE git_commits.message != excluded.message`);
166575
+ insertStatements.set(db, stmt);
166576
+ }
166577
+ return stmt;
166578
+ }
166579
+ function getExistingShasStatement(db) {
166580
+ let stmt = existingShasStatements.get(db);
166581
+ if (!stmt) {
166582
+ stmt = db.prepare("SELECT sha FROM git_commits WHERE project_path = ?");
166583
+ existingShasStatements.set(db, stmt);
166584
+ }
166585
+ return stmt;
166586
+ }
166587
+ function getProjectCountStatement(db) {
166588
+ let stmt = projectCountStatements.get(db);
166589
+ if (!stmt) {
166590
+ stmt = db.prepare("SELECT COUNT(*) AS count FROM git_commits WHERE project_path = ?");
166591
+ projectCountStatements.set(db, stmt);
166592
+ }
166593
+ return stmt;
166594
+ }
166595
+ function getLatestCommitTimeStatement(db) {
166596
+ let stmt = latestCommitTimeStatements.get(db);
166597
+ if (!stmt) {
166598
+ stmt = db.prepare("SELECT MAX(committed_at) AS latest FROM git_commits WHERE project_path = ?");
166599
+ latestCommitTimeStatements.set(db, stmt);
166600
+ }
166601
+ return stmt;
166602
+ }
166603
+ function getEvictOverflowStatement(db) {
166604
+ let stmt = evictOverflowStatements.get(db);
166605
+ if (!stmt) {
166606
+ stmt = db.prepare(`DELETE FROM git_commits
166607
+ WHERE rowid IN (
166608
+ SELECT rowid FROM git_commits
166609
+ WHERE project_path = ?
166610
+ ORDER BY committed_at DESC, sha DESC
166611
+ LIMIT -1 OFFSET ?
166612
+ )`);
166613
+ evictOverflowStatements.set(db, stmt);
166614
+ }
166615
+ return stmt;
166616
+ }
166617
+ function upsertCommits(db, projectPath, commits) {
166618
+ if (commits.length === 0)
166619
+ return { inserted: 0, updated: 0 };
166620
+ const existing = new Set;
166621
+ for (const row of getExistingShasStatement(db).all(projectPath)) {
166622
+ existing.add(row.sha);
166623
+ }
166624
+ let inserted = 0;
166625
+ let updated = 0;
166626
+ const now = Date.now();
166627
+ const insertStmt = getInsertStatement(db);
166628
+ db.transaction(() => {
166629
+ for (const commit of commits) {
166630
+ const result = insertStmt.run(commit.sha, projectPath, commit.shortSha, commit.message, commit.author, commit.committedAtMs, now);
166631
+ if (result.changes > 0) {
166632
+ if (existing.has(commit.sha)) {
166633
+ updated++;
166634
+ } else {
166635
+ inserted++;
166636
+ existing.add(commit.sha);
166637
+ }
166638
+ }
166639
+ }
166640
+ })();
166641
+ return { inserted, updated };
166642
+ }
166643
+ function getCommitCount(db, projectPath) {
166644
+ const row = getProjectCountStatement(db).get(projectPath);
166645
+ return row?.count ?? 0;
166646
+ }
166647
+ function getLatestIndexedCommitTimeMs(db, projectPath) {
166648
+ const row = getLatestCommitTimeStatement(db).get(projectPath);
166649
+ return row?.latest ?? null;
166650
+ }
166651
+ function enforceProjectCap(db, projectPath, maxCommits) {
166652
+ if (maxCommits <= 0)
166653
+ return 0;
166654
+ const count = getCommitCount(db, projectPath);
166655
+ if (count <= maxCommits)
166656
+ return 0;
166657
+ getEvictOverflowStatement(db).run(projectPath, maxCommits);
166658
+ const after = getCommitCount(db, projectPath);
166659
+ const evicted = Math.max(0, count - after);
166660
+ if (evicted > 0) {
166661
+ log(`[git-commits] evicted ${evicted} oldest commits for project ${projectPath} (cap=${maxCommits}, was=${count})`);
166662
+ }
166663
+ return evicted;
166664
+ }
166665
+ var insertStatements, existingShasStatements, projectCountStatements, evictStatements, evictOverflowStatements, latestCommitTimeStatements;
166666
+ var init_storage_git_commits = __esm(() => {
166667
+ init_logger();
166668
+ insertStatements = new WeakMap;
166669
+ existingShasStatements = new WeakMap;
166670
+ projectCountStatements = new WeakMap;
166671
+ evictStatements = new WeakMap;
166672
+ evictOverflowStatements = new WeakMap;
166673
+ latestCommitTimeStatements = new WeakMap;
166674
+ });
166675
+
166390
166676
  // src/features/magic-context/git-commits/sweep-coordinator.ts
166391
166677
  function runImmediate2(db, body) {
166392
166678
  db.exec("BEGIN IMMEDIATE");
@@ -166680,7 +166966,9 @@ function snapshotFor(registration) {
166680
166966
  enabled,
166681
166967
  gitCommitEnabled,
166682
166968
  modelId: registration.observationMode || !providerIsOn ? "off" : registration.modelId,
166683
- chunkModelId: registration.observationMode || !providerIsOn ? "off" : registration.chunkModelId
166969
+ chunkModelId: registration.observationMode || !providerIsOn ? "off" : registration.chunkModelId,
166970
+ model: registration.observationMode || !providerIsOn ? "off" : ("model" in registration.config) && registration.config.model.trim() ? registration.config.model.trim() : registration.modelId,
166971
+ provider: registration.observationMode || !providerIsOn ? "off" : registration.config.provider ?? "local"
166684
166972
  };
166685
166973
  }
166686
166974
  function disposeProvider(provider) {
@@ -166891,8 +167179,9 @@ async function embedUnembeddedMemoriesForProject(db, projectIdentity, batchSize
166891
167179
  }
166892
167180
  async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates, signal) {
166893
167181
  const noWork = [];
167182
+ const failed = [];
166894
167183
  if (candidates.length === 0)
166895
- return { embedded: 0, noWork };
167184
+ return { embedded: 0, noWork, failed };
166896
167185
  const maxInputTokens = getProjectEmbeddingMaxInputTokens(projectIdentity);
166897
167186
  const prepared = [];
166898
167187
  for (const candidate of candidates) {
@@ -166909,7 +167198,7 @@ async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates
166909
167198
  prepared.push({ candidate, windows });
166910
167199
  }
166911
167200
  if (prepared.length === 0)
166912
- return { embedded: 0, noWork };
167201
+ return { embedded: 0, noWork, failed };
166913
167202
  let embedded = 0;
166914
167203
  let i = 0;
166915
167204
  while (i < prepared.length) {
@@ -166926,35 +167215,60 @@ async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates
166926
167215
  const texts = [];
166927
167216
  for (const item of slice)
166928
167217
  texts.push(...item.windows.map((w) => w.text));
166929
- try {
166930
- const result = await embedBatchForProject(projectIdentity, texts, signal);
166931
- if (!result)
166932
- continue;
167218
+ const persistedIds = new Set;
167219
+ for (let attempt = 0;attempt < EMBED_SLICE_RETRY_ATTEMPTS; attempt++) {
166933
167220
  if (signal?.aborted)
166934
167221
  break;
166935
- let offset = 0;
166936
- for (const item of slice) {
166937
- const vectors = result.vectors.slice(offset, offset + item.windows.length);
166938
- offset += item.windows.length;
166939
- if (vectors.length !== item.windows.length || vectors.some((v) => !v)) {
166940
- continue;
167222
+ let result = null;
167223
+ const attemptStart = Date.now();
167224
+ try {
167225
+ result = await embedBatchForProject(projectIdentity, texts, signal);
167226
+ } catch (error51) {
167227
+ log("[magic-context] failed to proactively embed compartment chunks:", error51);
167228
+ }
167229
+ if (signal?.aborted)
167230
+ break;
167231
+ if (result) {
167232
+ let offset = 0;
167233
+ for (const item of slice) {
167234
+ const vectors = result.vectors.slice(offset, offset + item.windows.length);
167235
+ offset += item.windows.length;
167236
+ if (persistedIds.has(item.candidate.id))
167237
+ continue;
167238
+ if (vectors.length !== item.windows.length || vectors.some((v) => !v)) {
167239
+ continue;
167240
+ }
167241
+ const rows = item.windows.map((window, index) => ({
167242
+ compartmentId: item.candidate.id,
167243
+ sessionId: item.candidate.sessionId,
167244
+ projectPath: projectIdentity,
167245
+ window,
167246
+ modelId,
167247
+ vector: vectors[index]
167248
+ }));
167249
+ replaceCompartmentChunkEmbeddings(db, rows);
167250
+ persistedIds.add(item.candidate.id);
166941
167251
  }
166942
- const rows = item.windows.map((window, index) => ({
166943
- compartmentId: item.candidate.id,
166944
- sessionId: item.candidate.sessionId,
166945
- projectPath: projectIdentity,
166946
- window,
166947
- modelId,
166948
- vector: vectors[index]
166949
- }));
166950
- replaceCompartmentChunkEmbeddings(db, rows);
166951
- embedded += 1;
166952
167252
  }
166953
- } catch (error51) {
166954
- log("[magic-context] failed to proactively embed compartment chunks:", error51);
167253
+ if (persistedIds.size === slice.length)
167254
+ break;
167255
+ if (persistedIds.size > 0)
167256
+ break;
167257
+ if (Date.now() - attemptStart >= EMBED_SLOW_FAILURE_NO_RETRY_MS)
167258
+ break;
167259
+ if (attempt < EMBED_SLICE_RETRY_ATTEMPTS - 1) {
167260
+ await new Promise((resolve6) => setTimeout(resolve6, EMBED_SLICE_RETRY_BASE_MS * 2 ** attempt));
167261
+ }
167262
+ }
167263
+ embedded += persistedIds.size;
167264
+ if (!signal?.aborted) {
167265
+ for (const item of slice) {
167266
+ if (!persistedIds.has(item.candidate.id))
167267
+ failed.push(item.candidate.id);
167268
+ }
166955
167269
  }
166956
167270
  }
166957
- return { embedded, noWork };
167271
+ return { embedded, noWork, failed };
166958
167272
  }
166959
167273
  async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, options) {
166960
167274
  const snapshot = getProjectEmbeddingSnapshot(projectIdentity);
@@ -166977,9 +167291,11 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
166977
167291
  renewal.unref?.();
166978
167292
  const batchSize = Math.max(1, options?.batchSize ?? CHUNK_DRAIN_BATCH_SIZE);
166979
167293
  const skipIds = [];
167294
+ const failedIds = [];
166980
167295
  let embedded = 0;
166981
167296
  let aborted2 = false;
166982
- let providerStalled = false;
167297
+ let providerDown = false;
167298
+ let consecutiveFailedBatches = 0;
166983
167299
  try {
166984
167300
  options?.onProgress?.({ embedded, total });
166985
167301
  for (;; ) {
@@ -166987,15 +167303,26 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
166987
167303
  aborted2 = true;
166988
167304
  break;
166989
167305
  }
166990
- const candidates = loadUnembeddedSessionChunkCandidates(db, projectIdentity, sessionId, snapshot.chunkModelId, batchSize, skipIds);
167306
+ const candidates = loadUnembeddedSessionChunkCandidates(db, projectIdentity, sessionId, snapshot.chunkModelId, batchSize, [...skipIds, ...failedIds]);
166991
167307
  if (candidates.length === 0)
166992
167308
  break;
166993
- const { embedded: n, noWork } = await embedCandidateChunkBatch(db, projectIdentity, snapshot.chunkModelId, candidates, options?.signal);
167309
+ const {
167310
+ embedded: n,
167311
+ noWork,
167312
+ failed
167313
+ } = await embedCandidateChunkBatch(db, projectIdentity, snapshot.chunkModelId, candidates, options?.signal);
166994
167314
  for (const id of noWork)
166995
167315
  skipIds.push(id);
167316
+ for (const id of failed)
167317
+ failedIds.push(id);
166996
167318
  if (n === 0 && noWork.length === 0) {
166997
- providerStalled = true;
166998
- break;
167319
+ consecutiveFailedBatches += 1;
167320
+ if (consecutiveFailedBatches >= MAX_CONSECUTIVE_FAILED_BATCHES) {
167321
+ providerDown = true;
167322
+ break;
167323
+ }
167324
+ } else {
167325
+ consecutiveFailedBatches = 0;
166999
167326
  }
167000
167327
  embedded += n;
167001
167328
  options?.onProgress?.({ embedded: Math.min(embedded, total), total });
@@ -167003,23 +167330,58 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
167003
167330
  }
167004
167331
  } finally {
167005
167332
  clearInterval(renewal);
167006
- releaseGitSweepLease(db, projectIdentity, holderId);
167333
+ try {
167334
+ releaseGitSweepLease(db, projectIdentity, holderId);
167335
+ } catch (error51) {
167336
+ log("[magic-context] embed drain: lease release failed (will TTL-expire):", error51);
167337
+ }
167007
167338
  }
167008
167339
  if (aborted2)
167009
- return { status: "aborted", embedded, total };
167010
- if (providerStalled) {
167340
+ return { status: "aborted", embedded, total, failed: failedIds.length };
167341
+ if (providerDown || failedIds.length > 0) {
167011
167342
  const remaining = Math.max(0, countUnembeddedSessionCompartments(db, projectIdentity, sessionId, snapshot.chunkModelId) - skipIds.length);
167012
- if (remaining > 0)
167013
- return { status: "stalled", embedded, total, remaining };
167343
+ if (remaining > 0) {
167344
+ return { status: "stalled", embedded, total, remaining, failed: failedIds.length };
167345
+ }
167014
167346
  }
167015
- return { status: "done", embedded, total };
167347
+ return { status: "done", embedded, total, failed: failedIds.length };
167016
167348
  }
167017
- var OFF_PROVIDER_IDENTITY = "embedding-provider:off", SWEEP_MAX_WALL_CLOCK_MS, CHUNK_DRAIN_BATCH_SIZE = 8, MAX_WINDOWS_PER_EMBED_CALL = 16, SESSION_EMBED_LEASE_RENEWAL_MS, projectRegistrations, loadUnembeddedMemoriesStatements, globalRegistrationGeneration = 0, testProviderFactory = null;
167349
+ function getEmbeddingCoverageStatus(db, projectIdentity, sessionId) {
167350
+ const snapshot = getProjectEmbeddingSnapshot(projectIdentity);
167351
+ if (!snapshot?.enabled || snapshot.chunkModelId === "off") {
167352
+ return {
167353
+ enabled: false,
167354
+ model: snapshot?.model ?? "off",
167355
+ provider: snapshot?.provider ?? "off",
167356
+ session: { embedded: 0, total: 0 },
167357
+ memories: { embedded: 0, total: 0 },
167358
+ commits: { embedded: 0, total: 0, gitEnabled: false }
167359
+ };
167360
+ }
167361
+ const session = countSessionCompartmentEmbedCoverage(db, projectIdentity, sessionId, snapshot.chunkModelId);
167362
+ const memories = getMemoryEmbedCoverage(db, projectIdentity, snapshot.modelId);
167363
+ const gitEnabled = snapshot.gitCommitEnabled;
167364
+ const commits = gitEnabled ? {
167365
+ embedded: countEmbeddedCommits(db, projectIdentity),
167366
+ total: getCommitCount(db, projectIdentity),
167367
+ gitEnabled: true
167368
+ } : { embedded: 0, total: 0, gitEnabled: false };
167369
+ return {
167370
+ enabled: true,
167371
+ model: snapshot.model,
167372
+ provider: snapshot.provider,
167373
+ session,
167374
+ memories,
167375
+ commits
167376
+ };
167377
+ }
167378
+ var OFF_PROVIDER_IDENTITY = "embedding-provider:off", SWEEP_MAX_WALL_CLOCK_MS, CHUNK_DRAIN_BATCH_SIZE = 8, MAX_WINDOWS_PER_EMBED_CALL = 2, SESSION_EMBED_LEASE_RENEWAL_MS, EMBED_SLICE_RETRY_ATTEMPTS = 3, EMBED_SLICE_RETRY_BASE_MS = 250, EMBED_SLOW_FAILURE_NO_RETRY_MS = 1e4, MAX_CONSECUTIVE_FAILED_BATCHES = 3, projectRegistrations, loadUnembeddedMemoriesStatements, globalRegistrationGeneration = 0, testProviderFactory = null;
167018
167379
  var init_project_embedding_registry = __esm(() => {
167019
167380
  init_magic_context();
167020
167381
  init_logger();
167021
167382
  init_compartment_chunk_embedding();
167022
167383
  init_storage_git_commit_embeddings();
167384
+ init_storage_git_commits();
167023
167385
  init_sweep_coordinator();
167024
167386
  init_embedding_cache();
167025
167387
  init_embedding_identity();
@@ -167300,58 +167662,6 @@ var init_models_dev_cache = __esm(() => {
167300
167662
  init_logger();
167301
167663
  });
167302
167664
 
167303
- // src/shared/rpc-notifications.ts
167304
- var exports_rpc_notifications = {};
167305
- __export(exports_rpc_notifications, {
167306
- pushNotification: () => pushNotification,
167307
- isTuiConnected: () => isTuiConnected,
167308
- drainNotifications: () => drainNotifications
167309
- });
167310
- function pushNotification(type, payload, sessionId) {
167311
- queue2.push({ id: nextNotificationId++, type, payload, sessionId });
167312
- if (queue2.length > 100) {
167313
- const newestPerSession = new Map;
167314
- for (const n of queue2) {
167315
- const prev = newestPerSession.get(n.sessionId);
167316
- if (prev === undefined || n.id > prev) {
167317
- newestPerSession.set(n.sessionId, n.id);
167318
- }
167319
- }
167320
- const mustKeep = new Set(newestPerSession.values());
167321
- const byNewest = [...queue2].sort((a, b) => b.id - a.id);
167322
- const kept = [];
167323
- for (const n of byNewest) {
167324
- if (kept.length < 50 || mustKeep.has(n.id))
167325
- kept.push(n);
167326
- }
167327
- queue2 = kept.sort((a, b) => a.id - b.id);
167328
- }
167329
- }
167330
- function drainNotifications(lastReceivedId = 0, sessionId) {
167331
- const now = Date.now();
167332
- lastDrainAtAny = now;
167333
- if (sessionId !== undefined)
167334
- lastDrainAtBySession.set(sessionId, now);
167335
- const matchesClient = (notification) => sessionId === undefined || notification.sessionId === undefined || notification.sessionId === sessionId;
167336
- if (lastReceivedId > 0) {
167337
- queue2 = queue2.filter((notification) => !(notification.id <= lastReceivedId && matchesClient(notification)));
167338
- }
167339
- return queue2.filter((notification) => notification.id > lastReceivedId && matchesClient(notification));
167340
- }
167341
- function isTuiConnected(sessionId) {
167342
- const now = Date.now();
167343
- if (sessionId !== undefined) {
167344
- const at = lastDrainAtBySession.get(sessionId) ?? 0;
167345
- return at > 0 && now - at < TUI_CONNECTED_WINDOW_MS;
167346
- }
167347
- return lastDrainAtAny > 0 && now - lastDrainAtAny < TUI_CONNECTED_WINDOW_MS;
167348
- }
167349
- var queue2, nextNotificationId = 1, lastDrainAtBySession, lastDrainAtAny = 0, TUI_CONNECTED_WINDOW_MS = 3000;
167350
- var init_rpc_notifications = __esm(() => {
167351
- queue2 = [];
167352
- lastDrainAtBySession = new Map;
167353
- });
167354
-
167355
167665
  // src/features/magic-context/compartment-embedding.ts
167356
167666
  async function embedAndStoreCompartmentChunks(db, sessionId, projectPath, compartments) {
167357
167667
  if (compartments.length === 0)
@@ -170903,7 +171213,7 @@ function buildToolArcs(messages) {
170903
171213
  }
170904
171214
  return arcs.sort((a, b) => a.invOrdinal - b.invOrdinal || (a.resOrdinal ?? Number.MAX_SAFE_INTEGER) - (b.resOrdinal ?? Number.MAX_SAFE_INTEGER));
170905
171215
  }
170906
- function fenceBoundaryForToolArcs(candidate, arcs, lastCompartmentEndOrdinal) {
171216
+ function fenceBoundaryForToolArcs(candidate, arcs, lastCompartmentEndOrdinal, recentOpenArcCutoff) {
170907
171217
  let boundary = candidate;
170908
171218
  for (const arc of arcs) {
170909
171219
  if (arc.resOrdinal !== null) {
@@ -170912,6 +171222,8 @@ function fenceBoundaryForToolArcs(candidate, arcs, lastCompartmentEndOrdinal) {
170912
171222
  }
170913
171223
  continue;
170914
171224
  }
171225
+ if (arc.invOrdinal < recentOpenArcCutoff)
171226
+ continue;
170915
171227
  if (arc.invOrdinal >= lastCompartmentEndOrdinal + 1 && arc.invOrdinal < boundary) {
170916
171228
  return arc.invOrdinal;
170917
171229
  }
@@ -171151,7 +171463,7 @@ function semanticSnapBoundary(args) {
171151
171463
  return snapped;
171152
171464
  }
171153
171465
  function applyHeadCap(args) {
171154
- const { index, protectedTailStart, offset, arcs, capTokens } = args;
171466
+ const { index, protectedTailStart, offset, arcs, capTokens, recentOpenArcCutoff } = args;
171155
171467
  if (offset >= protectedTailStart)
171156
171468
  return { eligibleEndOrdinal: offset, oversizeAtomicUnit: false };
171157
171469
  let end = index.findHeadEndForCap(offset, protectedTailStart, capTokens);
@@ -171159,7 +171471,7 @@ function applyHeadCap(args) {
171159
171471
  for (const arc of arcs) {
171160
171472
  const resOrdinal = arc.resOrdinal;
171161
171473
  if (resOrdinal === null) {
171162
- if (arc.invOrdinal >= offset && arc.invOrdinal < end) {
171474
+ if (arc.invOrdinal >= recentOpenArcCutoff && arc.invOrdinal >= offset && arc.invOrdinal < end) {
171163
171475
  end = Math.min(end, arc.invOrdinal);
171164
171476
  }
171165
171477
  continue;
@@ -171226,7 +171538,14 @@ function resolveProtectedTailBoundary(ctx) {
171226
171538
  }
171227
171539
  if (ctx.mode === "manual-full-recomp") {
171228
171540
  const arcs2 = buildToolArcs(messages);
171229
- const firstOpenArc = arcs2.find((arc) => arc.resOrdinal === null && arc.invOrdinal >= offset);
171541
+ const recompTarget = deriveProtectedTailTokenTarget({
171542
+ contextLimit: ctx.contextLimit,
171543
+ executeThresholdPercentage: ctx.executeThresholdPercentage,
171544
+ usagePercentage: 0,
171545
+ triggerBudget: ctx.triggerBudget
171546
+ });
171547
+ const recentOpenArcCutoff2 = index.findSuffixStartForTokens(recompTarget.N);
171548
+ const firstOpenArc = arcs2.find((arc) => arc.resOrdinal === null && arc.invOrdinal >= offset && arc.invOrdinal >= recentOpenArcCutoff2);
171230
171549
  const protectedTailStart2 = firstOpenArc?.invOrdinal ?? rawMessageCount + 1;
171231
171550
  const rawRangeFingerprint2 = computeRawRangeFingerprint(messages, offset, protectedTailStart2);
171232
171551
  return {
@@ -171268,13 +171587,14 @@ function resolveProtectedTailBoundary(ctx) {
171268
171587
  const scaledN = ctx.emergencyTailScale ? Math.max(1, Math.floor(target.N * ctx.emergencyTailScale)) : target.N;
171269
171588
  const arcs = buildToolArcs(messages);
171270
171589
  let boundary = index.findSuffixStartForTokens(scaledN);
171590
+ const recentOpenArcCutoff = boundary;
171271
171591
  let boundaryReason = boundary === 1 ? "whole-session-smaller-than-tail" : "size-walk";
171272
171592
  const tokenAtBoundary = index.tokenForOrdinal(boundary);
171273
171593
  if (boundary <= rawMessageCount && tokenAtBoundary > Math.max(2 * scaledN, 64000) && boundary < rawMessageCount) {
171274
171594
  boundary += 1;
171275
171595
  boundaryReason = "huge-message-exception";
171276
171596
  }
171277
- boundary = fenceBoundaryForToolArcs(boundary, arcs, ctx.lastCompartmentEndOrdinal);
171597
+ boundary = fenceBoundaryForToolArcs(boundary, arcs, ctx.lastCompartmentEndOrdinal, recentOpenArcCutoff);
171278
171598
  const snapped = semanticSnapBoundary({
171279
171599
  messages,
171280
171600
  index,
@@ -171284,7 +171604,7 @@ function resolveProtectedTailBoundary(ctx) {
171284
171604
  });
171285
171605
  if (snapped !== boundary)
171286
171606
  boundaryReason = "semantic-snap";
171287
- boundary = fenceBoundaryForToolArcs(snapped, arcs, ctx.lastCompartmentEndOrdinal);
171607
+ boundary = fenceBoundaryForToolArcs(snapped, arcs, ctx.lastCompartmentEndOrdinal, recentOpenArcCutoff);
171288
171608
  let runtimeFloor = offset;
171289
171609
  if (ctx.migrationFloorActive)
171290
171610
  runtimeFloor = Math.max(runtimeFloor, ctx.priorBoundaryOrdinal);
@@ -171320,7 +171640,8 @@ function resolveProtectedTailBoundary(ctx) {
171320
171640
  offset,
171321
171641
  arcs,
171322
171642
  lastCompartmentEndOrdinal: ctx.lastCompartmentEndOrdinal,
171323
- capTokens: perRunCap
171643
+ capTokens: perRunCap,
171644
+ recentOpenArcCutoff
171324
171645
  });
171325
171646
  const rawRangeFingerprint = computeRawRangeFingerprint(messages, offset, head.eligibleEndOrdinal);
171326
171647
  return {
@@ -177249,15 +177570,15 @@ function shouldShowAnnouncement() {
177249
177570
  }
177250
177571
  return state.version !== ANNOUNCEMENT_VERSION;
177251
177572
  }
177252
- var ANNOUNCEMENT_VERSION = "0.24.0", ANNOUNCEMENT_FEATURES, ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU", STATE_FILENAME = "last_announced_version";
177573
+ var ANNOUNCEMENT_VERSION = "0.25.0", ANNOUNCEMENT_FEATURES, ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU", STATE_FILENAME = "last_announced_version";
177253
177574
  var init_announcement = __esm(() => {
177254
177575
  init_data_path();
177255
177576
  ANNOUNCEMENT_FEATURES = [
177256
- "Searchable session history: ctx_search can now find older discussion by meaning, not just keywords. New history is embedded automatically to backfill an EXISTING session's older history, run /ctx-embed-history once (it works in the background).",
177257
- "Cross-project workspaces: group related repos and share project memories across them, with per-category control over what's shared. Set them up in the dashboard's Workspaces panel.",
177258
- "Pi: fixed sessions overflowing the model context while still showing moderate usage Pi now sheds context before a tool-heavy turn overflows.",
177259
- "Fewer prompt-cache busts: doc edits, processed screenshots, and a rebuild-then-bust-again case no longer re-bill large prompt prefixes.",
177260
- "Setup wizard now lists your actual models with type-ahead instead of fixed recommendations, and explains the historian/dreamer roles (issue #144). Plus a GitHub Copilot tool-pairing fix (#135)."
177577
+ "Old tool output is now reclaimed automatically: once a file read / search / command output has gone a full execute cycle unused, it's dropped on the next one no need to call ctx_reduce for stale results.",
177578
+ "Recover anything that was dropped: ctx_expand({ message: N }) returns a dropped message's full content (every tool call's input + output) from storage. ctx_expand({ start, end, verbose: true }) lists a range message-by-message to find it.",
177579
+ "Searchable history made reliable: /ctx-embed shows embedding coverage and runs a resilient backfill (retries transient failures, no longer bails on the first hiccup); the active session now auto-embeds in the background. ctx_reduce guidance also reframed as deferred + recoverable so models trim spent output earlier.",
177580
+ "Pi: fixed /ctx-dream (was failing with 'Unknown named parameter') and local-embedding load failures on Windows/Desktop (#151, #128).",
177581
+ "Runaway background agents on weak/local models are now capped and force-stopped (#154, #152). Plus several prompt-cache busts removed."
177261
177582
  ];
177262
177583
  });
177263
177584
  // src/agents/permissions.ts
@@ -177986,9 +178307,9 @@ function getMagicContextBuiltinCommands() {
177986
178307
  template: "ctx-dream",
177987
178308
  description: "Run the hidden dreamer maintenance pass for this project now"
177988
178309
  },
177989
- "ctx-embed-history": {
177990
- template: "ctx-embed-history",
177991
- description: "Embed all of this session's history compartments for semantic search, in one pass"
178310
+ "ctx-embed": {
178311
+ template: "ctx-embed",
178312
+ description: "Embedding status, or start/pause history compartment embedding (start | pause)"
177992
178313
  }
177993
178314
  };
177994
178315
  }
@@ -178859,7 +179180,7 @@ function enqueueDream(db, projectIdentity, reason, force = false) {
178859
179180
  return db.transaction(() => {
178860
179181
  if (!hasActiveDreamLease(db)) {
178861
179182
  const staleThresholdMs = force ? 2 * 60 * 1000 : 120 * 60 * 1000;
178862
- db.prepare("DELETE FROM dream_queue WHERE project_path = ? AND started_at IS NOT NULL AND started_at < ?").run([projectIdentity, now - staleThresholdMs]);
179183
+ db.prepare("DELETE FROM dream_queue WHERE project_path = ? AND started_at IS NOT NULL AND started_at < ?").run(projectIdentity, now - staleThresholdMs);
178863
179184
  }
178864
179185
  const existing = db.prepare("SELECT id FROM dream_queue WHERE project_path = ?").get(projectIdentity);
178865
179186
  if (existing) {
@@ -180554,120 +180875,7 @@ ${body}` : subject;
180554
180875
  init_logger();
180555
180876
  init_embedding();
180556
180877
  init_storage_git_commit_embeddings();
180557
-
180558
- // src/features/magic-context/git-commits/storage-git-commits.ts
180559
- init_logger();
180560
- var insertStatements = new WeakMap;
180561
- var existingShasStatements = new WeakMap;
180562
- var projectCountStatements = new WeakMap;
180563
- var evictStatements = new WeakMap;
180564
- var evictOverflowStatements = new WeakMap;
180565
- var latestCommitTimeStatements = new WeakMap;
180566
- function getInsertStatement(db) {
180567
- let stmt = insertStatements.get(db);
180568
- if (!stmt) {
180569
- stmt = db.prepare(`INSERT INTO git_commits (sha, project_path, short_sha, message, author, committed_at, indexed_at)
180570
- VALUES (?, ?, ?, ?, ?, ?, ?)
180571
- ON CONFLICT(sha) DO UPDATE SET
180572
- project_path = excluded.project_path,
180573
- short_sha = excluded.short_sha,
180574
- message = excluded.message,
180575
- author = excluded.author,
180576
- committed_at = excluded.committed_at,
180577
- indexed_at = excluded.indexed_at
180578
- WHERE git_commits.message != excluded.message`);
180579
- insertStatements.set(db, stmt);
180580
- }
180581
- return stmt;
180582
- }
180583
- function getExistingShasStatement(db) {
180584
- let stmt = existingShasStatements.get(db);
180585
- if (!stmt) {
180586
- stmt = db.prepare("SELECT sha FROM git_commits WHERE project_path = ?");
180587
- existingShasStatements.set(db, stmt);
180588
- }
180589
- return stmt;
180590
- }
180591
- function getProjectCountStatement(db) {
180592
- let stmt = projectCountStatements.get(db);
180593
- if (!stmt) {
180594
- stmt = db.prepare("SELECT COUNT(*) AS count FROM git_commits WHERE project_path = ?");
180595
- projectCountStatements.set(db, stmt);
180596
- }
180597
- return stmt;
180598
- }
180599
- function getLatestCommitTimeStatement(db) {
180600
- let stmt = latestCommitTimeStatements.get(db);
180601
- if (!stmt) {
180602
- stmt = db.prepare("SELECT MAX(committed_at) AS latest FROM git_commits WHERE project_path = ?");
180603
- latestCommitTimeStatements.set(db, stmt);
180604
- }
180605
- return stmt;
180606
- }
180607
- function getEvictOverflowStatement(db) {
180608
- let stmt = evictOverflowStatements.get(db);
180609
- if (!stmt) {
180610
- stmt = db.prepare(`DELETE FROM git_commits
180611
- WHERE rowid IN (
180612
- SELECT rowid FROM git_commits
180613
- WHERE project_path = ?
180614
- ORDER BY committed_at DESC, sha DESC
180615
- LIMIT -1 OFFSET ?
180616
- )`);
180617
- evictOverflowStatements.set(db, stmt);
180618
- }
180619
- return stmt;
180620
- }
180621
- function upsertCommits(db, projectPath, commits) {
180622
- if (commits.length === 0)
180623
- return { inserted: 0, updated: 0 };
180624
- const existing = new Set;
180625
- for (const row of getExistingShasStatement(db).all(projectPath)) {
180626
- existing.add(row.sha);
180627
- }
180628
- let inserted = 0;
180629
- let updated = 0;
180630
- const now = Date.now();
180631
- const insertStmt = getInsertStatement(db);
180632
- db.transaction(() => {
180633
- for (const commit of commits) {
180634
- const result = insertStmt.run(commit.sha, projectPath, commit.shortSha, commit.message, commit.author, commit.committedAtMs, now);
180635
- if (result.changes > 0) {
180636
- if (existing.has(commit.sha)) {
180637
- updated++;
180638
- } else {
180639
- inserted++;
180640
- existing.add(commit.sha);
180641
- }
180642
- }
180643
- }
180644
- })();
180645
- return { inserted, updated };
180646
- }
180647
- function getCommitCount(db, projectPath) {
180648
- const row = getProjectCountStatement(db).get(projectPath);
180649
- return row?.count ?? 0;
180650
- }
180651
- function getLatestIndexedCommitTimeMs(db, projectPath) {
180652
- const row = getLatestCommitTimeStatement(db).get(projectPath);
180653
- return row?.latest ?? null;
180654
- }
180655
- function enforceProjectCap(db, projectPath, maxCommits) {
180656
- if (maxCommits <= 0)
180657
- return 0;
180658
- const count = getCommitCount(db, projectPath);
180659
- if (count <= maxCommits)
180660
- return 0;
180661
- getEvictOverflowStatement(db).run(projectPath, maxCommits);
180662
- const after = getCommitCount(db, projectPath);
180663
- const evicted = Math.max(0, count - after);
180664
- if (evicted > 0) {
180665
- log(`[git-commits] evicted ${evicted} oldest commits for project ${projectPath} (cap=${maxCommits}, was=${count})`);
180666
- }
180667
- return evicted;
180668
- }
180669
-
180670
- // src/features/magic-context/git-commits/indexer.ts
180878
+ init_storage_git_commits();
180671
180879
  var MS_PER_DAY = 24 * 60 * 60 * 1000;
180672
180880
  var EMBED_BATCH_SIZE = 16;
180673
180881
  var EMBED_MAX_PER_SWEEP = 500;
@@ -180905,6 +181113,7 @@ function searchGitCommitsSync(db, projectPath, query, options) {
180905
181113
 
180906
181114
  // src/features/magic-context/git-commits/index.ts
180907
181115
  init_storage_git_commit_embeddings();
181116
+ init_storage_git_commits();
180908
181117
  init_sweep_coordinator();
180909
181118
 
180910
181119
  // src/plugin/dream-timer.ts
@@ -182328,7 +182537,7 @@ function createMagicContextCommandHandler(deps) {
182328
182537
  const isAugCommand = (command) => command === "ctx-aug";
182329
182538
  const isDreamCommand = (command) => command === "ctx-dream";
182330
182539
  const isSessionUpgradeCommand = (command) => command === "ctx-session-upgrade";
182331
- const isEmbedHistoryCommand = (command) => command === "ctx-embed-history";
182540
+ const isEmbedCommand = (command) => command === "ctx-embed";
182332
182541
  return {
182333
182542
  "command.execute.before": async (input, _output, _params) => {
182334
182543
  const isStatus = isStatusCommand(input.command);
@@ -182337,8 +182546,8 @@ function createMagicContextCommandHandler(deps) {
182337
182546
  const isAug = isAugCommand(input.command);
182338
182547
  const isDream = isDreamCommand(input.command);
182339
182548
  const isSessionUpgrade = isSessionUpgradeCommand(input.command);
182340
- const isEmbedHistory = isEmbedHistoryCommand(input.command);
182341
- if (!isStatus && !isFlush && !isRecomp && !isAug && !isDream && !isSessionUpgrade && !isEmbedHistory) {
182549
+ const isEmbed = isEmbedCommand(input.command);
182550
+ if (!isStatus && !isFlush && !isRecomp && !isAug && !isDream && !isSessionUpgrade && !isEmbed) {
182342
182551
  return;
182343
182552
  }
182344
182553
  const sessionId = input.sessionID;
@@ -182351,15 +182560,50 @@ function createMagicContextCommandHandler(deps) {
182351
182560
  await executeDreaming(deps, sessionId);
182352
182561
  return;
182353
182562
  }
182354
- if (isEmbedHistory) {
182355
- const summary = deps.executeEmbedHistory ? await deps.executeEmbedHistory(sessionId) : "Semantic embedding is not configured for this project, so there is nothing to embed.";
182356
- await deps.sendNotification(sessionId, summary, {});
182357
- throwSentinel(input.command);
182563
+ if (isEmbed) {
182564
+ const sub = input.arguments.trim().toLowerCase();
182565
+ if (sub === "pause") {
182566
+ const summary = deps.pauseEmbedDrain ? deps.pauseEmbedDrain(sessionId) : "Embedding pause is unavailable.";
182567
+ if (isTuiConnected(sessionId)) {
182568
+ pushNotification("action", { action: "show-result-dialog", title: "Embed", message: summary }, sessionId);
182569
+ } else {
182570
+ await deps.sendNotification(sessionId, summary, {});
182571
+ }
182572
+ throwSentinel(input.command);
182573
+ }
182574
+ if (sub === "start") {
182575
+ const summary = deps.executeEmbedHistory ? await deps.executeEmbedHistory(sessionId) : "Semantic embedding is not configured for this project, so there is nothing to embed.";
182576
+ if (isTuiConnected(sessionId)) {
182577
+ pushNotification("action", { action: "show-result-dialog", title: "Embed", message: summary }, sessionId);
182578
+ } else {
182579
+ await deps.sendNotification(sessionId, summary, {});
182580
+ }
182581
+ throwSentinel(input.command);
182582
+ }
182583
+ if (sub !== "") {
182584
+ await deps.sendNotification(sessionId, "Usage: `/ctx-embed` (status), `/ctx-embed start`, or `/ctx-embed pause`.", {});
182585
+ throwSentinel(input.command);
182586
+ }
182587
+ if (isTuiConnected(sessionId)) {
182588
+ pushNotification("action", { action: "show-embed-dialog" }, sessionId);
182589
+ sessionLog(sessionId, "command ctx-embed: pushed show-embed-dialog to TUI");
182590
+ throwSentinel(input.command);
182591
+ }
182592
+ result = deps.getEmbedStatusText ? `## Embedding Status
182593
+
182594
+ ${deps.getEmbedStatusText(sessionId)}` : `## Embedding Status
182595
+
182596
+ Embedding status is unavailable.`;
182358
182597
  }
182359
182598
  if (isFlush) {
182360
182599
  result = executeFlush(deps.db, sessionId);
182361
182600
  clearCachedM0M1(deps.db, sessionId);
182362
182601
  deps.onFlush?.(sessionId);
182602
+ if (isTuiConnected(sessionId)) {
182603
+ pushNotification("action", { action: "show-flush-dialog", message: result }, sessionId);
182604
+ sessionLog(sessionId, "command ctx-flush: pushed show-flush-dialog to TUI");
182605
+ throwSentinel(input.command);
182606
+ }
182363
182607
  }
182364
182608
  if (isStatus) {
182365
182609
  if (isTuiConnected(sessionId)) {
@@ -182486,6 +182730,34 @@ ${snap.error}`;
182486
182730
  // src/hooks/magic-context/hook.ts
182487
182731
  init_derive_budgets();
182488
182732
 
182733
+ // src/hooks/magic-context/embed-session-state.ts
182734
+ var embedPauseBySession = new Set;
182735
+ var embedRunStateBySession = new Map;
182736
+ var autoEmbedAttemptedBySession = new Set;
182737
+ function getEmbedDrainUiStatus(sessionId, progress) {
182738
+ if (embedPauseBySession.has(sessionId)) {
182739
+ return { status: "paused" };
182740
+ }
182741
+ if (progress?.kind === "embed" && progress.phase === "recomp") {
182742
+ return { status: "running" };
182743
+ }
182744
+ if (progress?.kind === "embed" && (progress.phase === "failed" || progress.phase === "skipped") && progress.message) {
182745
+ if (/provider/i.test(progress.message)) {
182746
+ return { status: "stopped", detail: progress.message };
182747
+ }
182748
+ }
182749
+ return { status: "idle" };
182750
+ }
182751
+ function clearEmbedSessionState(sessionId) {
182752
+ embedPauseBySession.delete(sessionId);
182753
+ const ctrl = embedRunStateBySession.get(sessionId);
182754
+ if (ctrl) {
182755
+ ctrl.abort();
182756
+ embedRunStateBySession.delete(sessionId);
182757
+ }
182758
+ autoEmbedAttemptedBySession.delete(sessionId);
182759
+ }
182760
+
182489
182761
  // src/features/magic-context/message-index-async.ts
182490
182762
  init_logger();
182491
182763
  await init_message_index();
@@ -182846,6 +183118,13 @@ function computePressure(input) {
182846
183118
  function approxThousands(tokens) {
182847
183119
  return `${Math.round(tokens / 1000)}k`;
182848
183120
  }
183121
+ function formatOldestReclaimableHint(hint) {
183122
+ if (!hint || hint.length === 0)
183123
+ return "";
183124
+ const rendered = hint.slice(0, 4).map((tag) => `§${tag.tagNumber}§ ${tag.toolName ?? "tool"}`).join(" · ");
183125
+ return rendered.length > 0 ? `
183126
+ oldest reclaimable: ${rendered}.` : "";
183127
+ }
182849
183128
  var CHANNEL2_USABLE_FRACTION = 1 / 3;
182850
183129
  var CHANNEL2_MIN_RECLAIMABLE = 1e4;
182851
183130
  function shouldTriggerChannel2(input) {
@@ -182855,14 +183134,16 @@ function shouldTriggerChannel2(input) {
182855
183134
  return true;
182856
183135
  return input.reclaimableTokens >= input.usableTokens * CHANNEL2_USABLE_FRACTION;
182857
183136
  }
182858
- function buildChannel2Reminder(undroppedTokens) {
183137
+ function buildChannel2Reminder(undroppedTokens, hint) {
182859
183138
  const amount = approxThousands(undroppedTokens);
183139
+ const hintText = formatOldestReclaimableHint(hint);
182860
183140
  return `<system-reminder>
182861
- ` + `Routine context housekeeping is near: a large span of this session will be comparted soon, ` + `and ~${amount} tokens of tool output remain unreduced. Drop spent outputs with ctx_reduce ` + `first so the archived span is the part that matters.
183141
+ ` + `Routine context housekeeping is near: a large span of this session will be comparted soon, ` + `and ~${amount} tokens of tool output remain unreduced. Drop spent outputs with ctx_reduce ` + `first so the archived span is the part that matters.${hintText}
182862
183142
  ` + `</system-reminder>`;
182863
183143
  }
182864
- function buildChannel1Reminder(level, undroppedTokens) {
183144
+ function buildChannel1Reminder(level, undroppedTokens, hint) {
182865
183145
  const amount = approxThousands(undroppedTokens);
183146
+ const hintText = formatOldestReclaimableHint(hint);
182866
183147
  let body;
182867
183148
  switch (level) {
182868
183149
  case "gentle":
@@ -182878,7 +183159,7 @@ function buildChannel1Reminder(level, undroppedTokens) {
182878
183159
  return `
182879
183160
 
182880
183161
  <system-reminder>
182881
- ${body}
183162
+ ${body}${hintText}
182882
183163
  </system-reminder>`;
182883
183164
  }
182884
183165
 
@@ -182932,10 +183213,10 @@ async function maybeDeliverChannel2(sessionId, deps) {
182932
183213
  try {
182933
183214
  const client3 = getLiveServerClient(serverUrl, deps.directory);
182934
183215
  const promptContext = await resolvePromptContext(client3, sessionId);
182935
- const reminder = buildChannel2Reminder(deps.reclaimableTokens);
183216
+ const reminder = buildChannel2Reminder(deps.reclaimableTokens, deps.oldestReclaimableToolTags);
182936
183217
  const body = {
182937
183218
  noReply: false,
182938
- parts: [{ type: "text", text: reminder }]
183219
+ parts: [{ type: "text", text: reminder, synthetic: true }]
182939
183220
  };
182940
183221
  if (promptContext?.agent)
182941
183222
  body.agent = promptContext.agent;
@@ -183393,7 +183674,8 @@ function applyCavemanCleanup(sessionId, db, targets, tags, config2) {
183393
183674
  const result = {
183394
183675
  compressedToLite: 0,
183395
183676
  compressedToFull: 0,
183396
- compressedToUltra: 0
183677
+ compressedToUltra: 0,
183678
+ mutatedTextTags: 0
183397
183679
  };
183398
183680
  if (!config2.enabled)
183399
183681
  return result;
@@ -183434,7 +183716,9 @@ function applyCavemanCleanup(sessionId, db, targets, tags, config2) {
183434
183716
  const target = targets.get(tag.tagNumber);
183435
183717
  if (!target)
183436
183718
  continue;
183437
- target.setContent(compressed);
183719
+ const didMutate = target.setContent(compressed);
183720
+ if (didMutate)
183721
+ result.mutatedTextTags += 1;
183438
183722
  updateCavemanDepth(db, sessionId, tag.tagNumber, targetDepth);
183439
183723
  if (targetDepth === DEPTH_LITE)
183440
183724
  result.compressedToLite += 1;
@@ -184022,28 +184306,6 @@ function stripInlineThinking(messages, messageTagNumbers, clearReasoningAge) {
184022
184306
  }
184023
184307
  return stripped;
184024
184308
  }
184025
- function truncateErroredTools(messages, watermark, messageTagNumbers) {
184026
- let truncated = 0;
184027
- for (let i = 0;i < messages.length; i++) {
184028
- const maxTag = messageTagNumbers.get(messages[i]) ?? 0;
184029
- if (maxTag > watermark) {
184030
- continue;
184031
- }
184032
- for (const part of messages[i].parts) {
184033
- if (!isRecord(part) || part.type !== "tool" || !isRecord(part.state)) {
184034
- continue;
184035
- }
184036
- if (part.state.status !== "error") {
184037
- continue;
184038
- }
184039
- if (typeof part.state.error === "string" && part.state.error.length > 100) {
184040
- part.state.error = `${part.state.error.slice(0, 100)}... [truncated]`;
184041
- truncated++;
184042
- }
184043
- }
184044
- }
184045
- return truncated;
184046
- }
184047
184309
  var REASONING_IGNORED_PART_TYPES = new Set([
184048
184310
  "step-start",
184049
184311
  "step-finish",
@@ -184461,78 +184723,11 @@ function appendReminderToUserMessage(message, reminder) {
184461
184723
 
184462
184724
  // src/hooks/magic-context/apply-operations.ts
184463
184725
  await init_storage();
184464
-
184465
- // src/hooks/magic-context/system-injection-stripper.ts
184466
- var SYSTEM_INJECTION_MARKERS = [
184467
- "<!-- OMO_INTERNAL_INITIATOR -->",
184468
- "[SYSTEM DIRECTIVE: MAGIC-CONTEXT",
184469
- "[SYSTEM DIRECTIVE: OH-MY-OPENCODE",
184470
- "[Category+Skill Reminder]",
184471
- "[EDIT ERROR - IMMEDIATE ACTION REQUIRED]",
184472
- "[task CALL FAILED - IMMEDIATE RETRY REQUIRED]",
184473
- "[EMERGENCY CONTEXT WINDOW WARNING]",
184474
- "Unstable background agent appears idle",
184475
- "**THE SUBAGENT JUST CLAIMED THIS TASK IS DONE."
184476
- ];
184477
- var SYSTEM_REMINDER_REGEX = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
184478
- var OMO_MARKER_REGEX = /<!-- OMO_INTERNAL_INITIATOR -->/g;
184479
- function stripSystemInjection(text) {
184480
- let hasInjection = false;
184481
- for (const marker of SYSTEM_INJECTION_MARKERS) {
184482
- if (text.includes(marker)) {
184483
- hasInjection = true;
184484
- break;
184485
- }
184486
- }
184487
- if (SYSTEM_REMINDER_REGEX.test(text))
184488
- hasInjection = true;
184489
- SYSTEM_REMINDER_REGEX.lastIndex = 0;
184490
- if (!hasInjection)
184491
- return null;
184492
- let cleaned = text;
184493
- cleaned = cleaned.replace(SYSTEM_REMINDER_REGEX, "");
184494
- cleaned = cleaned.replace(OMO_MARKER_REGEX, "");
184495
- cleaned = cleaned.replace(/\[SYSTEM DIRECTIVE: OH-MY-(?:OPENCODE|CLAUDE)[^\]]*\][\s\S]*?(?=\n\n(?!\s*[-*])|$)/g, "");
184496
- for (const marker of SYSTEM_INJECTION_MARKERS) {
184497
- if (marker.startsWith("<!-- ") || marker.startsWith("[SYSTEM DIRECTIVE"))
184498
- continue;
184499
- const idx = cleaned.indexOf(marker);
184500
- if (idx === -1)
184501
- continue;
184502
- const blockEnd = cleaned.indexOf(`
184503
-
184504
- `, idx + marker.length);
184505
- cleaned = blockEnd !== -1 ? cleaned.slice(0, idx) + cleaned.slice(blockEnd) : cleaned.slice(0, idx);
184506
- }
184507
- return cleaned.trim();
184508
- }
184509
-
184510
- // src/hooks/magic-context/apply-operations.ts
184511
- init_tag_part_guards();
184512
- var USER_DROP_PREVIEW_CHARS = 250;
184513
184726
  var RECENT_TOOL_SKELETON_WINDOW = 20;
184514
- function buildReplacementContent(tagId, target) {
184515
- const role = target.message?.info.role;
184516
- if (role !== "user") {
184517
- return `[dropped §${tagId}§]`;
184518
- }
184519
- const currentContent = target.getContent?.() ?? "";
184520
- const strippedInjection = stripSystemInjection(currentContent);
184521
- if (strippedInjection !== null && stripTagPrefix(strippedInjection).trim().length === 0) {
184522
- return `[dropped §${tagId}§]`;
184523
- }
184524
- const originalText = stripTagPrefix(currentContent);
184525
- if (originalText.length <= USER_DROP_PREVIEW_CHARS) {
184526
- return `[truncated §${tagId}§]
184527
- ${originalText}`;
184528
- }
184529
- const hardCut = originalText.slice(0, USER_DROP_PREVIEW_CHARS);
184530
- const softCutIndex = hardCut.search(/\s\S*$/);
184531
- const preview = softCutIndex > USER_DROP_PREVIEW_CHARS - 30 ? hardCut.slice(0, softCutIndex) : hardCut;
184532
- return `[truncated §${tagId}§]
184533
- ${preview}…`;
184534
- }
184535
- function applyPendingOperations(sessionId, db, targets, protectedTags = 0, preloadedTags, preloadedPendingOps) {
184727
+ function buildReplacementContent(tagId) {
184728
+ return `[dropped §${tagId}§]`;
184729
+ }
184730
+ function applyPendingOperations(sessionId, db, targets, protectedTags = 0, preloadedTags, preloadedPendingOps, syntheticPendingOps = []) {
184536
184731
  let didMutateMessage = false;
184537
184732
  db.transaction(() => {
184538
184733
  const tags = preloadedTags ?? getTagsBySession(db, sessionId);
@@ -184540,11 +184735,16 @@ function applyPendingOperations(sessionId, db, targets, protectedTags = 0, prelo
184540
184735
  const tagTypeById = new Map(tags.map((tag) => [tag.tagNumber, tag.type]));
184541
184736
  const protectedTagIds = protectedTags > 0 ? new Set(tags.filter((tag) => tag.status === "active").map((tag) => tag.tagNumber).sort((left, right) => right - left).slice(0, protectedTags)) : new Set;
184542
184737
  const pendingOps = preloadedPendingOps ?? getPendingOps(db, sessionId);
184738
+ const opsToApply = [
184739
+ ...pendingOps.map((op) => ({ op, synthetic: false })),
184740
+ ...syntheticPendingOps.map((op) => ({ op, synthetic: true }))
184741
+ ];
184543
184742
  const skeletonWindow = new Set(tags.filter((tag) => tag.type === "tool").map((tag) => tag.tagNumber).sort((left, right) => right - left).slice(0, RECENT_TOOL_SKELETON_WINDOW));
184544
- for (const pendingOp of pendingOps) {
184743
+ for (const { op: pendingOp, synthetic } of opsToApply) {
184545
184744
  const tagStatus = tagStatusById.get(pendingOp.tagId);
184546
184745
  if (tagStatus === "compacted" || tagStatus === "dropped") {
184547
- removePendingOp(db, sessionId, pendingOp.tagId);
184746
+ if (!synthetic)
184747
+ removePendingOp(db, sessionId, pendingOp.tagId);
184548
184748
  continue;
184549
184749
  }
184550
184750
  if (protectedTagIds.has(pendingOp.tagId)) {
@@ -184552,33 +184752,46 @@ function applyPendingOperations(sessionId, db, targets, protectedTags = 0, prelo
184552
184752
  }
184553
184753
  const target = targets.get(pendingOp.tagId);
184554
184754
  const isToolTag = tagTypeById.get(pendingOp.tagId) === "tool";
184755
+ if (synthetic) {
184756
+ if (!isToolTag || target?.canDrop?.() !== true)
184757
+ continue;
184758
+ }
184759
+ let shouldPersistDrop = false;
184555
184760
  if (isToolTag) {
184556
184761
  if (skeletonWindow.has(pendingOp.tagId)) {
184557
184762
  const truncResult = target?.truncate?.() ?? "absent";
184558
- if (truncResult === "incomplete") {
184763
+ if (truncResult === "incomplete" || synthetic && truncResult !== "truncated") {
184559
184764
  continue;
184560
184765
  }
184561
184766
  if (truncResult === "truncated") {
184562
184767
  didMutateMessage = true;
184563
184768
  }
184564
184769
  updateTagDropMode(db, sessionId, pendingOp.tagId, "truncated");
184770
+ shouldPersistDrop = true;
184565
184771
  } else {
184566
184772
  const dropResult = target?.drop?.() ?? "absent";
184567
- if (dropResult === "incomplete") {
184773
+ if (dropResult === "incomplete" || synthetic && dropResult !== "removed") {
184568
184774
  continue;
184569
184775
  }
184570
184776
  if (dropResult === "removed") {
184571
184777
  didMutateMessage = true;
184572
184778
  }
184573
184779
  updateTagDropMode(db, sessionId, pendingOp.tagId, "full");
184780
+ shouldPersistDrop = true;
184574
184781
  }
184575
184782
  } else if (target) {
184576
- const changed = target.setContent(buildReplacementContent(pendingOp.tagId, target));
184783
+ const changed = target.setContent(buildReplacementContent(pendingOp.tagId));
184577
184784
  if (changed)
184578
184785
  didMutateMessage = true;
184786
+ shouldPersistDrop = true;
184787
+ } else if (!synthetic) {
184788
+ shouldPersistDrop = true;
184579
184789
  }
184790
+ if (!shouldPersistDrop)
184791
+ continue;
184580
184792
  updateTagStatus(db, sessionId, pendingOp.tagId, "dropped");
184581
- removePendingOp(db, sessionId, pendingOp.tagId);
184793
+ if (!synthetic)
184794
+ removePendingOp(db, sessionId, pendingOp.tagId);
184582
184795
  }
184583
184796
  })();
184584
184797
  return didMutateMessage;
@@ -184602,7 +184815,7 @@ function applyFlushedStatuses(sessionId, db, targets, preloadedTags) {
184602
184815
  }
184603
184816
  }
184604
184817
  } else if (target) {
184605
- const changed = target.setContent(buildReplacementContent(tag.tagNumber, target));
184818
+ const changed = target.setContent(buildReplacementContent(tag.tagNumber));
184606
184819
  if (changed)
184607
184820
  didMutateMessage = true;
184608
184821
  }
@@ -185119,7 +185332,7 @@ function tagMessages(sessionId, messages, tagger, db, options = {}) {
185119
185332
  logTransformTiming(sessionId, "tag.saveSource", performance.now() - accSaveSource);
185120
185333
  for (const [compositeKey, tagId] of toolTagByCallId) {
185121
185334
  const thinkingParts = toolThinkingByCallId.get(compositeKey) ?? [];
185122
- targets.set(tagId, createToolDropTarget(compositeKey, thinkingParts, toolCallIndex, batch));
185335
+ targets.set(tagId, createToolDropTarget(compositeKey, thinkingParts, toolCallIndex, batch, tagId));
185123
185336
  }
185124
185337
  const hasRecentReduceCall = lastReduceMessageIndex >= 0 && messages.length - lastReduceMessageIndex <= RECENT_REDUCE_LOOKBACK;
185125
185338
  return {
@@ -186227,6 +186440,51 @@ function planEmergencyDrop(input) {
186227
186440
  };
186228
186441
  }
186229
186442
 
186443
+ // src/hooks/magic-context/system-injection-stripper.ts
186444
+ var SYSTEM_INJECTION_MARKERS = [
186445
+ "<!-- OMO_INTERNAL_INITIATOR -->",
186446
+ "[SYSTEM DIRECTIVE: MAGIC-CONTEXT",
186447
+ "[SYSTEM DIRECTIVE: OH-MY-OPENCODE",
186448
+ "[Category+Skill Reminder]",
186449
+ "[EDIT ERROR - IMMEDIATE ACTION REQUIRED]",
186450
+ "[task CALL FAILED - IMMEDIATE RETRY REQUIRED]",
186451
+ "[EMERGENCY CONTEXT WINDOW WARNING]",
186452
+ "Unstable background agent appears idle",
186453
+ "**THE SUBAGENT JUST CLAIMED THIS TASK IS DONE."
186454
+ ];
186455
+ var SYSTEM_REMINDER_REGEX = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
186456
+ var OMO_MARKER_REGEX = /<!-- OMO_INTERNAL_INITIATOR -->/g;
186457
+ function stripSystemInjection(text) {
186458
+ let hasInjection = false;
186459
+ for (const marker of SYSTEM_INJECTION_MARKERS) {
186460
+ if (text.includes(marker)) {
186461
+ hasInjection = true;
186462
+ break;
186463
+ }
186464
+ }
186465
+ if (SYSTEM_REMINDER_REGEX.test(text))
186466
+ hasInjection = true;
186467
+ SYSTEM_REMINDER_REGEX.lastIndex = 0;
186468
+ if (!hasInjection)
186469
+ return null;
186470
+ let cleaned = text;
186471
+ cleaned = cleaned.replace(SYSTEM_REMINDER_REGEX, "");
186472
+ cleaned = cleaned.replace(OMO_MARKER_REGEX, "");
186473
+ cleaned = cleaned.replace(/\[SYSTEM DIRECTIVE: OH-MY-(?:OPENCODE|CLAUDE)[^\]]*\][\s\S]*?(?=\n\n(?!\s*[-*])|$)/g, "");
186474
+ for (const marker of SYSTEM_INJECTION_MARKERS) {
186475
+ if (marker.startsWith("<!-- ") || marker.startsWith("[SYSTEM DIRECTIVE"))
186476
+ continue;
186477
+ const idx = cleaned.indexOf(marker);
186478
+ if (idx === -1)
186479
+ continue;
186480
+ const blockEnd = cleaned.indexOf(`
186481
+
186482
+ `, idx + marker.length);
186483
+ cleaned = blockEnd !== -1 ? cleaned.slice(0, idx) + cleaned.slice(blockEnd) : cleaned.slice(0, idx);
186484
+ }
186485
+ return cleaned.trim();
186486
+ }
186487
+
186230
186488
  // src/hooks/magic-context/heuristic-cleanup.ts
186231
186489
  init_tag_part_guards();
186232
186490
  var DEDUP_SAFE_TOOLS = new Set([
@@ -186354,7 +186612,9 @@ function applyHeuristicCleanup(sessionId, db, targets, messageTagNumbers, config
186354
186612
  continue;
186355
186613
  updateTagDropMode(db, sessionId, tag.tagNumber, "full");
186356
186614
  updateTagStatus(db, sessionId, tag.tagNumber, "dropped");
186357
- deduplicatedTools++;
186615
+ if (result === "removed" || result === "truncated") {
186616
+ deduplicatedTools++;
186617
+ }
186358
186618
  }
186359
186619
  }
186360
186620
  })();
@@ -186363,6 +186623,7 @@ function applyHeuristicCleanup(sessionId, db, targets, messageTagNumbers, config
186363
186623
  sessionLog(sessionId, `heuristic cleanup: dropped ${droppedTools} tool tags, deduplicated ${deduplicatedTools} tool calls, dropped ${droppedInjections} system injections`);
186364
186624
  }
186365
186625
  let compressedTextTags = 0;
186626
+ let mutatedTextTags = 0;
186366
186627
  if (config2.caveman?.enabled) {
186367
186628
  const cavemanResult = applyCavemanCleanup(sessionId, db, targets, tags, {
186368
186629
  enabled: true,
@@ -186370,8 +186631,15 @@ function applyHeuristicCleanup(sessionId, db, targets, messageTagNumbers, config
186370
186631
  protectedTags: config2.protectedTags
186371
186632
  });
186372
186633
  compressedTextTags = cavemanResult.compressedToLite + cavemanResult.compressedToFull + cavemanResult.compressedToUltra;
186634
+ mutatedTextTags = cavemanResult.mutatedTextTags;
186373
186635
  }
186374
- return { droppedTools, deduplicatedTools, droppedInjections, compressedTextTags };
186636
+ return {
186637
+ droppedTools,
186638
+ deduplicatedTools,
186639
+ droppedInjections,
186640
+ compressedTextTags,
186641
+ mutatedTextTags
186642
+ };
186375
186643
  }
186376
186644
  function extractToolInfo(part) {
186377
186645
  if (part.type === "tool" && typeof part.tool === "string" && DEDUP_SAFE_TOOLS.has(part.tool)) {
@@ -186545,6 +186813,42 @@ function isTodoItem(value) {
186545
186813
  return typeof todo.content === "string" && typeof todo.status === "string" && (todo.priority === undefined || typeof todo.priority === "string");
186546
186814
  }
186547
186815
 
186816
+ // src/hooks/magic-context/tool-reclaim.ts
186817
+ await init_storage();
186818
+ function buildSyntheticToolReclaimOps(input) {
186819
+ const watermark = Math.max(0, input.watermark);
186820
+ if (watermark <= 0)
186821
+ return [];
186822
+ const realPendingTagIds = new Set((input.pendingOps ?? []).map((op) => op.tagId));
186823
+ const tags = getActiveTagsBySession(input.db, input.sessionId);
186824
+ const synthetic = [];
186825
+ for (const tag of tags) {
186826
+ if (tag.type !== "tool")
186827
+ continue;
186828
+ if (tag.status !== "active")
186829
+ continue;
186830
+ if (tag.tagNumber > watermark)
186831
+ continue;
186832
+ if (realPendingTagIds.has(tag.tagNumber))
186833
+ continue;
186834
+ if (input.targets.get(tag.tagNumber)?.canDrop?.() !== true)
186835
+ continue;
186836
+ synthetic.push({
186837
+ id: 0,
186838
+ sessionId: input.sessionId,
186839
+ tagId: tag.tagNumber,
186840
+ operation: "drop",
186841
+ queuedAt: 0
186842
+ });
186843
+ }
186844
+ return synthetic;
186845
+ }
186846
+ function advanceToolReclaimWatermarkToCurrentMax(db, sessionId) {
186847
+ const maxTagNumber = getMaxTagNumberBySession(db, sessionId);
186848
+ advanceToolReclaimWatermark(db, sessionId, maxTagNumber);
186849
+ return maxTagNumber;
186850
+ }
186851
+
186548
186852
  // src/hooks/magic-context/transform-postprocess-phase.ts
186549
186853
  var DEGRADE_CACHE_WARNING_THRESHOLD = 10;
186550
186854
  var degradedCacheCountBySession = new BoundedSessionMap(100);
@@ -186599,12 +186903,14 @@ async function runPostTransformPhase(args) {
186599
186903
  let deferredMaterializedSuccessfully = false;
186600
186904
  let heuristicsRanSuccessfully = false;
186601
186905
  let pendingOpsRanSuccessfully = false;
186906
+ let pendingOpsDidMutate = false;
186907
+ let heuristicOrReasoningDidMutate = false;
186602
186908
  try {
186603
186909
  if (shouldApplyPendingOps) {
186604
186910
  const applyReason = isExplicitFlush ? "explicit_flush" : deferredMaterialize ? "deferred_materialization" : `scheduler_execute (scheduler=${args.schedulerDecision})`;
186605
186911
  sessionLog(args.sessionId, `pending ops WILL APPLY — reason=${applyReason}, pendingOps=${pendingOps.length}, context=${args.contextUsage.percentage.toFixed(1)}%`);
186606
186912
  const tApply = performance.now();
186607
- applyPendingOperations(args.sessionId, args.db, args.targets, args.protectedTags, undefined, pendingOps);
186913
+ pendingOpsDidMutate = applyPendingOperations(args.sessionId, args.db, args.targets, args.protectedTags, undefined, pendingOps);
186608
186914
  logTransformTiming(args.sessionId, "applyPendingOperations", tApply);
186609
186915
  }
186610
186916
  if (shouldRunHeuristics) {
@@ -186622,7 +186928,8 @@ async function runPostTransformPhase(args) {
186622
186928
  } : undefined,
186623
186929
  caveman: cavemanConfig
186624
186930
  }, heuristicTags);
186625
- logTransformTiming(args.sessionId, "applyHeuristicCleanup", t5, `droppedTools=${cleanup.droppedTools} deduplicatedTools=${cleanup.deduplicatedTools} droppedInjections=${cleanup.droppedInjections} compressedTextTags=${cleanup.compressedTextTags}`);
186931
+ logTransformTiming(args.sessionId, "applyHeuristicCleanup", t5, `droppedTools=${cleanup.droppedTools} deduplicatedTools=${cleanup.deduplicatedTools} droppedInjections=${cleanup.droppedInjections} compressedTextTags=${cleanup.compressedTextTags} mutatedTextTags=${cleanup.mutatedTextTags}`);
186932
+ const heuristicMutationCount = cleanup.droppedTools + cleanup.deduplicatedTools + cleanup.droppedInjections + cleanup.mutatedTextTags;
186626
186933
  const t7 = performance.now();
186627
186934
  const clearedReasoning = clearOldReasoning(args.messages, args.reasoningByMessage, args.messageTagNumbers, args.clearReasoningAge);
186628
186935
  if (canUseEmptySentinels) {
@@ -186648,6 +186955,7 @@ async function runPostTransformPhase(args) {
186648
186955
  }
186649
186956
  }
186650
186957
  logTransformTiming(args.sessionId, "clearOldReasoning", t7);
186958
+ heuristicOrReasoningDidMutate = heuristicMutationCount + clearedReasoning + strippedInline > 0;
186651
186959
  if (pendingMaterializationAtPassStart) {
186652
186960
  args.pendingMaterializationSessions.delete(args.sessionId);
186653
186961
  }
@@ -186658,7 +186966,31 @@ async function runPostTransformPhase(args) {
186658
186966
  if (args.schedulerDecision === "execute" && !materializationRequested) {
186659
186967
  updateSessionMeta(args.db, args.sessionId, { lastResponseTime: Date.now() });
186660
186968
  }
186969
+ const toolReclaimExecutePass = args.schedulerDecision === "execute";
186970
+ const alreadyMutatingThisPass = pendingOpsDidMutate || heuristicOrReasoningDidMutate;
186971
+ let autoReclaimTargetCount = 0;
186972
+ let autoReclaimDidMutate = false;
186973
+ if (toolReclaimExecutePass && alreadyMutatingThisPass && !emergencyDropEligible) {
186974
+ const syntheticPendingOps = buildSyntheticToolReclaimOps({
186975
+ db: args.db,
186976
+ sessionId: args.sessionId,
186977
+ targets: args.targets,
186978
+ watermark: args.sessionMeta.toolReclaimWatermark ?? 0,
186979
+ pendingOps
186980
+ });
186981
+ autoReclaimTargetCount = syntheticPendingOps.length;
186982
+ if (syntheticPendingOps.length > 0) {
186983
+ autoReclaimDidMutate = applyPendingOperations(args.sessionId, args.db, args.targets, args.protectedTags, undefined, [], syntheticPendingOps);
186984
+ }
186985
+ }
186661
186986
  args.batch?.finalize();
186987
+ if (toolReclaimExecutePass) {
186988
+ const maxTagNumber = advanceToolReclaimWatermarkToCurrentMax(args.db, args.sessionId);
186989
+ args.sessionMeta.toolReclaimWatermark = Math.max(args.sessionMeta.toolReclaimWatermark ?? 0, maxTagNumber);
186990
+ }
186991
+ if (autoReclaimTargetCount > 0) {
186992
+ sessionLog(args.sessionId, `tool reclaim auto-drop: targets=${autoReclaimTargetCount} mutated=${autoReclaimDidMutate}`);
186993
+ }
186662
186994
  logTransformTiming(args.sessionId, "batchFinalize:heuristics", performance.now());
186663
186995
  if (args.sessionMeta.lastTransformError !== null) {
186664
186996
  updateSessionMeta(args.db, args.sessionId, { lastTransformError: null });
@@ -186670,11 +187002,6 @@ async function runPostTransformPhase(args) {
186670
187002
  deferredMaterializedSuccessfully = true;
186671
187003
  heuristicsRanSuccessfully = true;
186672
187004
  }
186673
- if (args.watermark > 0) {
186674
- const tWatermarkCleanup = performance.now();
186675
- truncateErroredTools(args.messages, args.watermark, args.messageTagNumbers);
186676
- logTransformTiming(args.sessionId, "watermarkCleanup", tWatermarkCleanup);
186677
- }
186678
187005
  if (shouldApplyPendingOps) {
186679
187006
  pendingOpsRanSuccessfully = true;
186680
187007
  }
@@ -187701,6 +188028,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
187701
188028
  const executeThresholdTokens = Math.round((resolvedContextLimit ?? 0) * resolvedExecuteThresholdPct / 100);
187702
188029
  const usableTokens = Math.max(0, executeThresholdTokens - contextUsage.inputTokens + liveTailTokens);
187703
188030
  resetLastNudgeCycleIfTailShrank(db, sessionId, tailToolTokens);
188031
+ const oldestReclaimableToolTags = getOldestActiveUnprotectedToolTags(db, sessionId, deps.protectedTags);
187704
188032
  deps.channel1StateBySession.set(sessionId, {
187705
188033
  tailToolTokens,
187706
188034
  historyBudgetTokens: historyBudgetTokens ?? 0,
@@ -187709,9 +188037,10 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
187709
188037
  lastInputTokens: contextUsage.inputTokens,
187710
188038
  turnToolTokens: 0,
187711
188039
  usableTokens,
187712
- reducedSinceRefresh: false
188040
+ reducedSinceRefresh: false,
188041
+ oldestReclaimableToolTags
187713
188042
  });
187714
- const channel2MetricsKnown = fullFeatureMode && resolvedContextLimit !== undefined && resolvedContextLimit > 0 && resolvedExecuteThresholdPct > 0;
188043
+ const channel2MetricsKnown = resolvedContextLimit !== undefined && resolvedContextLimit > 0 && resolvedExecuteThresholdPct > 0;
187715
188044
  if (channel2MetricsKnown) {
187716
188045
  const channel2ShouldTrigger = shouldTriggerChannel2({
187717
188046
  reclaimableTokens: tailToolTokens,
@@ -187735,6 +188064,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
187735
188064
  }
187736
188065
  const elapsed = (performance.now() - startTime).toFixed(1);
187737
188066
  sessionLog(sessionId, `transform completed in ${elapsed}ms (${messages.length} messages, ${targets.size} targets, watermark: ${watermark})`);
188067
+ deps.maybeAutoEmbedSession?.(sessionId);
187738
188068
  };
187739
188069
  }
187740
188070
  function resolveHistoryBudgetTokens(historyBudgetPercentage, contextUsage, executeThresholdPercentage, modelKey, executeThresholdTokens, resolvedContextLimit) {
@@ -187780,18 +188110,14 @@ function evictExpiredUsageEntries(contextUsageMap) {
187780
188110
  }
187781
188111
  async function deliverChannel2IfPending(deps, sessionId) {
187782
188112
  try {
187783
- try {
187784
- const meta3 = getOrCreateSessionMeta(deps.db, sessionId);
187785
- if (meta3.isSubagent)
187786
- return;
187787
- } catch {}
187788
188113
  const baseline = deps.channel1StateBySession?.get(sessionId);
187789
188114
  await maybeDeliverChannel2(sessionId, {
187790
188115
  db: deps.db,
187791
188116
  serverUrl: deps.serverUrl,
187792
188117
  directory: deps.directory ?? ".",
187793
188118
  reclaimableTokens: baseline ? baseline.tailToolTokens + baseline.turnToolTokens : undefined,
187794
- usableTokens: baseline?.usableTokens
188119
+ usableTokens: baseline?.usableTokens,
188120
+ oldestReclaimableToolTags: baseline?.oldestReclaimableToolTags
187795
188121
  });
187796
188122
  } catch (error51) {
187797
188123
  sessionLog(sessionId, "channel2 delivery wrapper failed (ignored):", error51);
@@ -188097,6 +188423,46 @@ function createEventHandler2(deps) {
188097
188423
  };
188098
188424
  }
188099
188425
 
188426
+ // src/hooks/magic-context/format-embed-status.ts
188427
+ function formatEmbedStatusText(coverage, drain) {
188428
+ if (!coverage.enabled) {
188429
+ return "Embedding is off (no provider configured).";
188430
+ }
188431
+ const lines = [];
188432
+ lines.push(`Embedding — model: ${coverage.model} (${coverage.provider})`);
188433
+ lines.push(`This session: ${coverage.session.embedded} / ${coverage.session.total} compartments embedded`);
188434
+ lines.push(`Project memories: ${coverage.memories.embedded} / ${coverage.memories.total} embedded`);
188435
+ if (coverage.commits.gitEnabled) {
188436
+ lines.push(`Git commits: ${coverage.commits.embedded} / ${coverage.commits.total}`);
188437
+ } else {
188438
+ lines.push("Git commits: 0 / 0 (git indexing off)");
188439
+ }
188440
+ let drainLine = "Drain: idle";
188441
+ switch (drain.status) {
188442
+ case "running": {
188443
+ const e = drain.embedded ?? coverage.session.embedded;
188444
+ const t = drain.total ?? coverage.session.total;
188445
+ const failedSuffix = drain.failed && drain.failed > 0 ? ` (${drain.failed} failed)` : "";
188446
+ drainLine = `Drain: running ${e}/${t}${failedSuffix}`;
188447
+ break;
188448
+ }
188449
+ case "paused": {
188450
+ const e = drain.embedded ?? coverage.session.embedded;
188451
+ const t = drain.total ?? coverage.session.total;
188452
+ drainLine = `Drain: paused ${e}/${t}`;
188453
+ break;
188454
+ }
188455
+ case "stopped":
188456
+ drainLine = "Drain: stopped (provider down)";
188457
+ break;
188458
+ default:
188459
+ drainLine = "Drain: idle";
188460
+ }
188461
+ lines.push(drainLine);
188462
+ return lines.join(`
188463
+ `);
188464
+ }
188465
+
188100
188466
  // src/hooks/magic-context/hook.ts
188101
188467
  await __promiseAll([
188102
188468
  init_inject_compartments(),
@@ -188338,7 +188704,7 @@ function maybeInjectChannel1Nudge(args, sessionId, tool, output) {
188338
188704
  setLastNudgeLevel(args.db, sessionId, decision.nextLastNudgeLevel);
188339
188705
  if (!decision.fire)
188340
188706
  return;
188341
- out.output += buildChannel1Reminder(decision.level, decision.undroppedTokens);
188707
+ out.output += buildChannel1Reminder(decision.level, decision.undroppedTokens, state.oldestReclaimableToolTags);
188342
188708
  sessionLog(sessionId, `channel1 nudge fired: level=${decision.level} undropped~${Math.round(decision.undroppedTokens / 1000)}k tool=${tool}`);
188343
188709
  }
188344
188710
  function createToolExecuteAfterHook(args) {
@@ -188410,9 +188776,7 @@ Context is managed for you entirely automatically — there's nothing to prune a
188410
188776
  var CTX_NOTE_GUIDANCE = `Use \`ctx_note\` ONLY for genuinely future concerns — something to revisit much later, not work coming up in the next few turns (that's already in your active context) and not active multi-step work (use todos for that). Magic Context preserves your full context across both compaction and restarts, so an upcoming restart or "let's come back to this later" is never a reason to take a note — nothing is lost either way. Notes you do take survive compression and resurface at natural work boundaries (after commits, historian runs, todo completion).`;
188411
188777
  var TOOL_HISTORY_GUIDANCE = `Compressed history intentionally omits tool calls and their outputs — summaries like "I edited file X" are historian records, not patterns to replicate. In the live conversation, older tool calls and their results are cleaned up to save context — you may see your own past messages referencing actions without the corresponding tool call or result visible. This is normal context management. ALWAYS use real tool calls; never simulate, fabricate, or inline tool outputs in your text. If there is no tool result message, the action did not happen. NEVER simulate, hallucinate or claim tool calls, command output, search results, file edits, or diffs in plain text as if they actually occurred.`;
188412
188778
  var BASE_INTRO = (protectedTags) => `Messages and tool outputs are tagged with §N§ identifiers (e.g., §1§, §42§).
188413
- Use \`ctx_reduce\` to manage context size. It supports one operation:
188414
- - \`drop\`: Remove entirely (best for tool outputs you already acted on).
188415
- Syntax: "3-5", "1,2,9", or "1-5,8,12-15". Last ${protectedTags} tags are protected.
188779
+ Use \`ctx_reduce\` to mark spent tagged content as discardable and reclaim space. Marking is NOT an immediate delete — it queues the content, which stays fully visible until space is actually needed (as soon as the next turn if you're already under pressure, much later if not), so mark a tool output as soon as you're done with it rather than hoarding the call for the end of the turn. The last ${protectedTags} tags are protected (marking one just queues it until it ages out). Syntax: "3-5", "1,2,9", or "1-5,8,12-15".
188416
188780
  Do not announce or narrate \`ctx_reduce\` drops — just call the tool silently. Saying "I'll drop these outputs" wastes tokens the user does not care about.
188417
188781
  ${CTX_NOTE_GUIDANCE}
188418
188782
  Use \`ctx_memory\` for durable project knowledge: write what future sessions must know, update/archive/merge the memories you see in \`<project-memory>\` when they drift. Memories persist across sessions and every new session starts with them.
@@ -188431,7 +188795,7 @@ Use \`ctx_expand\` to recover the raw conversation behind a \`<compartment>\` su
188431
188795
  \`ctx_search\` returns ranked results from memories, git commits, and raw message history. Use message ordinals from results with \`ctx_expand\` to retrieve surrounding conversation context.
188432
188796
  ${TOOL_HISTORY_GUIDANCE}
188433
188797
  NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
188434
- NEVER drop user messagesthey are short and will be summarized by compartmentalization automatically. Dropping them loses context the historian needs.
188798
+ Keep your user's instructions and intent never drop a user message for its directive, even an old one. But a large block of pasted content inside a user message (logs, data dumps, long code, attachments) is fair to mark discardable once you've extracted what you need — it stays searchable via \`ctx_search\`.
188435
188799
  NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
188436
188800
  Before your turn finishes, consider using \`ctx_reduce\` to drop large tool outputs you no longer need.`;
188437
188801
  var BASE_INTRO_NO_REDUCE = () => `${CTX_NOTE_GUIDANCE}
@@ -188852,29 +189216,55 @@ function createMagicContextHook(deps) {
188852
189216
  ensureProjectRegistered: ensureProjectRegisteredFromOpenCodeDirectory,
188853
189217
  getNotificationParams: (sid) => getLiveNotificationParams(sid, liveModelBySession, variantBySession, agentBySession)
188854
189218
  });
188855
- const executeEmbedHistory = async (sessionId) => {
189219
+ const executeEmbedHistory = async (sessionId, options) => {
188856
189220
  if (deps.config.memory?.enabled === false) {
188857
189221
  return "Memory is disabled for this project, so there is no semantic embedding to backfill.";
188858
189222
  }
188859
189223
  const directory = sessionDirectoryBySession.get(sessionId) ?? deps.directory;
189224
+ const active = embedRunStateBySession.get(sessionId);
189225
+ if (active && !active.signal.aborted && !options?.signal) {
189226
+ return "Embedding is already running for this session.";
189227
+ }
188860
189228
  await ensureProjectRegisteredFromOpenCodeDirectory(directory, db);
188861
189229
  const sessionProjectIdentity = resolveProjectIdentity(directory);
188862
- setRecompStarting({ recompProgressBySession }, sessionId, "Embedding history…", "embed");
188863
- const outcome = await embedSessionCompartmentChunks(db, sessionProjectIdentity, sessionId, {
188864
- onProgress: ({ embedded, total }) => {
188865
- const cur = recompProgressBySession.get(sessionId);
188866
- if (!cur || cur.phase !== "recomp")
188867
- return;
188868
- recompProgressBySession.set(sessionId, {
188869
- ...cur,
188870
- processedMessages: embedded,
188871
- totalMessages: total,
188872
- updatedAt: Date.now()
188873
- });
189230
+ embedPauseBySession.delete(sessionId);
189231
+ const prior = embedRunStateBySession.get(sessionId);
189232
+ if (prior)
189233
+ prior.abort();
189234
+ const controller = new AbortController;
189235
+ embedRunStateBySession.set(sessionId, controller);
189236
+ const signal = options?.signal ?? controller.signal;
189237
+ if (!options?.silent) {
189238
+ setRecompStarting({ recompProgressBySession }, sessionId, "Embedding history…", "embed");
189239
+ }
189240
+ let runFailed = 0;
189241
+ let outcome;
189242
+ try {
189243
+ outcome = await embedSessionCompartmentChunks(db, sessionProjectIdentity, sessionId, {
189244
+ signal,
189245
+ onProgress: ({ embedded, total }) => {
189246
+ const cur = recompProgressBySession.get(sessionId);
189247
+ if (!cur || cur.phase !== "recomp")
189248
+ return;
189249
+ recompProgressBySession.set(sessionId, {
189250
+ ...cur,
189251
+ processedMessages: embedded,
189252
+ totalMessages: total,
189253
+ updatedAt: Date.now()
189254
+ });
189255
+ }
189256
+ });
189257
+ } finally {
189258
+ if (embedRunStateBySession.get(sessionId) === controller) {
189259
+ embedRunStateBySession.delete(sessionId);
188874
189260
  }
188875
- });
189261
+ }
189262
+ if ("failed" in outcome)
189263
+ runFailed = outcome.failed;
188876
189264
  const terminal = (phase, message) => {
188877
- setRecompTerminal({ recompProgressBySession }, sessionId, phase, message);
189265
+ if (!options?.silent) {
189266
+ setRecompTerminal({ recompProgressBySession }, sessionId, phase, message);
189267
+ }
188878
189268
  return message;
188879
189269
  };
188880
189270
  switch (outcome.status) {
@@ -188883,15 +189273,78 @@ function createMagicContextHook(deps) {
188883
189273
  case "disabled":
188884
189274
  return terminal("skipped", "No embedding provider is configured, so there is nothing to embed.");
188885
189275
  case "busy":
188886
- return terminal("skipped", `Embedding is already running for this project — ${outcome.total} compartment${outcome.total === 1 ? "" : "s"} still pending. Try again shortly.`);
188887
- case "aborted":
188888
- return terminal("done", `Embedded ${outcome.embedded} of ${outcome.total} compartments before stopping.`);
189276
+ return terminal("skipped", "Embedding is already running for this project. Try again shortly.");
189277
+ case "aborted": {
189278
+ const cov = getEmbeddingCoverageStatus(db, sessionProjectIdentity, sessionId);
189279
+ const msg = `Paused at ${cov.session.embedded}/${cov.session.total} compartments embedded.`;
189280
+ return terminal("skipped", msg);
189281
+ }
188889
189282
  case "stalled":
188890
- return terminal("skipped", `Embedded ${outcome.embedded} compartments; ${outcome.remaining} could not be embedded (the provider returned no result). Run /ctx-embed-history again to retry them.`);
189283
+ return terminal("skipped", `Embedded ${outcome.embedded} compartments; ${outcome.remaining} could not be embedded (the provider returned no result). Run /ctx-embed start again to retry them.`);
188891
189284
  default:
188892
- return terminal("done", `Embedded ${outcome.embedded} compartment${outcome.embedded === 1 ? "" : "s"} of history for semantic search.`);
189285
+ return terminal("done", `Embedded ${outcome.embedded} compartment${outcome.embedded === 1 ? "" : "s"} of history for semantic search${runFailed > 0 ? ` (${runFailed} failed)` : ""}.`);
188893
189286
  }
188894
189287
  };
189288
+ const pauseEmbedDrain = (sessionId) => {
189289
+ embedPauseBySession.add(sessionId);
189290
+ const ctrl = embedRunStateBySession.get(sessionId);
189291
+ if (ctrl)
189292
+ ctrl.abort();
189293
+ const directory = sessionDirectoryBySession.get(sessionId) ?? deps.directory;
189294
+ const sessionProjectIdentity = resolveProjectIdentity(directory);
189295
+ const cov = getEmbeddingCoverageStatus(db, sessionProjectIdentity, sessionId);
189296
+ return `Paused at ${cov.session.embedded}/${cov.session.total} compartments embedded.`;
189297
+ };
189298
+ const getEmbedStatusText = (sessionId) => {
189299
+ const directory = sessionDirectoryBySession.get(sessionId) ?? deps.directory;
189300
+ const sessionProjectIdentity = resolveProjectIdentity(directory);
189301
+ const coverage = getEmbeddingCoverageStatus(db, sessionProjectIdentity, sessionId);
189302
+ const progress = recompProgressBySession.get(sessionId);
189303
+ const drainUi = getEmbedDrainUiStatus(sessionId, progress);
189304
+ return formatEmbedStatusText(coverage, {
189305
+ status: drainUi.status,
189306
+ embedded: progress?.processedMessages,
189307
+ total: progress?.totalMessages
189308
+ });
189309
+ };
189310
+ const maybeAutoEmbedSession = (sessionId) => {
189311
+ if (autoEmbedAttemptedBySession.has(sessionId))
189312
+ return;
189313
+ if (embedPauseBySession.has(sessionId))
189314
+ return;
189315
+ if (deps.config.memory?.enabled === false)
189316
+ return;
189317
+ autoEmbedAttemptedBySession.add(sessionId);
189318
+ const directory = sessionDirectoryBySession.get(sessionId) ?? deps.directory;
189319
+ (async () => {
189320
+ try {
189321
+ await new Promise((resolve6) => setTimeout(resolve6, 0));
189322
+ await ensureProjectRegisteredFromOpenCodeDirectory(directory, db);
189323
+ const sessionProjectIdentity = resolveProjectIdentity(directory);
189324
+ const coverage = getEmbeddingCoverageStatus(db, sessionProjectIdentity, sessionId);
189325
+ if (!coverage.enabled)
189326
+ return;
189327
+ const remaining = coverage.session.total - coverage.session.embedded;
189328
+ if (remaining <= 0)
189329
+ return;
189330
+ const notifyParams = getLiveNotificationParams(sessionId, liveModelBySession, variantBySession, agentBySession);
189331
+ if (!isTuiConnected(sessionId)) {
189332
+ const startMsg = `Embedding ${remaining} compartment${remaining === 1 ? "" : "s"} of history in the background…`;
189333
+ await sendIgnoredMessage(deps.client, sessionId, startMsg, {
189334
+ ...notifyParams
189335
+ });
189336
+ }
189337
+ const summary = await executeEmbedHistory(sessionId);
189338
+ if (!isTuiConnected(sessionId)) {
189339
+ await sendIgnoredMessage(deps.client, sessionId, summary, {
189340
+ ...notifyParams
189341
+ });
189342
+ }
189343
+ } catch (error51) {
189344
+ log("[magic-context] auto-embed drain failed:", error51);
189345
+ }
189346
+ })();
189347
+ };
188895
189348
  const sidekickRunnable = isSidekickRunnable(deps.config);
188896
189349
  const sidekickConfig = sidekickRunnable ? deps.config.sidekick : undefined;
188897
189350
  const transform2 = createTransform({
@@ -188953,7 +189406,8 @@ function createMagicContextHook(deps) {
188953
189406
  cavemanTextCompression: ctxReduceEnabled === false && deps.config.caveman_text_compression?.enabled === true ? {
188954
189407
  enabled: true,
188955
189408
  minChars: deps.config.caveman_text_compression.min_chars ?? 500
188956
- } : undefined
189409
+ } : undefined,
189410
+ maybeAutoEmbedSession
188957
189411
  });
188958
189412
  const eventHandler = createEventHandler2({
188959
189413
  contextUsageMap,
@@ -188982,6 +189436,7 @@ function createMagicContextHook(deps) {
188982
189436
  recompProgressBySession.delete(sessionId);
188983
189437
  internalChildSessions.delete(sessionId);
188984
189438
  channel1StateBySession.delete(sessionId);
189439
+ clearEmbedSessionState(sessionId);
188985
189440
  }
188986
189441
  });
188987
189442
  const runDreamQueueInBackground = () => {
@@ -189047,6 +189502,8 @@ function createMagicContextHook(deps) {
189047
189502
  executeRecomp: historianRunnable ? async (sessionId, options) => runManagedRecomp(buildManagedRecompCtx(sessionId), sessionId, options) : undefined,
189048
189503
  runUpgrade: historianRunnable ? async (sessionId) => runManagedUpgrade(buildManagedRecompCtx(sessionId), sessionId) : undefined,
189049
189504
  executeEmbedHistory,
189505
+ pauseEmbedDrain,
189506
+ getEmbedStatusText,
189050
189507
  sendNotification: async (sessionId, text, params) => {
189051
189508
  await sendIgnoredMessage(deps.client, sessionId, text, {
189052
189509
  ...getLiveNotificationParams(sessionId, liveModelBySession, variantBySession, agentBySession),
@@ -189259,6 +189716,7 @@ function truncateError(name2, code, message, maxLen = 240) {
189259
189716
 
189260
189717
  // src/plugin/rpc-handlers.ts
189261
189718
  init_project_identity();
189719
+ init_project_embedding_registry();
189262
189720
  init_tool_definition_tokens();
189263
189721
  await init_storage();
189264
189722
 
@@ -189979,6 +190437,26 @@ function buildStatusDetail(db, sessionId, directory, modelKey, config2, liveSess
189979
190437
  }
189980
190438
  return detail;
189981
190439
  }
190440
+ function buildEmbedDetail(db, sessionId, dir, liveSessionState) {
190441
+ const projectIdentity = resolveProjectIdentity(dir);
190442
+ const coverage = getEmbeddingCoverageStatus(db, projectIdentity, sessionId);
190443
+ const progress = liveSessionState.recompProgressBySession.get(sessionId);
190444
+ const drainUi = getEmbedDrainUiStatus(sessionId, progress);
190445
+ const statusText = formatEmbedStatusText(coverage, {
190446
+ status: drainUi.status,
190447
+ embedded: progress?.processedMessages,
190448
+ total: progress?.totalMessages
190449
+ });
190450
+ return {
190451
+ enabled: coverage.enabled,
190452
+ model: coverage.model,
190453
+ provider: coverage.provider,
190454
+ session: coverage.session,
190455
+ memories: coverage.memories,
190456
+ commits: coverage.commits,
190457
+ statusText
190458
+ };
190459
+ }
189982
190460
  function registerRpcHandlers(rpcServer, args) {
189983
190461
  const { directory, config: config2, liveSessionState } = args;
189984
190462
  const rawConfig = config2;
@@ -190001,6 +190479,19 @@ function registerRpcHandlers(rpcServer, args) {
190001
190479
  return { error: "unavailable" };
190002
190480
  return buildStatusDetail(db, sessionId, dir, modelKey, rawConfig, liveSessionState, injectionBudgetTokens);
190003
190481
  });
190482
+ rpcServer.handle("embed-detail", async (params) => {
190483
+ const sessionId = String(params.sessionId ?? "");
190484
+ const dir = String(params.directory ?? directory);
190485
+ const db = getDb();
190486
+ if (!db || !sessionId)
190487
+ return { error: "unavailable" };
190488
+ try {
190489
+ return buildEmbedDetail(db, sessionId, dir, liveSessionState);
190490
+ } catch (err) {
190491
+ log("[rpc] embed-detail error:", err);
190492
+ return { error: "unavailable" };
190493
+ }
190494
+ });
190004
190495
  rpcServer.handle("compartment-count", async (params) => {
190005
190496
  const sessionId = String(params.sessionId ?? "");
190006
190497
  const db = getDb();
@@ -190123,27 +190614,225 @@ Older parts of this session are summarized into <compartment> blocks inside <ses
190123
190614
 
190124
190615
  ctx_expand(start=120, end=245) ← the compartment's own start/end attributes
190125
190616
 
190126
- Returns the raw transcript as [N] U:/A: lines, capped at ~15K tokens; an oversized range returns the head and tells you where to continue. Also works with ordinals from ctx_search message results — expand a window around a hit (e.g. start=N-10, end=N+5). Ranges after the last compartment are your live tail — already visible in context, not expandable.`;
190617
+ Returns the raw transcript as [N] U:/A: lines, capped at ~15K tokens; an oversized range returns the head and tells you where to continue. Also works with ordinals from ctx_search message results — expand a window around a hit (e.g. start=N-10, end=N+5). Ranges after the last compartment are your live tail — already visible in context, not expandable.
190618
+
190619
+ Two recovery modes for finer detail:
190620
+ - ctx_expand(start=120, end=245, verbose=true) — lists each message SEPARATELY with its ordinal [N] and a per-part preview (each tool call shown with its output size). Use this to find the exact message or tool call you want, then recover it in full by ordinal.
190621
+ - ctx_expand(message=138) — returns the FULL untruncated content of the message at that ordinal: every text part, and every tool call's complete input + output, read from stored history. This is the cheap way to get back a tool output you dropped with ctx_reduce — the original is still in storage even though the wire shows [dropped §N§]. If the message was deleted from history (session prune/revert), it says so.`;
190127
190622
  var CTX_EXPAND_TOKEN_BUDGET = 15000;
190128
190623
 
190624
+ // src/tools/ctx-expand/render.ts
190625
+ init_read_session_formatting();
190626
+ await init_read_session_chunk();
190627
+ function isRecord3(value) {
190628
+ return value !== null && typeof value === "object" && !Array.isArray(value);
190629
+ }
190630
+ function roleLabel(role) {
190631
+ if (role === "assistant")
190632
+ return "A (assistant)";
190633
+ if (role === "user")
190634
+ return "U (user)";
190635
+ return role;
190636
+ }
190637
+ function truncate2(value, max) {
190638
+ const t = value.trim();
190639
+ return t.length <= max ? t : `${t.slice(0, max)}…`;
190640
+ }
190641
+ function keyArg(input) {
190642
+ if (!input)
190643
+ return "";
190644
+ for (const k of ["filePath", "path", "pattern", "query", "symbol", "module", "action"]) {
190645
+ const v = input[k];
190646
+ if (typeof v === "string" && v.length > 0)
190647
+ return truncate2(v, 60);
190648
+ }
190649
+ if (typeof input.description === "string")
190650
+ return truncate2(input.description, 60);
190651
+ return "";
190652
+ }
190653
+ function asToolPart(part) {
190654
+ const type = typeof part.type === "string" ? part.type : "";
190655
+ if (type === "tool") {
190656
+ const state = isRecord3(part.state) ? part.state : null;
190657
+ const output = state && typeof state.output === "string" ? state.output : state && state.output != null ? JSON.stringify(state.output) : null;
190658
+ const metadata = state && isRecord3(state.metadata) ? state.metadata : null;
190659
+ const title = state && typeof state.title === "string" && state.title || metadata && typeof metadata.title === "string" && metadata.title || null;
190660
+ return {
190661
+ name: typeof part.tool === "string" ? part.tool : "tool",
190662
+ callId: typeof part.callID === "string" ? part.callID : "",
190663
+ title,
190664
+ input: state && isRecord3(state.input) ? state.input : null,
190665
+ output
190666
+ };
190667
+ }
190668
+ if (type === "tool_use") {
190669
+ return {
190670
+ name: typeof part.name === "string" ? part.name : "tool",
190671
+ callId: typeof part.id === "string" ? part.id : "",
190672
+ title: null,
190673
+ input: isRecord3(part.input) ? part.input : null,
190674
+ output: null
190675
+ };
190676
+ }
190677
+ if (type === "tool_result") {
190678
+ const content = part.content;
190679
+ const output = typeof content === "string" ? content : content != null ? JSON.stringify(content) : null;
190680
+ return {
190681
+ name: "tool_result",
190682
+ callId: typeof part.tool_use_id === "string" ? part.tool_use_id : "",
190683
+ title: null,
190684
+ input: null,
190685
+ output
190686
+ };
190687
+ }
190688
+ return null;
190689
+ }
190690
+ function textOf(part) {
190691
+ if (part.type === "text" && typeof part.text === "string")
190692
+ return part.text;
190693
+ return null;
190694
+ }
190695
+ function reasoningOf(part) {
190696
+ if ((part.type === "reasoning" || part.type === "thinking") && typeof part.text === "string") {
190697
+ return part.text;
190698
+ }
190699
+ return null;
190700
+ }
190701
+ function renderPartPreview(part) {
190702
+ if (!isRecord3(part))
190703
+ return null;
190704
+ const text = textOf(part);
190705
+ if (text !== null) {
190706
+ const t = truncate2(text, 200);
190707
+ return t.length > 0 ? ` • ${t}` : null;
190708
+ }
190709
+ const tool = asToolPart(part);
190710
+ if (tool) {
190711
+ const arg = keyArg(tool.input);
190712
+ const head = arg ? `${tool.name}(${arg})` : tool.name;
190713
+ return tool.output !== null ? ` • tool ${head} → output ~${estimateTokens(tool.output)} tok` : ` • tool ${head}`;
190714
+ }
190715
+ const reasoning = reasoningOf(part);
190716
+ if (reasoning !== null)
190717
+ return ` • [reasoning] ${truncate2(reasoning, 120)}`;
190718
+ const type = typeof part.type === "string" ? part.type : "part";
190719
+ if (type === "file")
190720
+ return " • [file]";
190721
+ if (type === "step-start" || type === "step-finish")
190722
+ return null;
190723
+ return ` • [${type}]`;
190724
+ }
190725
+ function renderPartFull(part) {
190726
+ if (!isRecord3(part))
190727
+ return null;
190728
+ const text = textOf(part);
190729
+ if (text !== null) {
190730
+ return text.trim().length > 0 ? ` [text]
190731
+ ${text}` : null;
190732
+ }
190733
+ const tool = asToolPart(part);
190734
+ if (tool) {
190735
+ const lines = [];
190736
+ const idSuffix = tool.callId ? ` #${tool.callId}` : "";
190737
+ lines.push(` [tool: ${tool.name}${idSuffix}]`);
190738
+ if (tool.title && tool.title.trim().length > 0) {
190739
+ lines.push(` description: ${tool.title.trim()}`);
190740
+ }
190741
+ if (tool.input)
190742
+ lines.push(` input: ${JSON.stringify(tool.input)}`);
190743
+ if (tool.output !== null)
190744
+ lines.push(` output:
190745
+ ${tool.output}`);
190746
+ return lines.join(`
190747
+ `);
190748
+ }
190749
+ const type = typeof part.type === "string" ? part.type : "part";
190750
+ if (type === "file") {
190751
+ const name2 = typeof part.filename === "string" && part.filename || typeof part.url === "string" && part.url || "";
190752
+ return ` [file]${name2 ? ` ${name2}` : ""}`;
190753
+ }
190754
+ return null;
190755
+ }
190756
+ function renderMessageByOrdinal(sessionId, ordinal) {
190757
+ const msg = readRawSessionMessages(sessionId).find((m) => m.ordinal === ordinal);
190758
+ if (!msg) {
190759
+ return `No message at ordinal ${ordinal} in this session's stored history — it was deleted ` + `(session prune/revert) or the ordinal is wrong, so it can't be recovered. ` + `Re-run the tool if you still need the data.`;
190760
+ }
190761
+ const rendered = msg.parts.map(renderPartFull).filter((l) => l !== null);
190762
+ const lines = [`[${msg.ordinal}] ${roleLabel(msg.role)} — full recovery:`, ""];
190763
+ if (rendered.length === 0) {
190764
+ lines.push(" (no recoverable content — message had only structural/reasoning parts)");
190765
+ } else {
190766
+ lines.push(...rendered);
190767
+ }
190768
+ return lines.join(`
190769
+ `);
190770
+ }
190771
+ function renderVerboseRange(sessionId, start, end, tokenBudget) {
190772
+ const messages = readRawSessionMessages(sessionId).filter((m) => m.ordinal >= start && m.ordinal <= end);
190773
+ const out = [];
190774
+ let usedTokens = 0;
190775
+ let lastOrdinal = start - 1;
190776
+ let truncated = false;
190777
+ for (const msg of messages) {
190778
+ const header = `[${msg.ordinal}] ${roleLabel(msg.role)}`;
190779
+ const partLines = msg.parts.map(renderPartPreview).filter((l) => l !== null);
190780
+ const block = partLines.length > 0 ? `${header}
190781
+ ${partLines.join(`
190782
+ `)}` : header;
190783
+ const blockTokens = estimateTokens(block);
190784
+ if (usedTokens + blockTokens > tokenBudget && out.length > 0) {
190785
+ truncated = true;
190786
+ break;
190787
+ }
190788
+ out.push(block);
190789
+ usedTokens += blockTokens;
190790
+ lastOrdinal = msg.ordinal;
190791
+ }
190792
+ return { text: out.join(`
190793
+
190794
+ `), lastOrdinal, truncated };
190795
+ }
190796
+
190129
190797
  // src/tools/ctx-expand/tools.ts
190130
190798
  function createCtxExpandTool(deps) {
190131
190799
  return tool({
190132
190800
  description: CTX_EXPAND_DESCRIPTION,
190133
190801
  args: {
190134
- start: tool.schema.number().describe(`First message ordinal to expand — a compartment's start="N" attribute, or an ordinal from a ctx_search message hit`),
190135
- end: tool.schema.number().describe(`Last message ordinal to expand (inclusive) — a compartment's end="M" attribute`)
190802
+ start: tool.schema.number().optional().describe(`First message ordinal to expand — a compartment's start="N" attribute, or an ordinal from a ctx_search message hit`),
190803
+ end: tool.schema.number().optional().describe(`Last message ordinal to expand (inclusive) — a compartment's end="M" attribute`),
190804
+ verbose: tool.schema.boolean().optional().describe("With start/end: list each message separately with its ordinal [N] and per-part preview (each tool call shown with its output size), so you can pick one to recover in full by ordinal."),
190805
+ message: tool.schema.number().optional().describe("Full untruncated recovery of ONE message by its ordinal (every text part + every tool call's complete input/output). Use an ordinal from a compartment, ctx_search hit, or verbose range. Recovers a tool output you dropped with ctx_reduce.")
190136
190806
  },
190137
190807
  async execute(args, toolContext) {
190138
190808
  const sessionId = toolContext.sessionID;
190809
+ if (typeof args.message === "number" && args.message >= 1) {
190810
+ return renderMessageByOrdinal(sessionId, args.message);
190811
+ }
190139
190812
  if (!args.start || !args.end || args.start < 1 || args.end < args.start) {
190140
- return "Error: start and end must be positive integers with start <= end.";
190813
+ return "Error: provide either message=<ordinal>, or start and end (positive integers, start <= end).";
190141
190814
  }
190142
190815
  const lastCompartmentEnd = getLastCompartmentEndMessage(deps.db, sessionId);
190143
190816
  if (lastCompartmentEnd >= 0 && args.start > lastCompartmentEnd) {
190144
190817
  return `Range ${args.start}-${args.end} is entirely within the live tail (after the last compacted message ${lastCompartmentEnd}); those messages are already visible in context.`;
190145
190818
  }
190146
190819
  const effectiveEnd = lastCompartmentEnd >= 0 ? Math.min(args.end, lastCompartmentEnd) : args.end;
190820
+ if (args.verbose === true) {
190821
+ const v = renderVerboseRange(sessionId, args.start, effectiveEnd, CTX_EXPAND_TOKEN_BUDGET);
190822
+ if (!v.text) {
190823
+ return `No messages found in range ${args.start}-${effectiveEnd}. The range may be outside this session's history.`;
190824
+ }
190825
+ const out = [
190826
+ `Messages ${args.start}-${v.lastOrdinal} (verbose). Recover any one in full with ctx_expand(message=<ordinal>):`,
190827
+ "",
190828
+ v.text
190829
+ ];
190830
+ if (v.truncated) {
190831
+ out.push("", `Truncated at message ${v.lastOrdinal} (budget: ~${CTX_EXPAND_TOKEN_BUDGET} tokens). Call again with start=${v.lastOrdinal + 1} end=${effectiveEnd} verbose=true for more.`);
190832
+ }
190833
+ return out.join(`
190834
+ `);
190835
+ }
190147
190836
  const chunk = readSessionChunk(sessionId, CTX_EXPAND_TOKEN_BUDGET, args.start, effectiveEnd + 1);
190148
190837
  if (!chunk.text || chunk.messageCount === 0) {
190149
190838
  return `No messages found in range ${args.start}-${args.end}. The range may be outside this session's history.`;
@@ -190837,15 +191526,16 @@ function createCtxNoteTools(deps) {
190837
191526
  };
190838
191527
  }
190839
191528
  // src/tools/ctx-reduce/constants.ts
190840
- var CTX_REDUCE_DESCRIPTION = `Reduce context by dropping tagged content you no longer need.
190841
- Use §N§ identifiers visible in conversation. The \`drop\` param accepts ranges: "3-5", "1,2,9", "1-5,8".
190842
-
190843
- CRITICAL RULES:
190844
- - NEVER blanket-drop large ranges (e.g., "1-50"). Always review what each tag contains first.
190845
- - Only drop tool outputs you have already processed and no longer need.
190846
- - Protected tags are accepted but deferred until they leave the last protected range.
190847
- - Keep recent context only reduce OLD content that is no longer relevant to current work.
190848
- - Dropped content is gone forever.`;
191529
+ var CTX_REDUCE_DESCRIPTION = `Mark spent tagged content as discardable to reclaim context space. This is NOT an immediate delete. Use §N§ identifiers visible in the conversation. The \`drop\` param accepts ranges: "3-5", "1,2,9", "1-5,8".
191530
+
191531
+ How it works:
191532
+ - Marking QUEUES content for release. It stays fully visible to you until context space is actually needed — which may be as soon as the next turn if you are already under pressure, or many turns later if not. So mark spent outputs as soon as you finish with them; don't hoard the call for the end of the turn.
191533
+ - The newest tags are protected: marking one just queues it until it ages out of the recent window, so marking recent output is harmless.
191534
+ - When content is finally released it becomes a short placeholder, and re-running the tool is the only way to get it back. So mark only what you are genuinely DONE with — the test is "have I extracted what I need from this?", not "is it safe / do I have time before it drops?".
191535
+
191536
+ Mark discardable once processed: large outputs you've summarized, repeated or redundant dumps, data written to disk, status/log output that only confirmed an expected state.
191537
+ Keep: user messages, unresolved errors, raw evidence you haven't extracted yet, and outputs whose exact wording may matter later.
191538
+ Never blanket-mark large ranges (e.g. "1-50") — review what each tag holds first.`;
190849
191539
  // src/tools/ctx-reduce/tools.ts
190850
191540
  import { tool as tool4 } from "@opencode-ai/plugin";
190851
191541
 
@@ -191428,6 +192118,9 @@ class MagicContextRpcServer {
191428
192118
  }
191429
192119
 
191430
192120
  // src/index.ts
192121
+ var HISTORIAN_MAX_STEPS = 40;
192122
+ var SIDEKICK_MAX_STEPS = 40;
192123
+ var DREAMER_MAX_STEPS = 150;
191431
192124
  var plugin = async (ctx) => {
191432
192125
  const pluginConfig = loadPluginConfig(ctx.directory);
191433
192126
  setSqlitePragmaConfig({
@@ -191620,11 +192313,13 @@ var plugin = async (ctx) => {
191620
192313
  await hooks.magicContext?.["experimental.text.complete"]?.(input, output);
191621
192314
  },
191622
192315
  config: async (config2) => {
191623
- const buildHiddenAgentConfig = (agentId, prompt, allowedTools, overrides) => {
192316
+ const buildHiddenAgentConfig = (agentId, prompt, allowedTools, maxSteps, overrides) => {
191624
192317
  const { permission: overridePermission, ...restOverrides } = overrides ?? {};
191625
192318
  const basePermission = buildAllowOnlyPermission(allowedTools);
191626
192319
  return {
191627
192320
  prompt,
192321
+ steps: maxSteps,
192322
+ maxSteps,
191628
192323
  ...getAgentFallbackModels(agentId) ? { fallback_models: getAgentFallbackModels(agentId) } : {},
191629
192324
  ...restOverrides,
191630
192325
  permission: {
@@ -191665,10 +192360,10 @@ var plugin = async (ctx) => {
191665
192360
  })() : undefined;
191666
192361
  config2.agent = {
191667
192362
  ...config2.agent ?? {},
191668
- [DREAMER_AGENT]: buildHiddenAgentConfig(DREAMER_AGENT, DREAMER_SYSTEM_PROMPT, DREAMER_ALLOWED_TOOLS, dreamerAgentOverrides),
191669
- [HISTORIAN_AGENT]: buildHiddenAgentConfig(HISTORIAN_AGENT, COMPARTMENT_AGENT_SYSTEM_PROMPT, HISTORIAN_ALLOWED_TOOLS, historianAgentOverrides),
191670
- [HISTORIAN_EDITOR_AGENT]: buildHiddenAgentConfig(HISTORIAN_EDITOR_AGENT, HISTORIAN_EDITOR_SYSTEM_PROMPT, HISTORIAN_ALLOWED_TOOLS, historianAgentOverrides),
191671
- [SIDEKICK_AGENT]: buildHiddenAgentConfig(SIDEKICK_AGENT, SIDEKICK_SYSTEM_PROMPT, SIDEKICK_ALLOWED_TOOLS, sidekickAgentOverrides)
192363
+ [DREAMER_AGENT]: buildHiddenAgentConfig(DREAMER_AGENT, DREAMER_SYSTEM_PROMPT, DREAMER_ALLOWED_TOOLS, DREAMER_MAX_STEPS, dreamerAgentOverrides),
192364
+ [HISTORIAN_AGENT]: buildHiddenAgentConfig(HISTORIAN_AGENT, COMPARTMENT_AGENT_SYSTEM_PROMPT, HISTORIAN_ALLOWED_TOOLS, HISTORIAN_MAX_STEPS, historianAgentOverrides),
192365
+ [HISTORIAN_EDITOR_AGENT]: buildHiddenAgentConfig(HISTORIAN_EDITOR_AGENT, HISTORIAN_EDITOR_SYSTEM_PROMPT, HISTORIAN_ALLOWED_TOOLS, HISTORIAN_MAX_STEPS, historianAgentOverrides),
192366
+ [SIDEKICK_AGENT]: buildHiddenAgentConfig(SIDEKICK_AGENT, SIDEKICK_SYSTEM_PROMPT, SIDEKICK_ALLOWED_TOOLS, SIDEKICK_MAX_STEPS, sidekickAgentOverrides)
191672
192367
  };
191673
192368
  }
191674
192369
  };