@wolfx/opencode-magic-context 0.24.0 → 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 (100) 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 +18 -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/embedding-openai.d.ts +14 -0
  7. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  8. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts +6 -0
  9. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  10. package/dist/features/magic-context/project-embedding-registry.d.ts +38 -0
  11. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  12. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  13. package/dist/features/magic-context/storage-meta-session.d.ts +1 -0
  14. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  15. package/dist/features/magic-context/storage-meta-shared.d.ts +2 -1
  16. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  17. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  18. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  19. package/dist/features/magic-context/storage-tags.d.ts +20 -1
  20. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  21. package/dist/features/magic-context/storage.d.ts +2 -2
  22. package/dist/features/magic-context/storage.d.ts.map +1 -1
  23. package/dist/features/magic-context/types.d.ts +1 -0
  24. package/dist/features/magic-context/types.d.ts.map +1 -1
  25. package/dist/hooks/magic-context/apply-operations.d.ts +3 -2
  26. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  27. package/dist/hooks/magic-context/caveman-cleanup.d.ts +1 -0
  28. package/dist/hooks/magic-context/caveman-cleanup.d.ts.map +1 -1
  29. package/dist/hooks/magic-context/channel2-delivery.d.ts +2 -0
  30. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  31. package/dist/hooks/magic-context/command-handler.d.ts +7 -5
  32. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  33. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +14 -4
  34. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  35. package/dist/hooks/magic-context/embed-session-state.d.ts +14 -0
  36. package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -0
  37. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  38. package/dist/hooks/magic-context/format-embed-status.d.ts +9 -0
  39. package/dist/hooks/magic-context/format-embed-status.d.ts.map +1 -0
  40. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -0
  41. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  42. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +1 -1
  47. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  48. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/strip-content.d.ts +0 -1
  50. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  51. package/dist/hooks/magic-context/tag-content-primitives.d.ts +2 -0
  52. package/dist/hooks/magic-context/tag-content-primitives.d.ts.map +1 -1
  53. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  54. package/dist/hooks/magic-context/tool-drop-target.d.ts +1 -1
  55. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  56. package/dist/hooks/magic-context/tool-reclaim.d.ts +12 -0
  57. package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -0
  58. package/dist/hooks/magic-context/transform-operations.d.ts +1 -1
  59. package/dist/hooks/magic-context/transform-operations.d.ts.map +1 -1
  60. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/transform.d.ts +2 -0
  62. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +1117 -378
  65. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  66. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  67. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  68. package/dist/shared/announcement.d.ts +1 -1
  69. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  70. package/dist/shared/rpc-types.d.ts +20 -0
  71. package/dist/shared/rpc-types.d.ts.map +1 -1
  72. package/dist/shared/sqlite.d.ts +5 -1
  73. package/dist/shared/sqlite.d.ts.map +1 -1
  74. package/dist/shared/tui-preferences.d.ts +32 -0
  75. package/dist/shared/tui-preferences.d.ts.map +1 -0
  76. package/dist/tools/ctx-expand/constants.d.ts +1 -1
  77. package/dist/tools/ctx-expand/constants.d.ts.map +1 -1
  78. package/dist/tools/ctx-expand/render.d.ts +43 -0
  79. package/dist/tools/ctx-expand/render.d.ts.map +1 -0
  80. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  81. package/dist/tools/ctx-expand/types.d.ts +6 -2
  82. package/dist/tools/ctx-expand/types.d.ts.map +1 -1
  83. package/dist/tools/ctx-reduce/constants.d.ts +1 -1
  84. package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
  85. package/dist/tui/data/context-db.d.ts +4 -2
  86. package/dist/tui/data/context-db.d.ts.map +1 -1
  87. package/package.json +1 -1
  88. package/src/shared/announcement.ts +6 -6
  89. package/src/shared/model-suggestion-retry.test.ts +61 -1
  90. package/src/shared/model-suggestion-retry.ts +22 -0
  91. package/src/shared/rpc-types.ts +11 -0
  92. package/src/shared/sqlite-bind-style.test.ts +82 -0
  93. package/src/shared/sqlite.ts +30 -1
  94. package/src/shared/tag-transcript.test.ts +3 -1
  95. package/src/shared/tag-transcript.ts +19 -17
  96. package/src/shared/tui-preferences.test.ts +210 -0
  97. package/src/shared/tui-preferences.ts +303 -0
  98. package/src/tui/data/context-db.ts +34 -2
  99. package/src/tui/index.tsx +58 -4
  100. package/src/tui/slots/sidebar-content.tsx +102 -11
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",
@@ -154609,15 +154649,22 @@ function ownerMessageIdForTagRow(row) {
154609
154649
  }
154610
154650
  return row.message_id.replace(CONTENT_ID_SUFFIX, "");
154611
154651
  }
154612
- function getActiveTagTokenAggregate(db, sessionId) {
154613
- const row = db.prepare(`SELECT
154652
+ function getActiveTagTokenAggregate(db, sessionId, protectedTags = 0) {
154653
+ const toolOutputExpr = protectedTags > 0 ? `COALESCE(SUM(CASE WHEN type = 'tool' AND tag_number < (
154654
+ SELECT tag_number FROM tags
154655
+ WHERE session_id = ? AND status = 'active'
154656
+ ORDER BY tag_number DESC LIMIT 1 OFFSET ?
154657
+ ) THEN COALESCE(token_count, 0) ELSE 0 END), 0)` : `COALESCE(SUM(CASE WHEN type = 'tool' THEN COALESCE(token_count, 0) ELSE 0 END), 0)`;
154658
+ const sql = `SELECT
154614
154659
  COALESCE(SUM(CASE WHEN type != 'tool' THEN COALESCE(token_count, 0) ELSE 0 END), 0)
154615
154660
  + COALESCE(SUM(COALESCE(reasoning_token_count, 0)), 0) AS conversation,
154616
154661
  COALESCE(SUM(CASE WHEN type = 'tool' THEN COALESCE(token_count, 0) + COALESCE(input_token_count, 0) ELSE 0 END), 0) AS tool_call,
154617
- COALESCE(SUM(CASE WHEN type = 'tool' THEN COALESCE(token_count, 0) ELSE 0 END), 0) AS tool_output,
154662
+ ${toolOutputExpr} AS tool_output,
154618
154663
  COALESCE(SUM(CASE WHEN token_count IS NULL THEN 1 ELSE 0 END), 0) AS null_count
154619
154664
  FROM tags
154620
- WHERE session_id = ? AND status = 'active'`).get(sessionId);
154665
+ WHERE session_id = ? AND status = 'active'`;
154666
+ const params = protectedTags > 0 ? [sessionId, protectedTags - 1, sessionId] : [sessionId];
154667
+ const row = db.prepare(sql).get(...params);
154621
154668
  return {
154622
154669
  conversation: row?.conversation ?? 0,
154623
154670
  toolCall: row?.tool_call ?? 0,
@@ -154625,6 +154672,26 @@ function getActiveTagTokenAggregate(db, sessionId) {
154625
154672
  nullCount: row?.null_count ?? 0
154626
154673
  };
154627
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
+ }
154628
154695
  function getTriggerTagTokenUpperBound(db, sessionId) {
154629
154696
  const row = db.prepare(`SELECT
154630
154697
  COALESCE(SUM(COALESCE(token_count, 0) + COALESCE(input_token_count, 0) + COALESCE(reasoning_token_count, 0)), 0) AS bound,
@@ -156211,6 +156278,58 @@ var init_safe_notification_target = __esm(() => {
156211
156278
  DEFAULT_TITLE_RE = /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
156212
156279
  });
156213
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
+
156214
156333
  // src/plugin/conflict-warning-hook.ts
156215
156334
  var exports_conflict_warning_hook = {};
156216
156335
  __export(exports_conflict_warning_hook, {
@@ -156545,6 +156664,9 @@ async function sendStartupAnnouncement(client, directory, version2, features, fo
156545
156664
  if (!sessionId) {
156546
156665
  return;
156547
156666
  }
156667
+ const { isTuiConnected: isTuiConnected2 } = await Promise.resolve().then(() => (init_rpc_notifications(), exports_rpc_notifications));
156668
+ if (isTuiConnected2(sessionId) || isTuiConnected2())
156669
+ return;
156548
156670
  if (await waitForSafeNotificationTarget(client, sessionId) === "skip")
156549
156671
  return;
156550
156672
  const bullets = features.map((line) => ` • ${line}`).join(`
@@ -164524,6 +164646,20 @@ function getDistinctStoredModelIds(db, projectPath) {
164524
164646
  const rows = getDistinctStoredModelIdsStatement(db).all(projectPath);
164525
164647
  return new Set(rows.map((row) => typeof row.modelId === "string" ? row.modelId : null));
164526
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
+ }
164527
164663
  var saveEmbeddingStatements, loadAllEmbeddingsStatements, deleteEmbeddingStatements, getStoredModelIdStatements, clearAllEmbeddingsStatements, getDistinctStoredModelIdsStatements;
164528
164664
  var init_storage_memory_embeddings = __esm(() => {
164529
164665
  saveEmbeddingStatements = new WeakMap;
@@ -165372,9 +165508,10 @@ function chunkCanonicalText(canonicalText, startOrdinal, endOrdinal, maxInputTok
165372
165508
  if (lines.length === 0 || endOrdinal < startOrdinal)
165373
165509
  return [];
165374
165510
  const normalizedMax = normalizeCompartmentChunkMaxInputTokens(maxInputTokens);
165511
+ const effectiveMax = Math.max(1, Math.floor(normalizedMax * CHUNK_WINDOW_SAFETY_RATIO));
165375
165512
  const fullText = lines.join(`
165376
165513
  `);
165377
- if (estimateTokens(fullText) <= normalizedMax) {
165514
+ if (estimateTokens(fullText) <= effectiveMax) {
165378
165515
  return [
165379
165516
  {
165380
165517
  windowIndex: 0,
@@ -165412,7 +165549,7 @@ function chunkCanonicalText(canonicalText, startOrdinal, endOrdinal, maxInputTok
165412
165549
  const lineStart = range?.start ?? startOrdinal;
165413
165550
  const lineEnd = range?.end ?? lineStart;
165414
165551
  const lineTokens = estimateTokens(line);
165415
- if (currentLines.length > 0 && currentTokens + lineTokens > normalizedMax) {
165552
+ if (currentLines.length > 0 && currentTokens + lineTokens > effectiveMax) {
165416
165553
  flush2();
165417
165554
  }
165418
165555
  if (currentLines.length === 0) {
@@ -165567,7 +165704,29 @@ function countUnembeddedSessionCompartments(db, projectPath, sessionId, modelId)
165567
165704
  )`).get(projectPath, sessionId, projectPath, modelId);
165568
165705
  return typeof row?.n === "number" ? row.n : 0;
165569
165706
  }
165570
- var DEFAULT_COMPARTMENT_CHUNK_MAX_INPUT_TOKENS = 512, loadFtsRowsStatements, existingHashStatements, existingHashByProjectStatements, deleteByCompartmentStatements, insertEmbeddingStatements, distinctModelStatements, clearProjectStatements, clearProjectModelStatements, searchRowsStatements, searchRowsByModelStatements, backfillCandidateStatements, sessionBackfillCandidateStatements;
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
+ }
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;
165571
165730
  var init_compartment_chunk_embedding = __esm(() => {
165572
165731
  init_read_session_formatting();
165573
165732
  loadFtsRowsStatements = new WeakMap;
@@ -165724,6 +165883,18 @@ async function withQuietConsole(fn) {
165724
165883
  console.error = origError;
165725
165884
  }
165726
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
+ }
165727
165898
  function isTransientLoadError(error51) {
165728
165899
  const message = error51 instanceof Error ? error51.message : String(error51 ?? "");
165729
165900
  if (!message)
@@ -165788,6 +165959,9 @@ class LocalEmbeddingProvider {
165788
165959
  if (this.pipeline) {
165789
165960
  return true;
165790
165961
  }
165962
+ if (nativeRuntimeMissing) {
165963
+ return false;
165964
+ }
165791
165965
  if (this.initPromise) {
165792
165966
  await this.initPromise;
165793
165967
  return this.pipeline !== null;
@@ -165854,7 +166028,12 @@ class LocalEmbeddingProvider {
165854
166028
  await releaseLock();
165855
166029
  }
165856
166030
  } catch (error51) {
165857
- 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
+ }
165858
166037
  this.pipeline = null;
165859
166038
  } finally {
165860
166039
  this.initPromise = null;
@@ -165965,7 +166144,7 @@ class LocalEmbeddingProvider {
165965
166144
  return this.pipeline !== null;
165966
166145
  }
165967
166146
  }
165968
- 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;
165969
166148
  var init_embedding_local = __esm(() => {
165970
166149
  init_magic_context();
165971
166150
  init_data_path();
@@ -166035,6 +166214,13 @@ var init_embedding_ssrf = __esm(() => {
166035
166214
  function normalizeEndpoint3(endpoint) {
166036
166215
  return endpoint?.trim().replace(/\/+$/, "") ?? "";
166037
166216
  }
166217
+ function embeddingModelsMatch(served, requested) {
166218
+ const a = served.trim().toLowerCase();
166219
+ const b = requested.trim().toLowerCase();
166220
+ if (a.length === 0 || b.length === 0)
166221
+ return true;
166222
+ return a === b || a.includes(b) || b.includes(a);
166223
+ }
166038
166224
 
166039
166225
  class OpenAICompatibleEmbeddingProvider {
166040
166226
  modelId;
@@ -166048,6 +166234,7 @@ class OpenAICompatibleEmbeddingProvider {
166048
166234
  failureTimes = [];
166049
166235
  circuitOpenUntil = 0;
166050
166236
  openLogged = false;
166237
+ modelMismatchLogged = false;
166051
166238
  halfOpenProbeInFlight = false;
166052
166239
  constructor(options) {
166053
166240
  this.endpoint = normalizeEndpoint3(options.endpoint);
@@ -166146,6 +166333,15 @@ class OpenAICompatibleEmbeddingProvider {
166146
166333
  this.recordFailure(isProbe);
166147
166334
  return Array.from({ length: texts.length }, () => null);
166148
166335
  }
166336
+ const servedModel = typeof body.model === "string" ? body.model : "";
166337
+ if (this.model && servedModel && !embeddingModelsMatch(servedModel, this.model)) {
166338
+ if (!this.modelMismatchLogged) {
166339
+ log(`[magic-context] embedding endpoint served a DIFFERENT model than requested — refusing the substituted vectors (they have the wrong dimensions/space). requested="${this.model}" served="${servedModel}". The endpoint likely substituted a loaded model; load/select "${this.model}" on the endpoint, or set embedding.model to the served model.`);
166340
+ this.modelMismatchLogged = true;
166341
+ }
166342
+ this.recordFailure(isProbe);
166343
+ return Array.from({ length: texts.length }, () => null);
166344
+ }
166149
166345
  const items = Array.isArray(body.data) ? body.data : [];
166150
166346
  const results = Array.from({ length: texts.length }, (_, index) => {
166151
166347
  const embedding = items[index]?.embedding;
@@ -166362,6 +166558,121 @@ var init_storage_git_commit_embeddings = __esm(() => {
166362
166558
  distinctModelIdStatements = new WeakMap;
166363
166559
  });
166364
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
+
166365
166676
  // src/features/magic-context/git-commits/sweep-coordinator.ts
166366
166677
  function runImmediate2(db, body) {
166367
166678
  db.exec("BEGIN IMMEDIATE");
@@ -166632,7 +166943,7 @@ function getChunkEmbeddingModelId(config2, providerIdentity) {
166632
166943
  }
166633
166944
  const chunkIdentity = {
166634
166945
  providerIdentity,
166635
- chunkerVersion: 1,
166946
+ chunkerVersion: 2,
166636
166947
  maxInputTokens: normalizeCompartmentChunkMaxInputTokens("max_input_tokens" in config2 ? config2.max_input_tokens : undefined),
166637
166948
  truncate: config2.provider === "openai-compatible" ? config2.truncate ?? "" : ""
166638
166949
  };
@@ -166655,7 +166966,9 @@ function snapshotFor(registration) {
166655
166966
  enabled,
166656
166967
  gitCommitEnabled,
166657
166968
  modelId: registration.observationMode || !providerIsOn ? "off" : registration.modelId,
166658
- 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"
166659
166972
  };
166660
166973
  }
166661
166974
  function disposeProvider(provider) {
@@ -166866,8 +167179,9 @@ async function embedUnembeddedMemoriesForProject(db, projectIdentity, batchSize
166866
167179
  }
166867
167180
  async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates, signal) {
166868
167181
  const noWork = [];
167182
+ const failed = [];
166869
167183
  if (candidates.length === 0)
166870
- return { embedded: 0, noWork };
167184
+ return { embedded: 0, noWork, failed };
166871
167185
  const maxInputTokens = getProjectEmbeddingMaxInputTokens(projectIdentity);
166872
167186
  const prepared = [];
166873
167187
  for (const candidate of candidates) {
@@ -166884,7 +167198,7 @@ async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates
166884
167198
  prepared.push({ candidate, windows });
166885
167199
  }
166886
167200
  if (prepared.length === 0)
166887
- return { embedded: 0, noWork };
167201
+ return { embedded: 0, noWork, failed };
166888
167202
  let embedded = 0;
166889
167203
  let i = 0;
166890
167204
  while (i < prepared.length) {
@@ -166901,35 +167215,60 @@ async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates
166901
167215
  const texts = [];
166902
167216
  for (const item of slice)
166903
167217
  texts.push(...item.windows.map((w) => w.text));
166904
- try {
166905
- const result = await embedBatchForProject(projectIdentity, texts, signal);
166906
- if (!result)
166907
- continue;
167218
+ const persistedIds = new Set;
167219
+ for (let attempt = 0;attempt < EMBED_SLICE_RETRY_ATTEMPTS; attempt++) {
166908
167220
  if (signal?.aborted)
166909
167221
  break;
166910
- let offset = 0;
166911
- for (const item of slice) {
166912
- const vectors = result.vectors.slice(offset, offset + item.windows.length);
166913
- offset += item.windows.length;
166914
- if (vectors.length !== item.windows.length || vectors.some((v) => !v)) {
166915
- 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);
166916
167251
  }
166917
- const rows = item.windows.map((window, index) => ({
166918
- compartmentId: item.candidate.id,
166919
- sessionId: item.candidate.sessionId,
166920
- projectPath: projectIdentity,
166921
- window,
166922
- modelId,
166923
- vector: vectors[index]
166924
- }));
166925
- replaceCompartmentChunkEmbeddings(db, rows);
166926
- embedded += 1;
166927
167252
  }
166928
- } catch (error51) {
166929
- 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
+ }
166930
167269
  }
166931
167270
  }
166932
- return { embedded, noWork };
167271
+ return { embedded, noWork, failed };
166933
167272
  }
166934
167273
  async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, options) {
166935
167274
  const snapshot = getProjectEmbeddingSnapshot(projectIdentity);
@@ -166952,9 +167291,11 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
166952
167291
  renewal.unref?.();
166953
167292
  const batchSize = Math.max(1, options?.batchSize ?? CHUNK_DRAIN_BATCH_SIZE);
166954
167293
  const skipIds = [];
167294
+ const failedIds = [];
166955
167295
  let embedded = 0;
166956
167296
  let aborted2 = false;
166957
- let providerStalled = false;
167297
+ let providerDown = false;
167298
+ let consecutiveFailedBatches = 0;
166958
167299
  try {
166959
167300
  options?.onProgress?.({ embedded, total });
166960
167301
  for (;; ) {
@@ -166962,15 +167303,26 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
166962
167303
  aborted2 = true;
166963
167304
  break;
166964
167305
  }
166965
- const candidates = loadUnembeddedSessionChunkCandidates(db, projectIdentity, sessionId, snapshot.chunkModelId, batchSize, skipIds);
167306
+ const candidates = loadUnembeddedSessionChunkCandidates(db, projectIdentity, sessionId, snapshot.chunkModelId, batchSize, [...skipIds, ...failedIds]);
166966
167307
  if (candidates.length === 0)
166967
167308
  break;
166968
- 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);
166969
167314
  for (const id of noWork)
166970
167315
  skipIds.push(id);
167316
+ for (const id of failed)
167317
+ failedIds.push(id);
166971
167318
  if (n === 0 && noWork.length === 0) {
166972
- providerStalled = true;
166973
- break;
167319
+ consecutiveFailedBatches += 1;
167320
+ if (consecutiveFailedBatches >= MAX_CONSECUTIVE_FAILED_BATCHES) {
167321
+ providerDown = true;
167322
+ break;
167323
+ }
167324
+ } else {
167325
+ consecutiveFailedBatches = 0;
166974
167326
  }
166975
167327
  embedded += n;
166976
167328
  options?.onProgress?.({ embedded: Math.min(embedded, total), total });
@@ -166978,23 +167330,58 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
166978
167330
  }
166979
167331
  } finally {
166980
167332
  clearInterval(renewal);
166981
- 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
+ }
166982
167338
  }
166983
167339
  if (aborted2)
166984
- return { status: "aborted", embedded, total };
166985
- if (providerStalled) {
167340
+ return { status: "aborted", embedded, total, failed: failedIds.length };
167341
+ if (providerDown || failedIds.length > 0) {
166986
167342
  const remaining = Math.max(0, countUnembeddedSessionCompartments(db, projectIdentity, sessionId, snapshot.chunkModelId) - skipIds.length);
166987
- if (remaining > 0)
166988
- return { status: "stalled", embedded, total, remaining };
167343
+ if (remaining > 0) {
167344
+ return { status: "stalled", embedded, total, remaining, failed: failedIds.length };
167345
+ }
166989
167346
  }
166990
- return { status: "done", embedded, total };
167347
+ return { status: "done", embedded, total, failed: failedIds.length };
167348
+ }
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
+ };
166991
167377
  }
166992
- 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;
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;
166993
167379
  var init_project_embedding_registry = __esm(() => {
166994
167380
  init_magic_context();
166995
167381
  init_logger();
166996
167382
  init_compartment_chunk_embedding();
166997
167383
  init_storage_git_commit_embeddings();
167384
+ init_storage_git_commits();
166998
167385
  init_sweep_coordinator();
166999
167386
  init_embedding_cache();
167000
167387
  init_embedding_identity();
@@ -167275,58 +167662,6 @@ var init_models_dev_cache = __esm(() => {
167275
167662
  init_logger();
167276
167663
  });
167277
167664
 
167278
- // src/shared/rpc-notifications.ts
167279
- var exports_rpc_notifications = {};
167280
- __export(exports_rpc_notifications, {
167281
- pushNotification: () => pushNotification,
167282
- isTuiConnected: () => isTuiConnected,
167283
- drainNotifications: () => drainNotifications
167284
- });
167285
- function pushNotification(type, payload, sessionId) {
167286
- queue2.push({ id: nextNotificationId++, type, payload, sessionId });
167287
- if (queue2.length > 100) {
167288
- const newestPerSession = new Map;
167289
- for (const n of queue2) {
167290
- const prev = newestPerSession.get(n.sessionId);
167291
- if (prev === undefined || n.id > prev) {
167292
- newestPerSession.set(n.sessionId, n.id);
167293
- }
167294
- }
167295
- const mustKeep = new Set(newestPerSession.values());
167296
- const byNewest = [...queue2].sort((a, b) => b.id - a.id);
167297
- const kept = [];
167298
- for (const n of byNewest) {
167299
- if (kept.length < 50 || mustKeep.has(n.id))
167300
- kept.push(n);
167301
- }
167302
- queue2 = kept.sort((a, b) => a.id - b.id);
167303
- }
167304
- }
167305
- function drainNotifications(lastReceivedId = 0, sessionId) {
167306
- const now = Date.now();
167307
- lastDrainAtAny = now;
167308
- if (sessionId !== undefined)
167309
- lastDrainAtBySession.set(sessionId, now);
167310
- const matchesClient = (notification) => sessionId === undefined || notification.sessionId === undefined || notification.sessionId === sessionId;
167311
- if (lastReceivedId > 0) {
167312
- queue2 = queue2.filter((notification) => !(notification.id <= lastReceivedId && matchesClient(notification)));
167313
- }
167314
- return queue2.filter((notification) => notification.id > lastReceivedId && matchesClient(notification));
167315
- }
167316
- function isTuiConnected(sessionId) {
167317
- const now = Date.now();
167318
- if (sessionId !== undefined) {
167319
- const at = lastDrainAtBySession.get(sessionId) ?? 0;
167320
- return at > 0 && now - at < TUI_CONNECTED_WINDOW_MS;
167321
- }
167322
- return lastDrainAtAny > 0 && now - lastDrainAtAny < TUI_CONNECTED_WINDOW_MS;
167323
- }
167324
- var queue2, nextNotificationId = 1, lastDrainAtBySession, lastDrainAtAny = 0, TUI_CONNECTED_WINDOW_MS = 3000;
167325
- var init_rpc_notifications = __esm(() => {
167326
- queue2 = [];
167327
- lastDrainAtBySession = new Map;
167328
- });
167329
-
167330
167665
  // src/features/magic-context/compartment-embedding.ts
167331
167666
  async function embedAndStoreCompartmentChunks(db, sessionId, projectPath, compartments) {
167332
167667
  if (compartments.length === 0)
@@ -169271,7 +169606,7 @@ ${prepared.block}
169271
169606
  if (!firstMessage || !textPart || isDroppedPlaceholder(textPart.text)) {
169272
169607
  messages.unshift({
169273
169608
  info: { role: "user", sessionID: sessionId },
169274
- parts: [{ type: "text", text: historyBlock }]
169609
+ parts: [{ type: "text", text: historyBlock, synthetic: true }]
169275
169610
  });
169276
169611
  } else {
169277
169612
  textPart.text = `${historyBlock}
@@ -170161,10 +170496,16 @@ function softRefreshCachedM1(options) {
170161
170496
  function prependM0M1Messages(sessionId, messages, m0Text, m1Text) {
170162
170497
  messages.unshift({
170163
170498
  info: { role: "user", sessionID: sessionId },
170164
- parts: [{ type: "text", text: m0Text.length > 0 ? m0Text : M0_EMPTY_BODY }]
170499
+ parts: [
170500
+ {
170501
+ type: "text",
170502
+ text: m0Text.length > 0 ? m0Text : M0_EMPTY_BODY,
170503
+ synthetic: true
170504
+ }
170505
+ ]
170165
170506
  }, {
170166
170507
  info: { role: "user", sessionID: sessionId },
170167
- parts: [{ type: "text", text: m1Text }]
170508
+ parts: [{ type: "text", text: m1Text, synthetic: true }]
170168
170509
  });
170169
170510
  }
170170
170511
  function renderFreshM0NonPersisted(options) {
@@ -170872,7 +171213,7 @@ function buildToolArcs(messages) {
170872
171213
  }
170873
171214
  return arcs.sort((a, b) => a.invOrdinal - b.invOrdinal || (a.resOrdinal ?? Number.MAX_SAFE_INTEGER) - (b.resOrdinal ?? Number.MAX_SAFE_INTEGER));
170874
171215
  }
170875
- function fenceBoundaryForToolArcs(candidate, arcs, lastCompartmentEndOrdinal) {
171216
+ function fenceBoundaryForToolArcs(candidate, arcs, lastCompartmentEndOrdinal, recentOpenArcCutoff) {
170876
171217
  let boundary = candidate;
170877
171218
  for (const arc of arcs) {
170878
171219
  if (arc.resOrdinal !== null) {
@@ -170881,6 +171222,8 @@ function fenceBoundaryForToolArcs(candidate, arcs, lastCompartmentEndOrdinal) {
170881
171222
  }
170882
171223
  continue;
170883
171224
  }
171225
+ if (arc.invOrdinal < recentOpenArcCutoff)
171226
+ continue;
170884
171227
  if (arc.invOrdinal >= lastCompartmentEndOrdinal + 1 && arc.invOrdinal < boundary) {
170885
171228
  return arc.invOrdinal;
170886
171229
  }
@@ -171120,7 +171463,7 @@ function semanticSnapBoundary(args) {
171120
171463
  return snapped;
171121
171464
  }
171122
171465
  function applyHeadCap(args) {
171123
- const { index, protectedTailStart, offset, arcs, capTokens } = args;
171466
+ const { index, protectedTailStart, offset, arcs, capTokens, recentOpenArcCutoff } = args;
171124
171467
  if (offset >= protectedTailStart)
171125
171468
  return { eligibleEndOrdinal: offset, oversizeAtomicUnit: false };
171126
171469
  let end = index.findHeadEndForCap(offset, protectedTailStart, capTokens);
@@ -171128,7 +171471,7 @@ function applyHeadCap(args) {
171128
171471
  for (const arc of arcs) {
171129
171472
  const resOrdinal = arc.resOrdinal;
171130
171473
  if (resOrdinal === null) {
171131
- if (arc.invOrdinal >= offset && arc.invOrdinal < end) {
171474
+ if (arc.invOrdinal >= recentOpenArcCutoff && arc.invOrdinal >= offset && arc.invOrdinal < end) {
171132
171475
  end = Math.min(end, arc.invOrdinal);
171133
171476
  }
171134
171477
  continue;
@@ -171195,7 +171538,14 @@ function resolveProtectedTailBoundary(ctx) {
171195
171538
  }
171196
171539
  if (ctx.mode === "manual-full-recomp") {
171197
171540
  const arcs2 = buildToolArcs(messages);
171198
- 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);
171199
171549
  const protectedTailStart2 = firstOpenArc?.invOrdinal ?? rawMessageCount + 1;
171200
171550
  const rawRangeFingerprint2 = computeRawRangeFingerprint(messages, offset, protectedTailStart2);
171201
171551
  return {
@@ -171237,13 +171587,14 @@ function resolveProtectedTailBoundary(ctx) {
171237
171587
  const scaledN = ctx.emergencyTailScale ? Math.max(1, Math.floor(target.N * ctx.emergencyTailScale)) : target.N;
171238
171588
  const arcs = buildToolArcs(messages);
171239
171589
  let boundary = index.findSuffixStartForTokens(scaledN);
171590
+ const recentOpenArcCutoff = boundary;
171240
171591
  let boundaryReason = boundary === 1 ? "whole-session-smaller-than-tail" : "size-walk";
171241
171592
  const tokenAtBoundary = index.tokenForOrdinal(boundary);
171242
171593
  if (boundary <= rawMessageCount && tokenAtBoundary > Math.max(2 * scaledN, 64000) && boundary < rawMessageCount) {
171243
171594
  boundary += 1;
171244
171595
  boundaryReason = "huge-message-exception";
171245
171596
  }
171246
- boundary = fenceBoundaryForToolArcs(boundary, arcs, ctx.lastCompartmentEndOrdinal);
171597
+ boundary = fenceBoundaryForToolArcs(boundary, arcs, ctx.lastCompartmentEndOrdinal, recentOpenArcCutoff);
171247
171598
  const snapped = semanticSnapBoundary({
171248
171599
  messages,
171249
171600
  index,
@@ -171253,7 +171604,7 @@ function resolveProtectedTailBoundary(ctx) {
171253
171604
  });
171254
171605
  if (snapped !== boundary)
171255
171606
  boundaryReason = "semantic-snap";
171256
- boundary = fenceBoundaryForToolArcs(snapped, arcs, ctx.lastCompartmentEndOrdinal);
171607
+ boundary = fenceBoundaryForToolArcs(snapped, arcs, ctx.lastCompartmentEndOrdinal, recentOpenArcCutoff);
171257
171608
  let runtimeFloor = offset;
171258
171609
  if (ctx.migrationFloorActive)
171259
171610
  runtimeFloor = Math.max(runtimeFloor, ctx.priorBoundaryOrdinal);
@@ -171289,7 +171640,8 @@ function resolveProtectedTailBoundary(ctx) {
171289
171640
  offset,
171290
171641
  arcs,
171291
171642
  lastCompartmentEndOrdinal: ctx.lastCompartmentEndOrdinal,
171292
- capTokens: perRunCap
171643
+ capTokens: perRunCap,
171644
+ recentOpenArcCutoff
171293
171645
  });
171294
171646
  const rawRangeFingerprint = computeRawRangeFingerprint(messages, offset, head.eligibleEndOrdinal);
171295
171647
  return {
@@ -177054,6 +177406,11 @@ async function runManagedRecomp(ctx, sessionId, options) {
177054
177406
  try {
177055
177407
  const message = await executeContextRecomp(buildRecompDeps(ctx, sessionId), options);
177056
177408
  const terminalPhase = isRecompSkip(message) ? "skipped" : isRecompFailure(message) ? "failed" : "done";
177409
+ if (terminalPhase === "done") {
177410
+ try {
177411
+ clearEmergencyRecovery(ctx.db, sessionId);
177412
+ } catch {}
177413
+ }
177057
177414
  setRecompTerminal(ctx.liveSessionState, sessionId, terminalPhase, extractRecompReason(message));
177058
177415
  return message;
177059
177416
  } catch (error51) {
@@ -177152,6 +177509,7 @@ var RECOMP_DONE_GRACE_MS = 30000;
177152
177509
  var init_recomp_orchestrator = __esm(async () => {
177153
177510
  init_compartment_storage();
177154
177511
  init_project_identity2();
177512
+ init_storage_meta_persisted();
177155
177513
  await __promiseAll([
177156
177514
  init_memory_migration(),
177157
177515
  init_compartment_runner()
@@ -177212,15 +177570,15 @@ function shouldShowAnnouncement() {
177212
177570
  }
177213
177571
  return state.version !== ANNOUNCEMENT_VERSION;
177214
177572
  }
177215
- 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";
177216
177574
  var init_announcement = __esm(() => {
177217
177575
  init_data_path();
177218
177576
  ANNOUNCEMENT_FEATURES = [
177219
- "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).",
177220
- "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.",
177221
- "Pi: fixed sessions overflowing the model context while still showing moderate usage Pi now sheds context before a tool-heavy turn overflows.",
177222
- "Fewer prompt-cache busts: doc edits, processed screenshots, and a rebuild-then-bust-again case no longer re-bill large prompt prefixes.",
177223
- "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."
177224
177582
  ];
177225
177583
  });
177226
177584
  // src/agents/permissions.ts
@@ -177949,9 +178307,9 @@ function getMagicContextBuiltinCommands() {
177949
178307
  template: "ctx-dream",
177950
178308
  description: "Run the hidden dreamer maintenance pass for this project now"
177951
178309
  },
177952
- "ctx-embed-history": {
177953
- template: "ctx-embed-history",
177954
- 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)"
177955
178313
  }
177956
178314
  };
177957
178315
  }
@@ -178822,7 +179180,7 @@ function enqueueDream(db, projectIdentity, reason, force = false) {
178822
179180
  return db.transaction(() => {
178823
179181
  if (!hasActiveDreamLease(db)) {
178824
179182
  const staleThresholdMs = force ? 2 * 60 * 1000 : 120 * 60 * 1000;
178825
- 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);
178826
179184
  }
178827
179185
  const existing = db.prepare("SELECT id FROM dream_queue WHERE project_path = ?").get(projectIdentity);
178828
179186
  if (existing) {
@@ -180517,120 +180875,7 @@ ${body}` : subject;
180517
180875
  init_logger();
180518
180876
  init_embedding();
180519
180877
  init_storage_git_commit_embeddings();
180520
-
180521
- // src/features/magic-context/git-commits/storage-git-commits.ts
180522
- init_logger();
180523
- var insertStatements = new WeakMap;
180524
- var existingShasStatements = new WeakMap;
180525
- var projectCountStatements = new WeakMap;
180526
- var evictStatements = new WeakMap;
180527
- var evictOverflowStatements = new WeakMap;
180528
- var latestCommitTimeStatements = new WeakMap;
180529
- function getInsertStatement(db) {
180530
- let stmt = insertStatements.get(db);
180531
- if (!stmt) {
180532
- stmt = db.prepare(`INSERT INTO git_commits (sha, project_path, short_sha, message, author, committed_at, indexed_at)
180533
- VALUES (?, ?, ?, ?, ?, ?, ?)
180534
- ON CONFLICT(sha) DO UPDATE SET
180535
- project_path = excluded.project_path,
180536
- short_sha = excluded.short_sha,
180537
- message = excluded.message,
180538
- author = excluded.author,
180539
- committed_at = excluded.committed_at,
180540
- indexed_at = excluded.indexed_at
180541
- WHERE git_commits.message != excluded.message`);
180542
- insertStatements.set(db, stmt);
180543
- }
180544
- return stmt;
180545
- }
180546
- function getExistingShasStatement(db) {
180547
- let stmt = existingShasStatements.get(db);
180548
- if (!stmt) {
180549
- stmt = db.prepare("SELECT sha FROM git_commits WHERE project_path = ?");
180550
- existingShasStatements.set(db, stmt);
180551
- }
180552
- return stmt;
180553
- }
180554
- function getProjectCountStatement(db) {
180555
- let stmt = projectCountStatements.get(db);
180556
- if (!stmt) {
180557
- stmt = db.prepare("SELECT COUNT(*) AS count FROM git_commits WHERE project_path = ?");
180558
- projectCountStatements.set(db, stmt);
180559
- }
180560
- return stmt;
180561
- }
180562
- function getLatestCommitTimeStatement(db) {
180563
- let stmt = latestCommitTimeStatements.get(db);
180564
- if (!stmt) {
180565
- stmt = db.prepare("SELECT MAX(committed_at) AS latest FROM git_commits WHERE project_path = ?");
180566
- latestCommitTimeStatements.set(db, stmt);
180567
- }
180568
- return stmt;
180569
- }
180570
- function getEvictOverflowStatement(db) {
180571
- let stmt = evictOverflowStatements.get(db);
180572
- if (!stmt) {
180573
- stmt = db.prepare(`DELETE FROM git_commits
180574
- WHERE rowid IN (
180575
- SELECT rowid FROM git_commits
180576
- WHERE project_path = ?
180577
- ORDER BY committed_at DESC, sha DESC
180578
- LIMIT -1 OFFSET ?
180579
- )`);
180580
- evictOverflowStatements.set(db, stmt);
180581
- }
180582
- return stmt;
180583
- }
180584
- function upsertCommits(db, projectPath, commits) {
180585
- if (commits.length === 0)
180586
- return { inserted: 0, updated: 0 };
180587
- const existing = new Set;
180588
- for (const row of getExistingShasStatement(db).all(projectPath)) {
180589
- existing.add(row.sha);
180590
- }
180591
- let inserted = 0;
180592
- let updated = 0;
180593
- const now = Date.now();
180594
- const insertStmt = getInsertStatement(db);
180595
- db.transaction(() => {
180596
- for (const commit of commits) {
180597
- const result = insertStmt.run(commit.sha, projectPath, commit.shortSha, commit.message, commit.author, commit.committedAtMs, now);
180598
- if (result.changes > 0) {
180599
- if (existing.has(commit.sha)) {
180600
- updated++;
180601
- } else {
180602
- inserted++;
180603
- existing.add(commit.sha);
180604
- }
180605
- }
180606
- }
180607
- })();
180608
- return { inserted, updated };
180609
- }
180610
- function getCommitCount(db, projectPath) {
180611
- const row = getProjectCountStatement(db).get(projectPath);
180612
- return row?.count ?? 0;
180613
- }
180614
- function getLatestIndexedCommitTimeMs(db, projectPath) {
180615
- const row = getLatestCommitTimeStatement(db).get(projectPath);
180616
- return row?.latest ?? null;
180617
- }
180618
- function enforceProjectCap(db, projectPath, maxCommits) {
180619
- if (maxCommits <= 0)
180620
- return 0;
180621
- const count = getCommitCount(db, projectPath);
180622
- if (count <= maxCommits)
180623
- return 0;
180624
- getEvictOverflowStatement(db).run(projectPath, maxCommits);
180625
- const after = getCommitCount(db, projectPath);
180626
- const evicted = Math.max(0, count - after);
180627
- if (evicted > 0) {
180628
- log(`[git-commits] evicted ${evicted} oldest commits for project ${projectPath} (cap=${maxCommits}, was=${count})`);
180629
- }
180630
- return evicted;
180631
- }
180632
-
180633
- // src/features/magic-context/git-commits/indexer.ts
180878
+ init_storage_git_commits();
180634
180879
  var MS_PER_DAY = 24 * 60 * 60 * 1000;
180635
180880
  var EMBED_BATCH_SIZE = 16;
180636
180881
  var EMBED_MAX_PER_SWEEP = 500;
@@ -180868,6 +181113,7 @@ function searchGitCommitsSync(db, projectPath, query, options) {
180868
181113
 
180869
181114
  // src/features/magic-context/git-commits/index.ts
180870
181115
  init_storage_git_commit_embeddings();
181116
+ init_storage_git_commits();
180871
181117
  init_sweep_coordinator();
180872
181118
 
180873
181119
  // src/plugin/dream-timer.ts
@@ -182291,7 +182537,7 @@ function createMagicContextCommandHandler(deps) {
182291
182537
  const isAugCommand = (command) => command === "ctx-aug";
182292
182538
  const isDreamCommand = (command) => command === "ctx-dream";
182293
182539
  const isSessionUpgradeCommand = (command) => command === "ctx-session-upgrade";
182294
- const isEmbedHistoryCommand = (command) => command === "ctx-embed-history";
182540
+ const isEmbedCommand = (command) => command === "ctx-embed";
182295
182541
  return {
182296
182542
  "command.execute.before": async (input, _output, _params) => {
182297
182543
  const isStatus = isStatusCommand(input.command);
@@ -182300,8 +182546,8 @@ function createMagicContextCommandHandler(deps) {
182300
182546
  const isAug = isAugCommand(input.command);
182301
182547
  const isDream = isDreamCommand(input.command);
182302
182548
  const isSessionUpgrade = isSessionUpgradeCommand(input.command);
182303
- const isEmbedHistory = isEmbedHistoryCommand(input.command);
182304
- if (!isStatus && !isFlush && !isRecomp && !isAug && !isDream && !isSessionUpgrade && !isEmbedHistory) {
182549
+ const isEmbed = isEmbedCommand(input.command);
182550
+ if (!isStatus && !isFlush && !isRecomp && !isAug && !isDream && !isSessionUpgrade && !isEmbed) {
182305
182551
  return;
182306
182552
  }
182307
182553
  const sessionId = input.sessionID;
@@ -182314,15 +182560,50 @@ function createMagicContextCommandHandler(deps) {
182314
182560
  await executeDreaming(deps, sessionId);
182315
182561
  return;
182316
182562
  }
182317
- if (isEmbedHistory) {
182318
- const summary = deps.executeEmbedHistory ? await deps.executeEmbedHistory(sessionId) : "Semantic embedding is not configured for this project, so there is nothing to embed.";
182319
- await deps.sendNotification(sessionId, summary, {});
182320
- 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.`;
182321
182597
  }
182322
182598
  if (isFlush) {
182323
182599
  result = executeFlush(deps.db, sessionId);
182324
182600
  clearCachedM0M1(deps.db, sessionId);
182325
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
+ }
182326
182607
  }
182327
182608
  if (isStatus) {
182328
182609
  if (isTuiConnected(sessionId)) {
@@ -182449,6 +182730,34 @@ ${snap.error}`;
182449
182730
  // src/hooks/magic-context/hook.ts
182450
182731
  init_derive_budgets();
182451
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
+
182452
182761
  // src/features/magic-context/message-index-async.ts
182453
182762
  init_logger();
182454
182763
  await init_message_index();
@@ -182686,8 +182995,8 @@ var CHANNEL1_SENTINEL = "<system-reminder>";
182686
182995
  var TOKENS_PER_BYTE = 0.25;
182687
182996
  var CHANNEL1_FLOOR_TOKENS = 1e4;
182688
182997
  var CHANNEL1_REFIRE_FLOOR_TOKENS = 1e4;
182689
- function channel1RefireTokens(historyBudgetTokens) {
182690
- const scaled = Math.round(0.05 * Math.max(0, historyBudgetTokens));
182998
+ function channel1RefireTokens(workingWindowTokens) {
182999
+ const scaled = Math.round(0.05 * Math.max(0, workingWindowTokens));
182691
183000
  return Math.max(CHANNEL1_REFIRE_FLOOR_TOKENS, scaled);
182692
183001
  }
182693
183002
  var S_GENTLE = 0.2;
@@ -182757,7 +183066,7 @@ function computeTailTokenEstimate(messages) {
182757
183066
  };
182758
183067
  }
182759
183068
  function decideChannel1(input) {
182760
- const { undroppedTokens, pressure, historyBudgetTokens, hasRecentReduce } = input;
183069
+ const { undroppedTokens, pressure, workingWindowTokens, hasRecentReduce } = input;
182761
183070
  const resetCycle = hasRecentReduce || undroppedTokens < input.lastNudgeUndropped;
182762
183071
  const lastNudge = resetCycle ? 0 : input.lastNudgeUndropped;
182763
183072
  const lastLevel = resetCycle ? "" : input.lastNudgeLevel;
@@ -182772,7 +183081,7 @@ function decideChannel1(input) {
182772
183081
  return quiet();
182773
183082
  if (undroppedTokens < CHANNEL1_FLOOR_TOKENS)
182774
183083
  return quiet();
182775
- const budget = historyBudgetTokens > 0 ? historyBudgetTokens : undroppedTokens || 1;
183084
+ const budget = workingWindowTokens > 0 ? workingWindowTokens : undroppedTokens || 1;
182776
183085
  const severity = undroppedTokens / budget * pressure;
182777
183086
  if (severity < S_GENTLE)
182778
183087
  return quiet();
@@ -182784,7 +183093,7 @@ function decideChannel1(input) {
182784
183093
  else
182785
183094
  level = "gentle";
182786
183095
  if (lastLevel === "") {
182787
- if (undroppedTokens < lastNudge + channel1RefireTokens(historyBudgetTokens)) {
183096
+ if (undroppedTokens < lastNudge + channel1RefireTokens(workingWindowTokens)) {
182788
183097
  return quiet();
182789
183098
  }
182790
183099
  } else if (LEVEL_RANK[level] <= LEVEL_RANK[lastLevel]) {
@@ -182809,6 +183118,13 @@ function computePressure(input) {
182809
183118
  function approxThousands(tokens) {
182810
183119
  return `${Math.round(tokens / 1000)}k`;
182811
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
+ }
182812
183128
  var CHANNEL2_USABLE_FRACTION = 1 / 3;
182813
183129
  var CHANNEL2_MIN_RECLAIMABLE = 1e4;
182814
183130
  function shouldTriggerChannel2(input) {
@@ -182818,30 +183134,32 @@ function shouldTriggerChannel2(input) {
182818
183134
  return true;
182819
183135
  return input.reclaimableTokens >= input.usableTokens * CHANNEL2_USABLE_FRACTION;
182820
183136
  }
182821
- function buildChannel2Reminder(undroppedTokens) {
183137
+ function buildChannel2Reminder(undroppedTokens, hint) {
182822
183138
  const amount = approxThousands(undroppedTokens);
183139
+ const hintText = formatOldestReclaimableHint(hint);
182823
183140
  return `<system-reminder>
182824
- ` + `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}
182825
183142
  ` + `</system-reminder>`;
182826
183143
  }
182827
- function buildChannel1Reminder(level, undroppedTokens) {
183144
+ function buildChannel1Reminder(level, undroppedTokens, hint) {
182828
183145
  const amount = approxThousands(undroppedTokens);
183146
+ const hintText = formatOldestReclaimableHint(hint);
182829
183147
  let body;
182830
183148
  switch (level) {
182831
183149
  case "gentle":
182832
- body = `You have ~${amount} tokens of tool output you have not reduced. ` + `Once you are done with earlier outputs, drop them with ctx_reduce to keep context lean.`;
183150
+ body = `You have ~${amount} tokens of tool output you have not reduced. ` + `When you are done with earlier outputs, dropping them with ctx_reduce keeps context lean.`;
182833
183151
  break;
182834
183152
  case "firm":
182835
- body = `~${amount} tokens of unreduced tool output is accumulating. ` + `Drop what you have already processed with ctx_reduce before continuing.`;
183153
+ body = `~${amount} tokens of unreduced tool output has built up. ` + `At your next natural stopping point, consider dropping what you have already processed with ctx_reduce.`;
182836
183154
  break;
182837
183155
  case "urgent":
182838
- body = `~${amount} tokens of unreduced tool output remain. ` + `A large span of this session will be comparted soon; drop spent outputs with ctx_reduce first so the archived span is the part that matters.`;
183156
+ body = `~${amount} tokens of unreduced tool output remain, and a large span of this session will be comparted before long. ` + `Consider dropping spent outputs with ctx_reduce so the archived span is the part that matters.`;
182839
183157
  break;
182840
183158
  }
182841
183159
  return `
182842
183160
 
182843
183161
  <system-reminder>
182844
- ${body}
183162
+ ${body}${hintText}
182845
183163
  </system-reminder>`;
182846
183164
  }
182847
183165
 
@@ -182895,10 +183213,10 @@ async function maybeDeliverChannel2(sessionId, deps) {
182895
183213
  try {
182896
183214
  const client3 = getLiveServerClient(serverUrl, deps.directory);
182897
183215
  const promptContext = await resolvePromptContext(client3, sessionId);
182898
- const reminder = buildChannel2Reminder(deps.reclaimableTokens);
183216
+ const reminder = buildChannel2Reminder(deps.reclaimableTokens, deps.oldestReclaimableToolTags);
182899
183217
  const body = {
182900
183218
  noReply: false,
182901
- parts: [{ type: "text", text: reminder }]
183219
+ parts: [{ type: "text", text: reminder, synthetic: true }]
182902
183220
  };
182903
183221
  if (promptContext?.agent)
182904
183222
  body.agent = promptContext.agent;
@@ -183356,7 +183674,8 @@ function applyCavemanCleanup(sessionId, db, targets, tags, config2) {
183356
183674
  const result = {
183357
183675
  compressedToLite: 0,
183358
183676
  compressedToFull: 0,
183359
- compressedToUltra: 0
183677
+ compressedToUltra: 0,
183678
+ mutatedTextTags: 0
183360
183679
  };
183361
183680
  if (!config2.enabled)
183362
183681
  return result;
@@ -183397,7 +183716,9 @@ function applyCavemanCleanup(sessionId, db, targets, tags, config2) {
183397
183716
  const target = targets.get(tag.tagNumber);
183398
183717
  if (!target)
183399
183718
  continue;
183400
- target.setContent(compressed);
183719
+ const didMutate = target.setContent(compressed);
183720
+ if (didMutate)
183721
+ result.mutatedTextTags += 1;
183401
183722
  updateCavemanDepth(db, sessionId, tag.tagNumber, targetDepth);
183402
183723
  if (targetDepth === DEPTH_LITE)
183403
183724
  result.compressedToLite += 1;
@@ -183985,28 +184306,6 @@ function stripInlineThinking(messages, messageTagNumbers, clearReasoningAge) {
183985
184306
  }
183986
184307
  return stripped;
183987
184308
  }
183988
- function truncateErroredTools(messages, watermark, messageTagNumbers) {
183989
- let truncated = 0;
183990
- for (let i = 0;i < messages.length; i++) {
183991
- const maxTag = messageTagNumbers.get(messages[i]) ?? 0;
183992
- if (maxTag > watermark) {
183993
- continue;
183994
- }
183995
- for (const part of messages[i].parts) {
183996
- if (!isRecord(part) || part.type !== "tool" || !isRecord(part.state)) {
183997
- continue;
183998
- }
183999
- if (part.state.status !== "error") {
184000
- continue;
184001
- }
184002
- if (typeof part.state.error === "string" && part.state.error.length > 100) {
184003
- part.state.error = `${part.state.error.slice(0, 100)}... [truncated]`;
184004
- truncated++;
184005
- }
184006
- }
184007
- }
184008
- return truncated;
184009
- }
184010
184309
  var REASONING_IGNORED_PART_TYPES = new Set([
184011
184310
  "step-start",
184012
184311
  "step-finish",
@@ -184423,28 +184722,12 @@ function appendReminderToUserMessage(message, reminder) {
184423
184722
  }
184424
184723
 
184425
184724
  // src/hooks/magic-context/apply-operations.ts
184426
- init_tag_part_guards();
184427
184725
  await init_storage();
184428
- var USER_DROP_PREVIEW_CHARS = 250;
184429
184726
  var RECENT_TOOL_SKELETON_WINDOW = 20;
184430
- function buildReplacementContent(tagId, target) {
184431
- const role = target.message?.info.role;
184432
- if (role !== "user") {
184433
- return `[dropped §${tagId}§]`;
184434
- }
184435
- const currentContent = target.getContent?.() ?? "";
184436
- const originalText = stripTagPrefix(currentContent);
184437
- if (originalText.length <= USER_DROP_PREVIEW_CHARS) {
184438
- return `[truncated §${tagId}§]
184439
- ${originalText}`;
184440
- }
184441
- const hardCut = originalText.slice(0, USER_DROP_PREVIEW_CHARS);
184442
- const softCutIndex = hardCut.search(/\s\S*$/);
184443
- const preview = softCutIndex > USER_DROP_PREVIEW_CHARS - 30 ? hardCut.slice(0, softCutIndex) : hardCut;
184444
- return `[truncated §${tagId}§]
184445
- ${preview}…`;
184446
- }
184447
- 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 = []) {
184448
184731
  let didMutateMessage = false;
184449
184732
  db.transaction(() => {
184450
184733
  const tags = preloadedTags ?? getTagsBySession(db, sessionId);
@@ -184452,11 +184735,16 @@ function applyPendingOperations(sessionId, db, targets, protectedTags = 0, prelo
184452
184735
  const tagTypeById = new Map(tags.map((tag) => [tag.tagNumber, tag.type]));
184453
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;
184454
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
+ ];
184455
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));
184456
- for (const pendingOp of pendingOps) {
184743
+ for (const { op: pendingOp, synthetic } of opsToApply) {
184457
184744
  const tagStatus = tagStatusById.get(pendingOp.tagId);
184458
184745
  if (tagStatus === "compacted" || tagStatus === "dropped") {
184459
- removePendingOp(db, sessionId, pendingOp.tagId);
184746
+ if (!synthetic)
184747
+ removePendingOp(db, sessionId, pendingOp.tagId);
184460
184748
  continue;
184461
184749
  }
184462
184750
  if (protectedTagIds.has(pendingOp.tagId)) {
@@ -184464,33 +184752,46 @@ function applyPendingOperations(sessionId, db, targets, protectedTags = 0, prelo
184464
184752
  }
184465
184753
  const target = targets.get(pendingOp.tagId);
184466
184754
  const isToolTag = tagTypeById.get(pendingOp.tagId) === "tool";
184755
+ if (synthetic) {
184756
+ if (!isToolTag || target?.canDrop?.() !== true)
184757
+ continue;
184758
+ }
184759
+ let shouldPersistDrop = false;
184467
184760
  if (isToolTag) {
184468
184761
  if (skeletonWindow.has(pendingOp.tagId)) {
184469
184762
  const truncResult = target?.truncate?.() ?? "absent";
184470
- if (truncResult === "incomplete") {
184763
+ if (truncResult === "incomplete" || synthetic && truncResult !== "truncated") {
184471
184764
  continue;
184472
184765
  }
184473
184766
  if (truncResult === "truncated") {
184474
184767
  didMutateMessage = true;
184475
184768
  }
184476
184769
  updateTagDropMode(db, sessionId, pendingOp.tagId, "truncated");
184770
+ shouldPersistDrop = true;
184477
184771
  } else {
184478
184772
  const dropResult = target?.drop?.() ?? "absent";
184479
- if (dropResult === "incomplete") {
184773
+ if (dropResult === "incomplete" || synthetic && dropResult !== "removed") {
184480
184774
  continue;
184481
184775
  }
184482
184776
  if (dropResult === "removed") {
184483
184777
  didMutateMessage = true;
184484
184778
  }
184485
184779
  updateTagDropMode(db, sessionId, pendingOp.tagId, "full");
184780
+ shouldPersistDrop = true;
184486
184781
  }
184487
184782
  } else if (target) {
184488
- const changed = target.setContent(buildReplacementContent(pendingOp.tagId, target));
184783
+ const changed = target.setContent(buildReplacementContent(pendingOp.tagId));
184489
184784
  if (changed)
184490
184785
  didMutateMessage = true;
184786
+ shouldPersistDrop = true;
184787
+ } else if (!synthetic) {
184788
+ shouldPersistDrop = true;
184491
184789
  }
184790
+ if (!shouldPersistDrop)
184791
+ continue;
184492
184792
  updateTagStatus(db, sessionId, pendingOp.tagId, "dropped");
184493
- removePendingOp(db, sessionId, pendingOp.tagId);
184793
+ if (!synthetic)
184794
+ removePendingOp(db, sessionId, pendingOp.tagId);
184494
184795
  }
184495
184796
  })();
184496
184797
  return didMutateMessage;
@@ -184514,7 +184815,7 @@ function applyFlushedStatuses(sessionId, db, targets, preloadedTags) {
184514
184815
  }
184515
184816
  }
184516
184817
  } else if (target) {
184517
- const changed = target.setContent(buildReplacementContent(tag.tagNumber, target));
184818
+ const changed = target.setContent(buildReplacementContent(tag.tagNumber));
184518
184819
  if (changed)
184519
184820
  didMutateMessage = true;
184520
184821
  }
@@ -185031,7 +185332,7 @@ function tagMessages(sessionId, messages, tagger, db, options = {}) {
185031
185332
  logTransformTiming(sessionId, "tag.saveSource", performance.now() - accSaveSource);
185032
185333
  for (const [compositeKey, tagId] of toolTagByCallId) {
185033
185334
  const thinkingParts = toolThinkingByCallId.get(compositeKey) ?? [];
185034
- targets.set(tagId, createToolDropTarget(compositeKey, thinkingParts, toolCallIndex, batch));
185335
+ targets.set(tagId, createToolDropTarget(compositeKey, thinkingParts, toolCallIndex, batch, tagId));
185035
185336
  }
185036
185337
  const hasRecentReduceCall = lastReduceMessageIndex >= 0 && messages.length - lastReduceMessageIndex <= RECENT_REDUCE_LOOKBACK;
185037
185338
  return {
@@ -186311,7 +186612,9 @@ function applyHeuristicCleanup(sessionId, db, targets, messageTagNumbers, config
186311
186612
  continue;
186312
186613
  updateTagDropMode(db, sessionId, tag.tagNumber, "full");
186313
186614
  updateTagStatus(db, sessionId, tag.tagNumber, "dropped");
186314
- deduplicatedTools++;
186615
+ if (result === "removed" || result === "truncated") {
186616
+ deduplicatedTools++;
186617
+ }
186315
186618
  }
186316
186619
  }
186317
186620
  })();
@@ -186320,6 +186623,7 @@ function applyHeuristicCleanup(sessionId, db, targets, messageTagNumbers, config
186320
186623
  sessionLog(sessionId, `heuristic cleanup: dropped ${droppedTools} tool tags, deduplicated ${deduplicatedTools} tool calls, dropped ${droppedInjections} system injections`);
186321
186624
  }
186322
186625
  let compressedTextTags = 0;
186626
+ let mutatedTextTags = 0;
186323
186627
  if (config2.caveman?.enabled) {
186324
186628
  const cavemanResult = applyCavemanCleanup(sessionId, db, targets, tags, {
186325
186629
  enabled: true,
@@ -186327,8 +186631,15 @@ function applyHeuristicCleanup(sessionId, db, targets, messageTagNumbers, config
186327
186631
  protectedTags: config2.protectedTags
186328
186632
  });
186329
186633
  compressedTextTags = cavemanResult.compressedToLite + cavemanResult.compressedToFull + cavemanResult.compressedToUltra;
186634
+ mutatedTextTags = cavemanResult.mutatedTextTags;
186330
186635
  }
186331
- return { droppedTools, deduplicatedTools, droppedInjections, compressedTextTags };
186636
+ return {
186637
+ droppedTools,
186638
+ deduplicatedTools,
186639
+ droppedInjections,
186640
+ compressedTextTags,
186641
+ mutatedTextTags
186642
+ };
186332
186643
  }
186333
186644
  function extractToolInfo(part) {
186334
186645
  if (part.type === "tool" && typeof part.tool === "string" && DEDUP_SAFE_TOOLS.has(part.tool)) {
@@ -186502,6 +186813,42 @@ function isTodoItem(value) {
186502
186813
  return typeof todo.content === "string" && typeof todo.status === "string" && (todo.priority === undefined || typeof todo.priority === "string");
186503
186814
  }
186504
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
+
186505
186852
  // src/hooks/magic-context/transform-postprocess-phase.ts
186506
186853
  var DEGRADE_CACHE_WARNING_THRESHOLD = 10;
186507
186854
  var degradedCacheCountBySession = new BoundedSessionMap(100);
@@ -186556,12 +186903,14 @@ async function runPostTransformPhase(args) {
186556
186903
  let deferredMaterializedSuccessfully = false;
186557
186904
  let heuristicsRanSuccessfully = false;
186558
186905
  let pendingOpsRanSuccessfully = false;
186906
+ let pendingOpsDidMutate = false;
186907
+ let heuristicOrReasoningDidMutate = false;
186559
186908
  try {
186560
186909
  if (shouldApplyPendingOps) {
186561
186910
  const applyReason = isExplicitFlush ? "explicit_flush" : deferredMaterialize ? "deferred_materialization" : `scheduler_execute (scheduler=${args.schedulerDecision})`;
186562
186911
  sessionLog(args.sessionId, `pending ops WILL APPLY — reason=${applyReason}, pendingOps=${pendingOps.length}, context=${args.contextUsage.percentage.toFixed(1)}%`);
186563
186912
  const tApply = performance.now();
186564
- applyPendingOperations(args.sessionId, args.db, args.targets, args.protectedTags, undefined, pendingOps);
186913
+ pendingOpsDidMutate = applyPendingOperations(args.sessionId, args.db, args.targets, args.protectedTags, undefined, pendingOps);
186565
186914
  logTransformTiming(args.sessionId, "applyPendingOperations", tApply);
186566
186915
  }
186567
186916
  if (shouldRunHeuristics) {
@@ -186579,7 +186928,8 @@ async function runPostTransformPhase(args) {
186579
186928
  } : undefined,
186580
186929
  caveman: cavemanConfig
186581
186930
  }, heuristicTags);
186582
- 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;
186583
186933
  const t7 = performance.now();
186584
186934
  const clearedReasoning = clearOldReasoning(args.messages, args.reasoningByMessage, args.messageTagNumbers, args.clearReasoningAge);
186585
186935
  if (canUseEmptySentinels) {
@@ -186605,6 +186955,7 @@ async function runPostTransformPhase(args) {
186605
186955
  }
186606
186956
  }
186607
186957
  logTransformTiming(args.sessionId, "clearOldReasoning", t7);
186958
+ heuristicOrReasoningDidMutate = heuristicMutationCount + clearedReasoning + strippedInline > 0;
186608
186959
  if (pendingMaterializationAtPassStart) {
186609
186960
  args.pendingMaterializationSessions.delete(args.sessionId);
186610
186961
  }
@@ -186615,7 +186966,31 @@ async function runPostTransformPhase(args) {
186615
186966
  if (args.schedulerDecision === "execute" && !materializationRequested) {
186616
186967
  updateSessionMeta(args.db, args.sessionId, { lastResponseTime: Date.now() });
186617
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
+ }
186618
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
+ }
186619
186994
  logTransformTiming(args.sessionId, "batchFinalize:heuristics", performance.now());
186620
186995
  if (args.sessionMeta.lastTransformError !== null) {
186621
186996
  updateSessionMeta(args.db, args.sessionId, { lastTransformError: null });
@@ -186627,11 +187002,6 @@ async function runPostTransformPhase(args) {
186627
187002
  deferredMaterializedSuccessfully = true;
186628
187003
  heuristicsRanSuccessfully = true;
186629
187004
  }
186630
- if (args.watermark > 0) {
186631
- const tWatermarkCleanup = performance.now();
186632
- truncateErroredTools(args.messages, args.watermark, args.messageTagNumbers);
186633
- logTransformTiming(args.sessionId, "watermarkCleanup", tWatermarkCleanup);
186634
- }
186635
187005
  if (shouldApplyPendingOps) {
186636
187006
  pendingOpsRanSuccessfully = true;
186637
187007
  }
@@ -187647,7 +188017,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
187647
188017
  let tailToolTokens;
187648
188018
  let liveTailTokens;
187649
188019
  try {
187650
- const agg = getActiveTagTokenAggregate(db, sessionId);
188020
+ const agg = getActiveTagTokenAggregate(db, sessionId, deps.protectedTags);
187651
188021
  tailToolTokens = agg.toolOutput;
187652
188022
  liveTailTokens = agg.conversation + agg.toolCall;
187653
188023
  } catch {
@@ -187658,6 +188028,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
187658
188028
  const executeThresholdTokens = Math.round((resolvedContextLimit ?? 0) * resolvedExecuteThresholdPct / 100);
187659
188029
  const usableTokens = Math.max(0, executeThresholdTokens - contextUsage.inputTokens + liveTailTokens);
187660
188030
  resetLastNudgeCycleIfTailShrank(db, sessionId, tailToolTokens);
188031
+ const oldestReclaimableToolTags = getOldestActiveUnprotectedToolTags(db, sessionId, deps.protectedTags);
187661
188032
  deps.channel1StateBySession.set(sessionId, {
187662
188033
  tailToolTokens,
187663
188034
  historyBudgetTokens: historyBudgetTokens ?? 0,
@@ -187666,9 +188037,10 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
187666
188037
  lastInputTokens: contextUsage.inputTokens,
187667
188038
  turnToolTokens: 0,
187668
188039
  usableTokens,
187669
- reducedSinceRefresh: false
188040
+ reducedSinceRefresh: false,
188041
+ oldestReclaimableToolTags
187670
188042
  });
187671
- const channel2MetricsKnown = fullFeatureMode && resolvedContextLimit !== undefined && resolvedContextLimit > 0 && resolvedExecuteThresholdPct > 0;
188043
+ const channel2MetricsKnown = resolvedContextLimit !== undefined && resolvedContextLimit > 0 && resolvedExecuteThresholdPct > 0;
187672
188044
  if (channel2MetricsKnown) {
187673
188045
  const channel2ShouldTrigger = shouldTriggerChannel2({
187674
188046
  reclaimableTokens: tailToolTokens,
@@ -187692,6 +188064,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
187692
188064
  }
187693
188065
  const elapsed = (performance.now() - startTime).toFixed(1);
187694
188066
  sessionLog(sessionId, `transform completed in ${elapsed}ms (${messages.length} messages, ${targets.size} targets, watermark: ${watermark})`);
188067
+ deps.maybeAutoEmbedSession?.(sessionId);
187695
188068
  };
187696
188069
  }
187697
188070
  function resolveHistoryBudgetTokens(historyBudgetPercentage, contextUsage, executeThresholdPercentage, modelKey, executeThresholdTokens, resolvedContextLimit) {
@@ -187737,18 +188110,14 @@ function evictExpiredUsageEntries(contextUsageMap) {
187737
188110
  }
187738
188111
  async function deliverChannel2IfPending(deps, sessionId) {
187739
188112
  try {
187740
- try {
187741
- const meta3 = getOrCreateSessionMeta(deps.db, sessionId);
187742
- if (meta3.isSubagent)
187743
- return;
187744
- } catch {}
187745
188113
  const baseline = deps.channel1StateBySession?.get(sessionId);
187746
188114
  await maybeDeliverChannel2(sessionId, {
187747
188115
  db: deps.db,
187748
188116
  serverUrl: deps.serverUrl,
187749
188117
  directory: deps.directory ?? ".",
187750
188118
  reclaimableTokens: baseline ? baseline.tailToolTokens + baseline.turnToolTokens : undefined,
187751
- usableTokens: baseline?.usableTokens
188119
+ usableTokens: baseline?.usableTokens,
188120
+ oldestReclaimableToolTags: baseline?.oldestReclaimableToolTags
187752
188121
  });
187753
188122
  } catch (error51) {
187754
188123
  sessionLog(sessionId, "channel2 delivery wrapper failed (ignored):", error51);
@@ -188054,6 +188423,46 @@ function createEventHandler2(deps) {
188054
188423
  };
188055
188424
  }
188056
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
+
188057
188466
  // src/hooks/magic-context/hook.ts
188058
188467
  await __promiseAll([
188059
188468
  init_inject_compartments(),
@@ -188282,10 +188691,11 @@ function maybeInjectChannel1Nudge(args, sessionId, tool, output) {
188282
188691
  contextLimit: state.contextLimit,
188283
188692
  executeThresholdPercentage: state.executeThresholdPercentage
188284
188693
  });
188694
+ const workingWindowTokens = Math.round(state.contextLimit * state.executeThresholdPercentage / 100);
188285
188695
  const decision = decideChannel1({
188286
188696
  undroppedTokens,
188287
188697
  pressure,
188288
- historyBudgetTokens: state.historyBudgetTokens,
188698
+ workingWindowTokens,
188289
188699
  lastNudgeUndropped: getLastNudgeUndropped(args.db, sessionId),
188290
188700
  lastNudgeLevel: getLastNudgeLevel(args.db, sessionId),
188291
188701
  hasRecentReduce: false
@@ -188294,7 +188704,7 @@ function maybeInjectChannel1Nudge(args, sessionId, tool, output) {
188294
188704
  setLastNudgeLevel(args.db, sessionId, decision.nextLastNudgeLevel);
188295
188705
  if (!decision.fire)
188296
188706
  return;
188297
- out.output += buildChannel1Reminder(decision.level, decision.undroppedTokens);
188707
+ out.output += buildChannel1Reminder(decision.level, decision.undroppedTokens, state.oldestReclaimableToolTags);
188298
188708
  sessionLog(sessionId, `channel1 nudge fired: level=${decision.level} undropped~${Math.round(decision.undroppedTokens / 1000)}k tool=${tool}`);
188299
188709
  }
188300
188710
  function createToolExecuteAfterHook(args) {
@@ -188366,9 +188776,7 @@ Context is managed for you entirely automatically — there's nothing to prune a
188366
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).`;
188367
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.`;
188368
188778
  var BASE_INTRO = (protectedTags) => `Messages and tool outputs are tagged with §N§ identifiers (e.g., §1§, §42§).
188369
- Use \`ctx_reduce\` to manage context size. It supports one operation:
188370
- - \`drop\`: Remove entirely (best for tool outputs you already acted on).
188371
- 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".
188372
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.
188373
188781
  ${CTX_NOTE_GUIDANCE}
188374
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.
@@ -188387,7 +188795,7 @@ Use \`ctx_expand\` to recover the raw conversation behind a \`<compartment>\` su
188387
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.
188388
188796
  ${TOOL_HISTORY_GUIDANCE}
188389
188797
  NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
188390
- 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\`.
188391
188799
  NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
188392
188800
  Before your turn finishes, consider using \`ctx_reduce\` to drop large tool outputs you no longer need.`;
188393
188801
  var BASE_INTRO_NO_REDUCE = () => `${CTX_NOTE_GUIDANCE}
@@ -188808,29 +189216,55 @@ function createMagicContextHook(deps) {
188808
189216
  ensureProjectRegistered: ensureProjectRegisteredFromOpenCodeDirectory,
188809
189217
  getNotificationParams: (sid) => getLiveNotificationParams(sid, liveModelBySession, variantBySession, agentBySession)
188810
189218
  });
188811
- const executeEmbedHistory = async (sessionId) => {
189219
+ const executeEmbedHistory = async (sessionId, options) => {
188812
189220
  if (deps.config.memory?.enabled === false) {
188813
189221
  return "Memory is disabled for this project, so there is no semantic embedding to backfill.";
188814
189222
  }
188815
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
+ }
188816
189228
  await ensureProjectRegisteredFromOpenCodeDirectory(directory, db);
188817
189229
  const sessionProjectIdentity = resolveProjectIdentity(directory);
188818
- setRecompStarting({ recompProgressBySession }, sessionId, "Embedding history…", "embed");
188819
- const outcome = await embedSessionCompartmentChunks(db, sessionProjectIdentity, sessionId, {
188820
- onProgress: ({ embedded, total }) => {
188821
- const cur = recompProgressBySession.get(sessionId);
188822
- if (!cur || cur.phase !== "recomp")
188823
- return;
188824
- recompProgressBySession.set(sessionId, {
188825
- ...cur,
188826
- processedMessages: embedded,
188827
- totalMessages: total,
188828
- updatedAt: Date.now()
188829
- });
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);
188830
189260
  }
188831
- });
189261
+ }
189262
+ if ("failed" in outcome)
189263
+ runFailed = outcome.failed;
188832
189264
  const terminal = (phase, message) => {
188833
- setRecompTerminal({ recompProgressBySession }, sessionId, phase, message);
189265
+ if (!options?.silent) {
189266
+ setRecompTerminal({ recompProgressBySession }, sessionId, phase, message);
189267
+ }
188834
189268
  return message;
188835
189269
  };
188836
189270
  switch (outcome.status) {
@@ -188839,15 +189273,78 @@ function createMagicContextHook(deps) {
188839
189273
  case "disabled":
188840
189274
  return terminal("skipped", "No embedding provider is configured, so there is nothing to embed.");
188841
189275
  case "busy":
188842
- return terminal("skipped", `Embedding is already running for this project — ${outcome.total} compartment${outcome.total === 1 ? "" : "s"} still pending. Try again shortly.`);
188843
- case "aborted":
188844
- 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
+ }
188845
189282
  case "stalled":
188846
- 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.`);
188847
189284
  default:
188848
- 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)` : ""}.`);
188849
189286
  }
188850
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
+ };
188851
189348
  const sidekickRunnable = isSidekickRunnable(deps.config);
188852
189349
  const sidekickConfig = sidekickRunnable ? deps.config.sidekick : undefined;
188853
189350
  const transform2 = createTransform({
@@ -188909,7 +189406,8 @@ function createMagicContextHook(deps) {
188909
189406
  cavemanTextCompression: ctxReduceEnabled === false && deps.config.caveman_text_compression?.enabled === true ? {
188910
189407
  enabled: true,
188911
189408
  minChars: deps.config.caveman_text_compression.min_chars ?? 500
188912
- } : undefined
189409
+ } : undefined,
189410
+ maybeAutoEmbedSession
188913
189411
  });
188914
189412
  const eventHandler = createEventHandler2({
188915
189413
  contextUsageMap,
@@ -188938,6 +189436,7 @@ function createMagicContextHook(deps) {
188938
189436
  recompProgressBySession.delete(sessionId);
188939
189437
  internalChildSessions.delete(sessionId);
188940
189438
  channel1StateBySession.delete(sessionId);
189439
+ clearEmbedSessionState(sessionId);
188941
189440
  }
188942
189441
  });
188943
189442
  const runDreamQueueInBackground = () => {
@@ -189003,6 +189502,8 @@ function createMagicContextHook(deps) {
189003
189502
  executeRecomp: historianRunnable ? async (sessionId, options) => runManagedRecomp(buildManagedRecompCtx(sessionId), sessionId, options) : undefined,
189004
189503
  runUpgrade: historianRunnable ? async (sessionId) => runManagedUpgrade(buildManagedRecompCtx(sessionId), sessionId) : undefined,
189005
189504
  executeEmbedHistory,
189505
+ pauseEmbedDrain,
189506
+ getEmbedStatusText,
189006
189507
  sendNotification: async (sessionId, text, params) => {
189007
189508
  await sendIgnoredMessage(deps.client, sessionId, text, {
189008
189509
  ...getLiveNotificationParams(sessionId, liveModelBySession, variantBySession, agentBySession),
@@ -189215,6 +189716,7 @@ function truncateError(name2, code, message, maxLen = 240) {
189215
189716
 
189216
189717
  // src/plugin/rpc-handlers.ts
189217
189718
  init_project_identity();
189719
+ init_project_embedding_registry();
189218
189720
  init_tool_definition_tokens();
189219
189721
  await init_storage();
189220
189722
 
@@ -189935,6 +190437,26 @@ function buildStatusDetail(db, sessionId, directory, modelKey, config2, liveSess
189935
190437
  }
189936
190438
  return detail;
189937
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
+ }
189938
190460
  function registerRpcHandlers(rpcServer, args) {
189939
190461
  const { directory, config: config2, liveSessionState } = args;
189940
190462
  const rawConfig = config2;
@@ -189957,6 +190479,19 @@ function registerRpcHandlers(rpcServer, args) {
189957
190479
  return { error: "unavailable" };
189958
190480
  return buildStatusDetail(db, sessionId, dir, modelKey, rawConfig, liveSessionState, injectionBudgetTokens);
189959
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
+ });
189960
190495
  rpcServer.handle("compartment-count", async (params) => {
189961
190496
  const sessionId = String(params.sessionId ?? "");
189962
190497
  const db = getDb();
@@ -190079,27 +190614,225 @@ Older parts of this session are summarized into <compartment> blocks inside <ses
190079
190614
 
190080
190615
  ctx_expand(start=120, end=245) ← the compartment's own start/end attributes
190081
190616
 
190082
- 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.`;
190083
190622
  var CTX_EXPAND_TOKEN_BUDGET = 15000;
190084
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
+
190085
190797
  // src/tools/ctx-expand/tools.ts
190086
190798
  function createCtxExpandTool(deps) {
190087
190799
  return tool({
190088
190800
  description: CTX_EXPAND_DESCRIPTION,
190089
190801
  args: {
190090
- 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`),
190091
- 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.")
190092
190806
  },
190093
190807
  async execute(args, toolContext) {
190094
190808
  const sessionId = toolContext.sessionID;
190809
+ if (typeof args.message === "number" && args.message >= 1) {
190810
+ return renderMessageByOrdinal(sessionId, args.message);
190811
+ }
190095
190812
  if (!args.start || !args.end || args.start < 1 || args.end < args.start) {
190096
- 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).";
190097
190814
  }
190098
190815
  const lastCompartmentEnd = getLastCompartmentEndMessage(deps.db, sessionId);
190099
190816
  if (lastCompartmentEnd >= 0 && args.start > lastCompartmentEnd) {
190100
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.`;
190101
190818
  }
190102
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
+ }
190103
190836
  const chunk = readSessionChunk(sessionId, CTX_EXPAND_TOKEN_BUDGET, args.start, effectiveEnd + 1);
190104
190837
  if (!chunk.text || chunk.messageCount === 0) {
190105
190838
  return `No messages found in range ${args.start}-${args.end}. The range may be outside this session's history.`;
@@ -190793,15 +191526,16 @@ function createCtxNoteTools(deps) {
190793
191526
  };
190794
191527
  }
190795
191528
  // src/tools/ctx-reduce/constants.ts
190796
- var CTX_REDUCE_DESCRIPTION = `Reduce context by dropping tagged content you no longer need.
190797
- Use §N§ identifiers visible in conversation. The \`drop\` param accepts ranges: "3-5", "1,2,9", "1-5,8".
190798
-
190799
- CRITICAL RULES:
190800
- - NEVER blanket-drop large ranges (e.g., "1-50"). Always review what each tag contains first.
190801
- - Only drop tool outputs you have already processed and no longer need.
190802
- - Protected tags are accepted but deferred until they leave the last protected range.
190803
- - Keep recent context only reduce OLD content that is no longer relevant to current work.
190804
- - 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.`;
190805
191539
  // src/tools/ctx-reduce/tools.ts
190806
191540
  import { tool as tool4 } from "@opencode-ai/plugin";
190807
191541
 
@@ -191384,6 +192118,9 @@ class MagicContextRpcServer {
191384
192118
  }
191385
192119
 
191386
192120
  // src/index.ts
192121
+ var HISTORIAN_MAX_STEPS = 40;
192122
+ var SIDEKICK_MAX_STEPS = 40;
192123
+ var DREAMER_MAX_STEPS = 150;
191387
192124
  var plugin = async (ctx) => {
191388
192125
  const pluginConfig = loadPluginConfig(ctx.directory);
191389
192126
  setSqlitePragmaConfig({
@@ -191576,11 +192313,13 @@ var plugin = async (ctx) => {
191576
192313
  await hooks.magicContext?.["experimental.text.complete"]?.(input, output);
191577
192314
  },
191578
192315
  config: async (config2) => {
191579
- const buildHiddenAgentConfig = (agentId, prompt, allowedTools, overrides) => {
192316
+ const buildHiddenAgentConfig = (agentId, prompt, allowedTools, maxSteps, overrides) => {
191580
192317
  const { permission: overridePermission, ...restOverrides } = overrides ?? {};
191581
192318
  const basePermission = buildAllowOnlyPermission(allowedTools);
191582
192319
  return {
191583
192320
  prompt,
192321
+ steps: maxSteps,
192322
+ maxSteps,
191584
192323
  ...getAgentFallbackModels(agentId) ? { fallback_models: getAgentFallbackModels(agentId) } : {},
191585
192324
  ...restOverrides,
191586
192325
  permission: {
@@ -191621,10 +192360,10 @@ var plugin = async (ctx) => {
191621
192360
  })() : undefined;
191622
192361
  config2.agent = {
191623
192362
  ...config2.agent ?? {},
191624
- [DREAMER_AGENT]: buildHiddenAgentConfig(DREAMER_AGENT, DREAMER_SYSTEM_PROMPT, DREAMER_ALLOWED_TOOLS, dreamerAgentOverrides),
191625
- [HISTORIAN_AGENT]: buildHiddenAgentConfig(HISTORIAN_AGENT, COMPARTMENT_AGENT_SYSTEM_PROMPT, HISTORIAN_ALLOWED_TOOLS, historianAgentOverrides),
191626
- [HISTORIAN_EDITOR_AGENT]: buildHiddenAgentConfig(HISTORIAN_EDITOR_AGENT, HISTORIAN_EDITOR_SYSTEM_PROMPT, HISTORIAN_ALLOWED_TOOLS, historianAgentOverrides),
191627
- [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)
191628
192367
  };
191629
192368
  }
191630
192369
  };