@cortexkit/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 (101) hide show
  1. package/README.md +4 -2
  2. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  3. package/dist/features/magic-context/compartment-chunk-embedding.d.ts +18 -0
  4. package/dist/features/magic-context/compartment-chunk-embedding.d.ts.map +1 -1
  5. package/dist/features/magic-context/memory/embedding-local.d.ts +4 -0
  6. package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
  7. package/dist/features/magic-context/memory/embedding-openai.d.ts +14 -0
  8. package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
  9. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts +6 -0
  10. package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
  11. package/dist/features/magic-context/project-embedding-registry.d.ts +38 -0
  12. package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
  13. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  14. package/dist/features/magic-context/storage-meta-session.d.ts +1 -0
  15. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  16. package/dist/features/magic-context/storage-meta-shared.d.ts +2 -1
  17. package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
  18. package/dist/features/magic-context/storage-meta.d.ts +1 -1
  19. package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
  20. package/dist/features/magic-context/storage-tags.d.ts +20 -1
  21. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  22. package/dist/features/magic-context/storage.d.ts +2 -2
  23. package/dist/features/magic-context/storage.d.ts.map +1 -1
  24. package/dist/features/magic-context/types.d.ts +1 -0
  25. package/dist/features/magic-context/types.d.ts.map +1 -1
  26. package/dist/hooks/magic-context/apply-operations.d.ts +3 -2
  27. package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
  28. package/dist/hooks/magic-context/caveman-cleanup.d.ts +1 -0
  29. package/dist/hooks/magic-context/caveman-cleanup.d.ts.map +1 -1
  30. package/dist/hooks/magic-context/channel2-delivery.d.ts +2 -0
  31. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  32. package/dist/hooks/magic-context/command-handler.d.ts +7 -5
  33. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  34. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +14 -4
  35. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  36. package/dist/hooks/magic-context/embed-session-state.d.ts +14 -0
  37. package/dist/hooks/magic-context/embed-session-state.d.ts.map +1 -0
  38. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  39. package/dist/hooks/magic-context/format-embed-status.d.ts +9 -0
  40. package/dist/hooks/magic-context/format-embed-status.d.ts.map +1 -0
  41. package/dist/hooks/magic-context/heuristic-cleanup.d.ts +1 -0
  42. package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
  43. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  44. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  45. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  46. package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
  47. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +1 -1
  48. package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
  49. package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
  50. package/dist/hooks/magic-context/strip-content.d.ts +0 -1
  51. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  52. package/dist/hooks/magic-context/tag-content-primitives.d.ts +2 -0
  53. package/dist/hooks/magic-context/tag-content-primitives.d.ts.map +1 -1
  54. package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
  55. package/dist/hooks/magic-context/tool-drop-target.d.ts +1 -1
  56. package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/tool-reclaim.d.ts +12 -0
  58. package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -0
  59. package/dist/hooks/magic-context/transform-operations.d.ts +1 -1
  60. package/dist/hooks/magic-context/transform-operations.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  62. package/dist/hooks/magic-context/transform.d.ts +2 -0
  63. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +1117 -378
  66. package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
  67. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  68. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  69. package/dist/shared/announcement.d.ts +1 -1
  70. package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
  71. package/dist/shared/rpc-types.d.ts +20 -0
  72. package/dist/shared/rpc-types.d.ts.map +1 -1
  73. package/dist/shared/sqlite.d.ts +5 -1
  74. package/dist/shared/sqlite.d.ts.map +1 -1
  75. package/dist/shared/tui-preferences.d.ts +32 -0
  76. package/dist/shared/tui-preferences.d.ts.map +1 -0
  77. package/dist/tools/ctx-expand/constants.d.ts +1 -1
  78. package/dist/tools/ctx-expand/constants.d.ts.map +1 -1
  79. package/dist/tools/ctx-expand/render.d.ts +43 -0
  80. package/dist/tools/ctx-expand/render.d.ts.map +1 -0
  81. package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
  82. package/dist/tools/ctx-expand/types.d.ts +6 -2
  83. package/dist/tools/ctx-expand/types.d.ts.map +1 -1
  84. package/dist/tools/ctx-reduce/constants.d.ts +1 -1
  85. package/dist/tools/ctx-reduce/constants.d.ts.map +1 -1
  86. package/dist/tui/data/context-db.d.ts +4 -2
  87. package/dist/tui/data/context-db.d.ts.map +1 -1
  88. package/package.json +1 -1
  89. package/src/shared/announcement.ts +6 -6
  90. package/src/shared/model-suggestion-retry.test.ts +61 -1
  91. package/src/shared/model-suggestion-retry.ts +22 -0
  92. package/src/shared/rpc-types.ts +11 -0
  93. package/src/shared/sqlite-bind-style.test.ts +82 -0
  94. package/src/shared/sqlite.ts +30 -1
  95. package/src/shared/tag-transcript.test.ts +3 -1
  96. package/src/shared/tag-transcript.ts +19 -17
  97. package/src/shared/tui-preferences.test.ts +210 -0
  98. package/src/shared/tui-preferences.ts +303 -0
  99. package/src/tui/data/context-db.ts +34 -2
  100. package/src/tui/index.tsx +58 -4
  101. package/src/tui/slots/sidebar-content.tsx +106 -12
package/dist/index.js CHANGED
@@ -15562,9 +15562,11 @@ async function promptWithTimeout(client, args, timeoutMs, signal) {
15562
15562
  });
15563
15563
  } catch (error51) {
15564
15564
  if (signal?.aborted) {
15565
+ await abortChildRun(client, args.path.id);
15565
15566
  throw new Error("prompt aborted by external signal");
15566
15567
  }
15567
15568
  if (controller.signal.aborted) {
15569
+ await abortChildRun(client, args.path.id);
15568
15570
  throw new Error(`prompt timed out after ${timeoutMs}ms`);
15569
15571
  }
15570
15572
  throw error51;
@@ -15573,6 +15575,13 @@ async function promptWithTimeout(client, args, timeoutMs, signal) {
15573
15575
  signal?.removeEventListener("abort", onExternalAbort);
15574
15576
  }
15575
15577
  }
15578
+ async function abortChildRun(client, sessionId) {
15579
+ try {
15580
+ await client.session.abort({ path: { id: sessionId } });
15581
+ } catch (error51) {
15582
+ log(`[model-retry] child session abort failed for ${sessionId}: ${String(error51)}`);
15583
+ }
15584
+ }
15576
15585
  function isNonRetryable(error51, externalSignal) {
15577
15586
  if (externalSignal?.aborted)
15578
15587
  return true;
@@ -15844,7 +15853,7 @@ function isSessionMetaRow(row) {
15844
15853
  if (row === null || typeof row !== "object")
15845
15854
  return false;
15846
15855
  const r = row;
15847
- 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);
15856
+ 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);
15848
15857
  }
15849
15858
  function getDefaultSessionMeta(sessionId) {
15850
15859
  return {
@@ -15867,6 +15876,7 @@ function getDefaultSessionMeta(sessionId) {
15867
15876
  conversationTokens: 0,
15868
15877
  toolCallTokens: 0,
15869
15878
  clearedReasoningThroughTag: 0,
15879
+ toolReclaimWatermark: 0,
15870
15880
  lastTodoState: "",
15871
15881
  cachedM0Bytes: null,
15872
15882
  cachedM1Bytes: null,
@@ -15930,6 +15940,7 @@ function toSessionMeta(row) {
15930
15940
  conversationTokens: numOrZero(row.conversation_tokens),
15931
15941
  toolCallTokens: numOrZero(row.tool_call_tokens),
15932
15942
  clearedReasoningThroughTag: numOrZero(row.cleared_reasoning_through_tag),
15943
+ toolReclaimWatermark: numOrZero(row.tool_reclaim_watermark),
15933
15944
  lastTodoState: lastTodoStateRaw,
15934
15945
  cachedM0Bytes: toBufferOrNull(row.cached_m0_bytes),
15935
15946
  cachedM1Bytes: toBufferOrNull(row.cached_m1_bytes),
@@ -16039,6 +16050,7 @@ var init_storage_meta_shared = __esm(() => {
16039
16050
  "conversation_tokens",
16040
16051
  "tool_call_tokens",
16041
16052
  "cleared_reasoning_through_tag",
16053
+ "tool_reclaim_watermark",
16042
16054
  "last_todo_state",
16043
16055
  "cached_m0_bytes",
16044
16056
  "cached_m1_bytes",
@@ -16087,6 +16099,7 @@ var init_storage_meta_shared = __esm(() => {
16087
16099
  conversationTokens: "conversation_tokens",
16088
16100
  toolCallTokens: "tool_call_tokens",
16089
16101
  clearedReasoningThroughTag: "cleared_reasoning_through_tag",
16102
+ toolReclaimWatermark: "tool_reclaim_watermark",
16090
16103
  lastTodoState: "last_todo_state",
16091
16104
  cachedM0Bytes: "cached_m0_bytes",
16092
16105
  cachedM1Bytes: "cached_m1_bytes",
@@ -16363,6 +16376,14 @@ function buildNodeSqliteDatabaseClass(DatabaseSync) {
16363
16376
  }
16364
16377
  super(typeof filename === "string" ? filename : ":memory:", translated);
16365
16378
  }
16379
+ prepare(sql) {
16380
+ const stmt = super.prepare(sql);
16381
+ for (const method of ["run", "get", "all"]) {
16382
+ const original = stmt[method].bind(stmt);
16383
+ stmt[method] = (...args) => args.length === 1 && Array.isArray(args[0]) ? original(...args[0]) : original(...args);
16384
+ }
16385
+ return stmt;
16386
+ }
16366
16387
  transaction(fn) {
16367
16388
  const self = this;
16368
16389
  const wrapped = function(...args) {
@@ -149339,6 +149360,9 @@ function stripCompleteTagPairsGlobally(value) {
149339
149360
  function stripMalformedTagNotationGlobally(value) {
149340
149361
  return value.replace(MALFORMED_TAG_GLOBAL_REGEX, "");
149341
149362
  }
149363
+ function stripDanglingTagNotationGlobally(value) {
149364
+ return value.replace(DANGLING_TAG_GLOBAL_REGEX, "");
149365
+ }
149342
149366
  function stripTagSectionCharacters(value) {
149343
149367
  return value.replace(STRAY_SECTION_CHAR_REGEX, "");
149344
149368
  }
@@ -149346,6 +149370,7 @@ function stripPersistedAssistantText(value) {
149346
149370
  let text = stripWellFormedLeadingTagPrefix(value);
149347
149371
  text = stripCompleteTagPairsGlobally(text);
149348
149372
  text = stripMalformedTagNotationGlobally(text);
149373
+ text = stripDanglingTagNotationGlobally(text);
149349
149374
  text = stripTagSectionCharacters(text);
149350
149375
  return text.trim();
149351
149376
  }
@@ -149358,6 +149383,7 @@ function stripTagPrefix(value) {
149358
149383
  const prev = stripped;
149359
149384
  stripped = stripped.replace(MALFORMED_TAG_PREFIX_REGEX, "");
149360
149385
  stripped = stripped.replace(TAG_PREFIX_REGEX, "");
149386
+ stripped = stripped.replace(DANGLING_TAG_PREFIX_REGEX, "");
149361
149387
  if (stripped === prev)
149362
149388
  break;
149363
149389
  }
@@ -149379,11 +149405,13 @@ function isThinkingPart(part) {
149379
149405
  const candidate = part;
149380
149406
  return candidate.type === "thinking" || candidate.type === "reasoning";
149381
149407
  }
149382
- var encoder, TAG_PREFIX_REGEX, MALFORMED_TAG_PREFIX_REGEX, COMPLETE_TAG_PAIR_GLOBAL_REGEX, MALFORMED_TAG_GLOBAL_REGEX, STRAY_SECTION_CHAR_REGEX;
149408
+ 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;
149383
149409
  var init_tag_content_primitives = __esm(() => {
149384
149410
  encoder = new TextEncoder;
149385
149411
  TAG_PREFIX_REGEX = /^(?:§\d+§\s*)+/;
149386
149412
  MALFORMED_TAG_PREFIX_REGEX = /^(?:§\d+">§(?:\d+§)?\s*)+/;
149413
+ DANGLING_TAG_GLOBAL_REGEX = /\u00a7\d+(?!\.\d)[^\s\u00a7\w.]?/g;
149414
+ DANGLING_TAG_PREFIX_REGEX = /^(?:\u00a7\d+(?!\.\d)[^\s\u00a7\w.]?\s*)+/;
149387
149415
  COMPLETE_TAG_PAIR_GLOBAL_REGEX = /\u00a7\d+\u00a7/g;
149388
149416
  MALFORMED_TAG_GLOBAL_REGEX = /\u00a7\d+">(?:\u00a7(?:\d+\u00a7)?)?/g;
149389
149417
  STRAY_SECTION_CHAR_REGEX = /\u00a7/g;
@@ -149447,12 +149475,13 @@ function setToolContent(part, content) {
149447
149475
  part.content = content;
149448
149476
  }
149449
149477
  }
149450
- function truncateToolPart(part) {
149478
+ function truncateToolPart(part, tagId) {
149451
149479
  if (!isRecord(part))
149452
149480
  return;
149481
+ const sentinel = `[dropped §${tagId}§]`;
149453
149482
  if (part.type === "tool" && isRecord(part.state)) {
149454
149483
  const state = part.state;
149455
- state.output = "[truncated]";
149484
+ state.output = sentinel;
149456
149485
  if (isRecord(state.input)) {
149457
149486
  const inputSize = estimateInputSize(state.input);
149458
149487
  if (inputSize > 500) {
@@ -149462,7 +149491,7 @@ function truncateToolPart(part) {
149462
149491
  return;
149463
149492
  }
149464
149493
  if (part.type === "tool_result") {
149465
- part.content = "[truncated]";
149494
+ part.content = sentinel;
149466
149495
  return;
149467
149496
  }
149468
149497
  if (part.type === "tool-invocation" && isRecord(part.args)) {
@@ -149579,7 +149608,7 @@ class ToolMutationBatch {
149579
149608
  this.affectedMessages.clear();
149580
149609
  }
149581
149610
  }
149582
- function createToolDropTarget(compositeKey, thinkingParts, index, batch) {
149611
+ function createToolDropTarget(compositeKey, thinkingParts, index, batch, tagId) {
149583
149612
  const drop = () => {
149584
149613
  const entry = index.get(compositeKey);
149585
149614
  if (!entry || entry.occurrences.length === 0)
@@ -149600,7 +149629,7 @@ function createToolDropTarget(compositeKey, thinkingParts, index, batch) {
149600
149629
  if (!entry.hasResult)
149601
149630
  return "incomplete";
149602
149631
  for (const occurrence of entry.occurrences) {
149603
- truncateToolPart(occurrence.part);
149632
+ truncateToolPart(occurrence.part, tagId);
149604
149633
  }
149605
149634
  clearThinkingParts(thinkingParts);
149606
149635
  return "truncated";
@@ -151334,6 +151363,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
151334
151363
  ensureColumn(db, "session_meta", "historian_last_failure_at", "INTEGER DEFAULT NULL");
151335
151364
  ensureColumn(db, "session_meta", "system_prompt_hash", "TEXT DEFAULT ''");
151336
151365
  ensureColumn(db, "session_meta", "cleared_reasoning_through_tag", "INTEGER DEFAULT 0");
151366
+ ensureColumn(db, "session_meta", "tool_reclaim_watermark", "INTEGER DEFAULT 0");
151337
151367
  ensureColumn(db, "session_meta", "stripped_placeholder_ids", "TEXT DEFAULT ''");
151338
151368
  ensureColumn(db, "session_meta", "stale_reduce_stripped_ids", "TEXT DEFAULT ''");
151339
151369
  ensureColumn(db, "session_meta", "processed_image_stripped_ids", "TEXT DEFAULT ''");
@@ -154123,7 +154153,8 @@ var exports_storage_meta_session = {};
154123
154153
  __export(exports_storage_meta_session, {
154124
154154
  updateSessionMeta: () => updateSessionMeta,
154125
154155
  getOrCreateSessionMeta: () => getOrCreateSessionMeta,
154126
- clearSession: () => clearSession
154156
+ clearSession: () => clearSession,
154157
+ advanceToolReclaimWatermark: () => advanceToolReclaimWatermark
154127
154158
  });
154128
154159
  import { Buffer as Buffer3 } from "node:buffer";
154129
154160
  function getSessionMetaSelectColumns(db) {
@@ -154184,6 +154215,14 @@ function updateSessionMeta(db, sessionId, updates) {
154184
154215
  db.prepare(`UPDATE session_meta SET ${setClauses.join(", ")} WHERE session_id = ?`).run(...values, sessionId);
154185
154216
  })();
154186
154217
  }
154218
+ function advanceToolReclaimWatermark(db, sessionId, maxTagNumber) {
154219
+ if (maxTagNumber <= 0)
154220
+ return;
154221
+ db.transaction(() => {
154222
+ ensureSessionMetaRow(db, sessionId);
154223
+ db.prepare("UPDATE session_meta SET tool_reclaim_watermark = MAX(COALESCE(tool_reclaim_watermark, 0), ?) WHERE session_id = ?").run(maxTagNumber, sessionId);
154224
+ })();
154225
+ }
154187
154226
  function clearSession(db, sessionId) {
154188
154227
  db.transaction(() => {
154189
154228
  db.prepare("DELETE FROM pending_ops WHERE session_id = ?").run(sessionId);
@@ -154222,6 +154261,7 @@ var init_storage_meta_session = __esm(async () => {
154222
154261
  last_transform_error: "'' AS last_transform_error",
154223
154262
  system_prompt_hash: "'' AS system_prompt_hash",
154224
154263
  last_todo_state: "'' AS last_todo_state",
154264
+ tool_reclaim_watermark: "0 AS tool_reclaim_watermark",
154225
154265
  cached_m0_bytes: "NULL AS cached_m0_bytes",
154226
154266
  cached_m1_bytes: "NULL AS cached_m1_bytes",
154227
154267
  cached_m0_project_memory_epoch: "NULL AS cached_m0_project_memory_epoch",
@@ -154606,15 +154646,22 @@ function ownerMessageIdForTagRow(row) {
154606
154646
  }
154607
154647
  return row.message_id.replace(CONTENT_ID_SUFFIX, "");
154608
154648
  }
154609
- function getActiveTagTokenAggregate(db, sessionId) {
154610
- const row = db.prepare(`SELECT
154649
+ function getActiveTagTokenAggregate(db, sessionId, protectedTags = 0) {
154650
+ const toolOutputExpr = protectedTags > 0 ? `COALESCE(SUM(CASE WHEN type = 'tool' AND tag_number < (
154651
+ SELECT tag_number FROM tags
154652
+ WHERE session_id = ? AND status = 'active'
154653
+ ORDER BY tag_number DESC LIMIT 1 OFFSET ?
154654
+ ) THEN COALESCE(token_count, 0) ELSE 0 END), 0)` : `COALESCE(SUM(CASE WHEN type = 'tool' THEN COALESCE(token_count, 0) ELSE 0 END), 0)`;
154655
+ const sql = `SELECT
154611
154656
  COALESCE(SUM(CASE WHEN type != 'tool' THEN COALESCE(token_count, 0) ELSE 0 END), 0)
154612
154657
  + COALESCE(SUM(COALESCE(reasoning_token_count, 0)), 0) AS conversation,
154613
154658
  COALESCE(SUM(CASE WHEN type = 'tool' THEN COALESCE(token_count, 0) + COALESCE(input_token_count, 0) ELSE 0 END), 0) AS tool_call,
154614
- COALESCE(SUM(CASE WHEN type = 'tool' THEN COALESCE(token_count, 0) ELSE 0 END), 0) AS tool_output,
154659
+ ${toolOutputExpr} AS tool_output,
154615
154660
  COALESCE(SUM(CASE WHEN token_count IS NULL THEN 1 ELSE 0 END), 0) AS null_count
154616
154661
  FROM tags
154617
- WHERE session_id = ? AND status = 'active'`).get(sessionId);
154662
+ WHERE session_id = ? AND status = 'active'`;
154663
+ const params = protectedTags > 0 ? [sessionId, protectedTags - 1, sessionId] : [sessionId];
154664
+ const row = db.prepare(sql).get(...params);
154618
154665
  return {
154619
154666
  conversation: row?.conversation ?? 0,
154620
154667
  toolCall: row?.tool_call ?? 0,
@@ -154622,6 +154669,26 @@ function getActiveTagTokenAggregate(db, sessionId) {
154622
154669
  nullCount: row?.null_count ?? 0
154623
154670
  };
154624
154671
  }
154672
+ function getOldestActiveUnprotectedToolTags(db, sessionId, protectedTags = 0, limit = 4) {
154673
+ if (limit <= 0)
154674
+ return [];
154675
+ const boundedLimit = Math.max(1, Math.min(10, Math.floor(limit)));
154676
+ const whereProtected = protectedTags > 0 ? `AND tag_number < (
154677
+ SELECT tag_number FROM tags
154678
+ WHERE session_id = ? AND status = 'active'
154679
+ ORDER BY tag_number DESC LIMIT 1 OFFSET ?
154680
+ )` : "";
154681
+ const params = protectedTags > 0 ? [sessionId, sessionId, protectedTags - 1, boundedLimit] : [sessionId, boundedLimit];
154682
+ const rows = db.prepare(`SELECT tag_number, tool_name
154683
+ FROM tags
154684
+ WHERE session_id = ? AND status = 'active' AND type = 'tool' ${whereProtected}
154685
+ ORDER BY tag_number ASC, id ASC
154686
+ LIMIT ?`).all(...params);
154687
+ return rows.filter((row) => typeof row.tag_number === "number").map((row) => ({
154688
+ tagNumber: row.tag_number,
154689
+ toolName: typeof row.tool_name === "string" ? row.tool_name : null
154690
+ }));
154691
+ }
154625
154692
  function getTriggerTagTokenUpperBound(db, sessionId) {
154626
154693
  const row = db.prepare(`SELECT
154627
154694
  COALESCE(SUM(COALESCE(token_count, 0) + COALESCE(input_token_count, 0) + COALESCE(reasoning_token_count, 0)), 0) AS bound,
@@ -163965,6 +164032,58 @@ var init_safe_notification_target = __esm(() => {
163965
164032
  DEFAULT_TITLE_RE = /^(New session - |Child session - )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
163966
164033
  });
163967
164034
 
164035
+ // src/shared/rpc-notifications.ts
164036
+ var exports_rpc_notifications = {};
164037
+ __export(exports_rpc_notifications, {
164038
+ pushNotification: () => pushNotification,
164039
+ isTuiConnected: () => isTuiConnected,
164040
+ drainNotifications: () => drainNotifications
164041
+ });
164042
+ function pushNotification(type, payload, sessionId) {
164043
+ queue.push({ id: nextNotificationId++, type, payload, sessionId });
164044
+ if (queue.length > 100) {
164045
+ const newestPerSession = new Map;
164046
+ for (const n of queue) {
164047
+ const prev = newestPerSession.get(n.sessionId);
164048
+ if (prev === undefined || n.id > prev) {
164049
+ newestPerSession.set(n.sessionId, n.id);
164050
+ }
164051
+ }
164052
+ const mustKeep = new Set(newestPerSession.values());
164053
+ const byNewest = [...queue].sort((a, b) => b.id - a.id);
164054
+ const kept = [];
164055
+ for (const n of byNewest) {
164056
+ if (kept.length < 50 || mustKeep.has(n.id))
164057
+ kept.push(n);
164058
+ }
164059
+ queue = kept.sort((a, b) => a.id - b.id);
164060
+ }
164061
+ }
164062
+ function drainNotifications(lastReceivedId = 0, sessionId) {
164063
+ const now = Date.now();
164064
+ lastDrainAtAny = now;
164065
+ if (sessionId !== undefined)
164066
+ lastDrainAtBySession.set(sessionId, now);
164067
+ const matchesClient = (notification) => sessionId === undefined || notification.sessionId === undefined || notification.sessionId === sessionId;
164068
+ if (lastReceivedId > 0) {
164069
+ queue = queue.filter((notification) => !(notification.id <= lastReceivedId && matchesClient(notification)));
164070
+ }
164071
+ return queue.filter((notification) => notification.id > lastReceivedId && matchesClient(notification));
164072
+ }
164073
+ function isTuiConnected(sessionId) {
164074
+ const now = Date.now();
164075
+ if (sessionId !== undefined) {
164076
+ const at = lastDrainAtBySession.get(sessionId) ?? 0;
164077
+ return at > 0 && now - at < TUI_CONNECTED_WINDOW_MS;
164078
+ }
164079
+ return lastDrainAtAny > 0 && now - lastDrainAtAny < TUI_CONNECTED_WINDOW_MS;
164080
+ }
164081
+ var queue, nextNotificationId = 1, lastDrainAtBySession, lastDrainAtAny = 0, TUI_CONNECTED_WINDOW_MS = 3000;
164082
+ var init_rpc_notifications = __esm(() => {
164083
+ queue = [];
164084
+ lastDrainAtBySession = new Map;
164085
+ });
164086
+
163968
164087
  // src/plugin/conflict-warning-hook.ts
163969
164088
  var exports_conflict_warning_hook = {};
163970
164089
  __export(exports_conflict_warning_hook, {
@@ -164299,6 +164418,9 @@ async function sendStartupAnnouncement(client, directory, version2, features, fo
164299
164418
  if (!sessionId) {
164300
164419
  return;
164301
164420
  }
164421
+ const { isTuiConnected: isTuiConnected2 } = await Promise.resolve().then(() => (init_rpc_notifications(), exports_rpc_notifications));
164422
+ if (isTuiConnected2(sessionId) || isTuiConnected2())
164423
+ return;
164302
164424
  if (await waitForSafeNotificationTarget(client, sessionId) === "skip")
164303
164425
  return;
164304
164426
  const bullets = features.map((line) => ` • ${line}`).join(`
@@ -164521,6 +164643,20 @@ function getDistinctStoredModelIds(db, projectPath) {
164521
164643
  const rows = getDistinctStoredModelIdsStatement(db).all(projectPath);
164522
164644
  return new Set(rows.map((row) => typeof row.modelId === "string" ? row.modelId : null));
164523
164645
  }
164646
+ function getMemoryEmbedCoverage(db, projectPath, modelId) {
164647
+ const row = db.prepare(`SELECT
164648
+ COUNT(*) AS total,
164649
+ SUM(CASE WHEN EXISTS (
164650
+ SELECT 1 FROM memory_embeddings e
164651
+ WHERE e.memory_id = m.id AND e.model_id = ?
164652
+ ) THEN 1 ELSE 0 END) AS embedded
164653
+ FROM memories m
164654
+ WHERE m.project_path = ? AND m.status = 'active'`).get(modelId, projectPath);
164655
+ return {
164656
+ total: typeof row?.total === "number" ? row.total : 0,
164657
+ embedded: typeof row?.embedded === "number" ? row.embedded : 0
164658
+ };
164659
+ }
164524
164660
  var saveEmbeddingStatements, loadAllEmbeddingsStatements, deleteEmbeddingStatements, getStoredModelIdStatements, clearAllEmbeddingsStatements, getDistinctStoredModelIdsStatements;
164525
164661
  var init_storage_memory_embeddings = __esm(() => {
164526
164662
  saveEmbeddingStatements = new WeakMap;
@@ -165369,9 +165505,10 @@ function chunkCanonicalText(canonicalText, startOrdinal, endOrdinal, maxInputTok
165369
165505
  if (lines.length === 0 || endOrdinal < startOrdinal)
165370
165506
  return [];
165371
165507
  const normalizedMax = normalizeCompartmentChunkMaxInputTokens(maxInputTokens);
165508
+ const effectiveMax = Math.max(1, Math.floor(normalizedMax * CHUNK_WINDOW_SAFETY_RATIO));
165372
165509
  const fullText = lines.join(`
165373
165510
  `);
165374
- if (estimateTokens(fullText) <= normalizedMax) {
165511
+ if (estimateTokens(fullText) <= effectiveMax) {
165375
165512
  return [
165376
165513
  {
165377
165514
  windowIndex: 0,
@@ -165409,7 +165546,7 @@ function chunkCanonicalText(canonicalText, startOrdinal, endOrdinal, maxInputTok
165409
165546
  const lineStart = range?.start ?? startOrdinal;
165410
165547
  const lineEnd = range?.end ?? lineStart;
165411
165548
  const lineTokens = estimateTokens(line);
165412
- if (currentLines.length > 0 && currentTokens + lineTokens > normalizedMax) {
165549
+ if (currentLines.length > 0 && currentTokens + lineTokens > effectiveMax) {
165413
165550
  flush2();
165414
165551
  }
165415
165552
  if (currentLines.length === 0) {
@@ -165564,7 +165701,29 @@ function countUnembeddedSessionCompartments(db, projectPath, sessionId, modelId)
165564
165701
  )`).get(projectPath, sessionId, projectPath, modelId);
165565
165702
  return typeof row?.n === "number" ? row.n : 0;
165566
165703
  }
165567
- var DEFAULT_COMPARTMENT_CHUNK_MAX_INPUT_TOKENS = 512, loadFtsRowsStatements, existingHashStatements, existingHashByProjectStatements, deleteByCompartmentStatements, insertEmbeddingStatements, distinctModelStatements, clearProjectStatements, clearProjectModelStatements, searchRowsStatements, searchRowsByModelStatements, backfillCandidateStatements, sessionBackfillCandidateStatements;
165704
+ function countSessionCompartmentEmbedCoverage(db, projectPath, sessionId, modelId) {
165705
+ const row = db.prepare(`SELECT
165706
+ COUNT(*) AS total,
165707
+ SUM(CASE WHEN EXISTS (
165708
+ SELECT 1 FROM compartment_chunk_embeddings e
165709
+ WHERE e.compartment_id = c.id
165710
+ AND e.project_path = ?
165711
+ AND e.model_id = ?
165712
+ ) THEN 1 ELSE 0 END) AS embedded
165713
+ FROM compartments c
165714
+ JOIN session_projects sp
165715
+ ON sp.session_id = c.session_id
165716
+ AND sp.harness = c.harness
165717
+ AND sp.project_path = ?
165718
+ WHERE c.session_id = ?
165719
+ AND c.start_message IS NOT NULL
165720
+ AND c.end_message IS NOT NULL`).get(projectPath, modelId, projectPath, sessionId);
165721
+ return {
165722
+ total: typeof row?.total === "number" ? row.total : 0,
165723
+ embedded: typeof row?.embedded === "number" ? row.embedded : 0
165724
+ };
165725
+ }
165726
+ 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;
165568
165727
  var init_compartment_chunk_embedding = __esm(() => {
165569
165728
  init_read_session_formatting();
165570
165729
  loadFtsRowsStatements = new WeakMap;
@@ -165721,6 +165880,18 @@ async function withQuietConsole(fn) {
165721
165880
  console.error = origError;
165722
165881
  }
165723
165882
  }
165883
+ function isNativeRuntimeMissingError(error51) {
165884
+ const message = error51 instanceof Error ? error51.message : String(error51 ?? "");
165885
+ const lower = message.toLowerCase();
165886
+ const code = error51?.code;
165887
+ const name2 = error51?.name;
165888
+ if (code === "ERR_DLOPEN_FAILED" && lower.includes("onnxruntime")) {
165889
+ return true;
165890
+ }
165891
+ if (!lower.includes("onnxruntime-node"))
165892
+ return false;
165893
+ return code === "ERR_MODULE_NOT_FOUND" || name2 === "ResolveMessage" || lower.includes("cannot find package") || lower.includes("cannot find module") || lower.includes("err_module_not_found");
165894
+ }
165724
165895
  function isTransientLoadError(error51) {
165725
165896
  const message = error51 instanceof Error ? error51.message : String(error51 ?? "");
165726
165897
  if (!message)
@@ -165785,6 +165956,9 @@ class LocalEmbeddingProvider {
165785
165956
  if (this.pipeline) {
165786
165957
  return true;
165787
165958
  }
165959
+ if (nativeRuntimeMissing) {
165960
+ return false;
165961
+ }
165788
165962
  if (this.initPromise) {
165789
165963
  await this.initPromise;
165790
165964
  return this.pipeline !== null;
@@ -165851,7 +166025,12 @@ class LocalEmbeddingProvider {
165851
166025
  await releaseLock();
165852
166026
  }
165853
166027
  } catch (error51) {
165854
- log("[magic-context] embedding model failed to load:", error51);
166028
+ if (isNativeRuntimeMissingError(error51)) {
166029
+ nativeRuntimeMissing = true;
166030
+ 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 @cortexkit/magic-context@latest doctor --force`), or configure an `openai-compatible`/`ollama` embedding endpoint instead. Existing memories are unaffected.");
166031
+ } else {
166032
+ log("[magic-context] embedding model failed to load:", error51);
166033
+ }
165855
166034
  this.pipeline = null;
165856
166035
  } finally {
165857
166036
  this.initPromise = null;
@@ -165962,7 +166141,7 @@ class LocalEmbeddingProvider {
165962
166141
  return this.pipeline !== null;
165963
166142
  }
165964
166143
  }
165965
- var LOCK_POLL_MS = 150, STALE_LOCK_MS, MAX_LOCK_WAIT_MS;
166144
+ var LOCK_POLL_MS = 150, STALE_LOCK_MS, MAX_LOCK_WAIT_MS, nativeRuntimeMissing = false;
165966
166145
  var init_embedding_local = __esm(() => {
165967
166146
  init_magic_context();
165968
166147
  init_data_path();
@@ -166032,6 +166211,13 @@ var init_embedding_ssrf = __esm(() => {
166032
166211
  function normalizeEndpoint3(endpoint) {
166033
166212
  return endpoint?.trim().replace(/\/+$/, "") ?? "";
166034
166213
  }
166214
+ function embeddingModelsMatch(served, requested) {
166215
+ const a = served.trim().toLowerCase();
166216
+ const b = requested.trim().toLowerCase();
166217
+ if (a.length === 0 || b.length === 0)
166218
+ return true;
166219
+ return a === b || a.includes(b) || b.includes(a);
166220
+ }
166035
166221
 
166036
166222
  class OpenAICompatibleEmbeddingProvider {
166037
166223
  modelId;
@@ -166045,6 +166231,7 @@ class OpenAICompatibleEmbeddingProvider {
166045
166231
  failureTimes = [];
166046
166232
  circuitOpenUntil = 0;
166047
166233
  openLogged = false;
166234
+ modelMismatchLogged = false;
166048
166235
  halfOpenProbeInFlight = false;
166049
166236
  constructor(options) {
166050
166237
  this.endpoint = normalizeEndpoint3(options.endpoint);
@@ -166143,6 +166330,15 @@ class OpenAICompatibleEmbeddingProvider {
166143
166330
  this.recordFailure(isProbe);
166144
166331
  return Array.from({ length: texts.length }, () => null);
166145
166332
  }
166333
+ const servedModel = typeof body.model === "string" ? body.model : "";
166334
+ if (this.model && servedModel && !embeddingModelsMatch(servedModel, this.model)) {
166335
+ if (!this.modelMismatchLogged) {
166336
+ 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.`);
166337
+ this.modelMismatchLogged = true;
166338
+ }
166339
+ this.recordFailure(isProbe);
166340
+ return Array.from({ length: texts.length }, () => null);
166341
+ }
166146
166342
  const items = Array.isArray(body.data) ? body.data : [];
166147
166343
  const results = Array.from({ length: texts.length }, (_, index) => {
166148
166344
  const embedding = items[index]?.embedding;
@@ -166359,6 +166555,121 @@ var init_storage_git_commit_embeddings = __esm(() => {
166359
166555
  distinctModelIdStatements = new WeakMap;
166360
166556
  });
166361
166557
 
166558
+ // src/features/magic-context/git-commits/storage-git-commits.ts
166559
+ function getInsertStatement(db) {
166560
+ let stmt = insertStatements.get(db);
166561
+ if (!stmt) {
166562
+ stmt = db.prepare(`INSERT INTO git_commits (sha, project_path, short_sha, message, author, committed_at, indexed_at)
166563
+ VALUES (?, ?, ?, ?, ?, ?, ?)
166564
+ ON CONFLICT(sha) DO UPDATE SET
166565
+ project_path = excluded.project_path,
166566
+ short_sha = excluded.short_sha,
166567
+ message = excluded.message,
166568
+ author = excluded.author,
166569
+ committed_at = excluded.committed_at,
166570
+ indexed_at = excluded.indexed_at
166571
+ WHERE git_commits.message != excluded.message`);
166572
+ insertStatements.set(db, stmt);
166573
+ }
166574
+ return stmt;
166575
+ }
166576
+ function getExistingShasStatement(db) {
166577
+ let stmt = existingShasStatements.get(db);
166578
+ if (!stmt) {
166579
+ stmt = db.prepare("SELECT sha FROM git_commits WHERE project_path = ?");
166580
+ existingShasStatements.set(db, stmt);
166581
+ }
166582
+ return stmt;
166583
+ }
166584
+ function getProjectCountStatement(db) {
166585
+ let stmt = projectCountStatements.get(db);
166586
+ if (!stmt) {
166587
+ stmt = db.prepare("SELECT COUNT(*) AS count FROM git_commits WHERE project_path = ?");
166588
+ projectCountStatements.set(db, stmt);
166589
+ }
166590
+ return stmt;
166591
+ }
166592
+ function getLatestCommitTimeStatement(db) {
166593
+ let stmt = latestCommitTimeStatements.get(db);
166594
+ if (!stmt) {
166595
+ stmt = db.prepare("SELECT MAX(committed_at) AS latest FROM git_commits WHERE project_path = ?");
166596
+ latestCommitTimeStatements.set(db, stmt);
166597
+ }
166598
+ return stmt;
166599
+ }
166600
+ function getEvictOverflowStatement(db) {
166601
+ let stmt = evictOverflowStatements.get(db);
166602
+ if (!stmt) {
166603
+ stmt = db.prepare(`DELETE FROM git_commits
166604
+ WHERE rowid IN (
166605
+ SELECT rowid FROM git_commits
166606
+ WHERE project_path = ?
166607
+ ORDER BY committed_at DESC, sha DESC
166608
+ LIMIT -1 OFFSET ?
166609
+ )`);
166610
+ evictOverflowStatements.set(db, stmt);
166611
+ }
166612
+ return stmt;
166613
+ }
166614
+ function upsertCommits(db, projectPath, commits) {
166615
+ if (commits.length === 0)
166616
+ return { inserted: 0, updated: 0 };
166617
+ const existing = new Set;
166618
+ for (const row of getExistingShasStatement(db).all(projectPath)) {
166619
+ existing.add(row.sha);
166620
+ }
166621
+ let inserted = 0;
166622
+ let updated = 0;
166623
+ const now = Date.now();
166624
+ const insertStmt = getInsertStatement(db);
166625
+ db.transaction(() => {
166626
+ for (const commit of commits) {
166627
+ const result = insertStmt.run(commit.sha, projectPath, commit.shortSha, commit.message, commit.author, commit.committedAtMs, now);
166628
+ if (result.changes > 0) {
166629
+ if (existing.has(commit.sha)) {
166630
+ updated++;
166631
+ } else {
166632
+ inserted++;
166633
+ existing.add(commit.sha);
166634
+ }
166635
+ }
166636
+ }
166637
+ })();
166638
+ return { inserted, updated };
166639
+ }
166640
+ function getCommitCount(db, projectPath) {
166641
+ const row = getProjectCountStatement(db).get(projectPath);
166642
+ return row?.count ?? 0;
166643
+ }
166644
+ function getLatestIndexedCommitTimeMs(db, projectPath) {
166645
+ const row = getLatestCommitTimeStatement(db).get(projectPath);
166646
+ return row?.latest ?? null;
166647
+ }
166648
+ function enforceProjectCap(db, projectPath, maxCommits) {
166649
+ if (maxCommits <= 0)
166650
+ return 0;
166651
+ const count = getCommitCount(db, projectPath);
166652
+ if (count <= maxCommits)
166653
+ return 0;
166654
+ getEvictOverflowStatement(db).run(projectPath, maxCommits);
166655
+ const after = getCommitCount(db, projectPath);
166656
+ const evicted = Math.max(0, count - after);
166657
+ if (evicted > 0) {
166658
+ log(`[git-commits] evicted ${evicted} oldest commits for project ${projectPath} (cap=${maxCommits}, was=${count})`);
166659
+ }
166660
+ return evicted;
166661
+ }
166662
+ var insertStatements, existingShasStatements, projectCountStatements, evictStatements, evictOverflowStatements, latestCommitTimeStatements;
166663
+ var init_storage_git_commits = __esm(() => {
166664
+ init_logger();
166665
+ insertStatements = new WeakMap;
166666
+ existingShasStatements = new WeakMap;
166667
+ projectCountStatements = new WeakMap;
166668
+ evictStatements = new WeakMap;
166669
+ evictOverflowStatements = new WeakMap;
166670
+ latestCommitTimeStatements = new WeakMap;
166671
+ });
166672
+
166362
166673
  // src/features/magic-context/git-commits/sweep-coordinator.ts
166363
166674
  function runImmediate2(db, body) {
166364
166675
  db.exec("BEGIN IMMEDIATE");
@@ -166629,7 +166940,7 @@ function getChunkEmbeddingModelId(config2, providerIdentity) {
166629
166940
  }
166630
166941
  const chunkIdentity = {
166631
166942
  providerIdentity,
166632
- chunkerVersion: 1,
166943
+ chunkerVersion: 2,
166633
166944
  maxInputTokens: normalizeCompartmentChunkMaxInputTokens("max_input_tokens" in config2 ? config2.max_input_tokens : undefined),
166634
166945
  truncate: config2.provider === "openai-compatible" ? config2.truncate ?? "" : ""
166635
166946
  };
@@ -166652,7 +166963,9 @@ function snapshotFor(registration) {
166652
166963
  enabled,
166653
166964
  gitCommitEnabled,
166654
166965
  modelId: registration.observationMode || !providerIsOn ? "off" : registration.modelId,
166655
- chunkModelId: registration.observationMode || !providerIsOn ? "off" : registration.chunkModelId
166966
+ chunkModelId: registration.observationMode || !providerIsOn ? "off" : registration.chunkModelId,
166967
+ model: registration.observationMode || !providerIsOn ? "off" : ("model" in registration.config) && registration.config.model.trim() ? registration.config.model.trim() : registration.modelId,
166968
+ provider: registration.observationMode || !providerIsOn ? "off" : registration.config.provider ?? "local"
166656
166969
  };
166657
166970
  }
166658
166971
  function disposeProvider(provider) {
@@ -166863,8 +167176,9 @@ async function embedUnembeddedMemoriesForProject(db, projectIdentity, batchSize
166863
167176
  }
166864
167177
  async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates, signal) {
166865
167178
  const noWork = [];
167179
+ const failed = [];
166866
167180
  if (candidates.length === 0)
166867
- return { embedded: 0, noWork };
167181
+ return { embedded: 0, noWork, failed };
166868
167182
  const maxInputTokens = getProjectEmbeddingMaxInputTokens(projectIdentity);
166869
167183
  const prepared = [];
166870
167184
  for (const candidate of candidates) {
@@ -166881,7 +167195,7 @@ async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates
166881
167195
  prepared.push({ candidate, windows });
166882
167196
  }
166883
167197
  if (prepared.length === 0)
166884
- return { embedded: 0, noWork };
167198
+ return { embedded: 0, noWork, failed };
166885
167199
  let embedded = 0;
166886
167200
  let i = 0;
166887
167201
  while (i < prepared.length) {
@@ -166898,35 +167212,60 @@ async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates
166898
167212
  const texts = [];
166899
167213
  for (const item of slice)
166900
167214
  texts.push(...item.windows.map((w) => w.text));
166901
- try {
166902
- const result = await embedBatchForProject(projectIdentity, texts, signal);
166903
- if (!result)
166904
- continue;
167215
+ const persistedIds = new Set;
167216
+ for (let attempt = 0;attempt < EMBED_SLICE_RETRY_ATTEMPTS; attempt++) {
166905
167217
  if (signal?.aborted)
166906
167218
  break;
166907
- let offset = 0;
166908
- for (const item of slice) {
166909
- const vectors = result.vectors.slice(offset, offset + item.windows.length);
166910
- offset += item.windows.length;
166911
- if (vectors.length !== item.windows.length || vectors.some((v) => !v)) {
166912
- continue;
167219
+ let result = null;
167220
+ const attemptStart = Date.now();
167221
+ try {
167222
+ result = await embedBatchForProject(projectIdentity, texts, signal);
167223
+ } catch (error51) {
167224
+ log("[magic-context] failed to proactively embed compartment chunks:", error51);
167225
+ }
167226
+ if (signal?.aborted)
167227
+ break;
167228
+ if (result) {
167229
+ let offset = 0;
167230
+ for (const item of slice) {
167231
+ const vectors = result.vectors.slice(offset, offset + item.windows.length);
167232
+ offset += item.windows.length;
167233
+ if (persistedIds.has(item.candidate.id))
167234
+ continue;
167235
+ if (vectors.length !== item.windows.length || vectors.some((v) => !v)) {
167236
+ continue;
167237
+ }
167238
+ const rows = item.windows.map((window, index) => ({
167239
+ compartmentId: item.candidate.id,
167240
+ sessionId: item.candidate.sessionId,
167241
+ projectPath: projectIdentity,
167242
+ window,
167243
+ modelId,
167244
+ vector: vectors[index]
167245
+ }));
167246
+ replaceCompartmentChunkEmbeddings(db, rows);
167247
+ persistedIds.add(item.candidate.id);
166913
167248
  }
166914
- const rows = item.windows.map((window, index) => ({
166915
- compartmentId: item.candidate.id,
166916
- sessionId: item.candidate.sessionId,
166917
- projectPath: projectIdentity,
166918
- window,
166919
- modelId,
166920
- vector: vectors[index]
166921
- }));
166922
- replaceCompartmentChunkEmbeddings(db, rows);
166923
- embedded += 1;
166924
167249
  }
166925
- } catch (error51) {
166926
- log("[magic-context] failed to proactively embed compartment chunks:", error51);
167250
+ if (persistedIds.size === slice.length)
167251
+ break;
167252
+ if (persistedIds.size > 0)
167253
+ break;
167254
+ if (Date.now() - attemptStart >= EMBED_SLOW_FAILURE_NO_RETRY_MS)
167255
+ break;
167256
+ if (attempt < EMBED_SLICE_RETRY_ATTEMPTS - 1) {
167257
+ await new Promise((resolve7) => setTimeout(resolve7, EMBED_SLICE_RETRY_BASE_MS * 2 ** attempt));
167258
+ }
167259
+ }
167260
+ embedded += persistedIds.size;
167261
+ if (!signal?.aborted) {
167262
+ for (const item of slice) {
167263
+ if (!persistedIds.has(item.candidate.id))
167264
+ failed.push(item.candidate.id);
167265
+ }
166927
167266
  }
166928
167267
  }
166929
- return { embedded, noWork };
167268
+ return { embedded, noWork, failed };
166930
167269
  }
166931
167270
  async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, options) {
166932
167271
  const snapshot = getProjectEmbeddingSnapshot(projectIdentity);
@@ -166949,9 +167288,11 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
166949
167288
  renewal.unref?.();
166950
167289
  const batchSize = Math.max(1, options?.batchSize ?? CHUNK_DRAIN_BATCH_SIZE);
166951
167290
  const skipIds = [];
167291
+ const failedIds = [];
166952
167292
  let embedded = 0;
166953
167293
  let aborted2 = false;
166954
- let providerStalled = false;
167294
+ let providerDown = false;
167295
+ let consecutiveFailedBatches = 0;
166955
167296
  try {
166956
167297
  options?.onProgress?.({ embedded, total });
166957
167298
  for (;; ) {
@@ -166959,15 +167300,26 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
166959
167300
  aborted2 = true;
166960
167301
  break;
166961
167302
  }
166962
- const candidates = loadUnembeddedSessionChunkCandidates(db, projectIdentity, sessionId, snapshot.chunkModelId, batchSize, skipIds);
167303
+ const candidates = loadUnembeddedSessionChunkCandidates(db, projectIdentity, sessionId, snapshot.chunkModelId, batchSize, [...skipIds, ...failedIds]);
166963
167304
  if (candidates.length === 0)
166964
167305
  break;
166965
- const { embedded: n, noWork } = await embedCandidateChunkBatch(db, projectIdentity, snapshot.chunkModelId, candidates, options?.signal);
167306
+ const {
167307
+ embedded: n,
167308
+ noWork,
167309
+ failed
167310
+ } = await embedCandidateChunkBatch(db, projectIdentity, snapshot.chunkModelId, candidates, options?.signal);
166966
167311
  for (const id of noWork)
166967
167312
  skipIds.push(id);
167313
+ for (const id of failed)
167314
+ failedIds.push(id);
166968
167315
  if (n === 0 && noWork.length === 0) {
166969
- providerStalled = true;
166970
- break;
167316
+ consecutiveFailedBatches += 1;
167317
+ if (consecutiveFailedBatches >= MAX_CONSECUTIVE_FAILED_BATCHES) {
167318
+ providerDown = true;
167319
+ break;
167320
+ }
167321
+ } else {
167322
+ consecutiveFailedBatches = 0;
166971
167323
  }
166972
167324
  embedded += n;
166973
167325
  options?.onProgress?.({ embedded: Math.min(embedded, total), total });
@@ -166975,23 +167327,58 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
166975
167327
  }
166976
167328
  } finally {
166977
167329
  clearInterval(renewal);
166978
- releaseGitSweepLease(db, projectIdentity, holderId);
167330
+ try {
167331
+ releaseGitSweepLease(db, projectIdentity, holderId);
167332
+ } catch (error51) {
167333
+ log("[magic-context] embed drain: lease release failed (will TTL-expire):", error51);
167334
+ }
166979
167335
  }
166980
167336
  if (aborted2)
166981
- return { status: "aborted", embedded, total };
166982
- if (providerStalled) {
167337
+ return { status: "aborted", embedded, total, failed: failedIds.length };
167338
+ if (providerDown || failedIds.length > 0) {
166983
167339
  const remaining = Math.max(0, countUnembeddedSessionCompartments(db, projectIdentity, sessionId, snapshot.chunkModelId) - skipIds.length);
166984
- if (remaining > 0)
166985
- return { status: "stalled", embedded, total, remaining };
167340
+ if (remaining > 0) {
167341
+ return { status: "stalled", embedded, total, remaining, failed: failedIds.length };
167342
+ }
166986
167343
  }
166987
- return { status: "done", embedded, total };
167344
+ return { status: "done", embedded, total, failed: failedIds.length };
167345
+ }
167346
+ function getEmbeddingCoverageStatus(db, projectIdentity, sessionId) {
167347
+ const snapshot = getProjectEmbeddingSnapshot(projectIdentity);
167348
+ if (!snapshot?.enabled || snapshot.chunkModelId === "off") {
167349
+ return {
167350
+ enabled: false,
167351
+ model: snapshot?.model ?? "off",
167352
+ provider: snapshot?.provider ?? "off",
167353
+ session: { embedded: 0, total: 0 },
167354
+ memories: { embedded: 0, total: 0 },
167355
+ commits: { embedded: 0, total: 0, gitEnabled: false }
167356
+ };
167357
+ }
167358
+ const session = countSessionCompartmentEmbedCoverage(db, projectIdentity, sessionId, snapshot.chunkModelId);
167359
+ const memories = getMemoryEmbedCoverage(db, projectIdentity, snapshot.modelId);
167360
+ const gitEnabled = snapshot.gitCommitEnabled;
167361
+ const commits = gitEnabled ? {
167362
+ embedded: countEmbeddedCommits(db, projectIdentity),
167363
+ total: getCommitCount(db, projectIdentity),
167364
+ gitEnabled: true
167365
+ } : { embedded: 0, total: 0, gitEnabled: false };
167366
+ return {
167367
+ enabled: true,
167368
+ model: snapshot.model,
167369
+ provider: snapshot.provider,
167370
+ session,
167371
+ memories,
167372
+ commits
167373
+ };
166988
167374
  }
166989
- 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;
167375
+ 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;
166990
167376
  var init_project_embedding_registry = __esm(() => {
166991
167377
  init_magic_context();
166992
167378
  init_logger();
166993
167379
  init_compartment_chunk_embedding();
166994
167380
  init_storage_git_commit_embeddings();
167381
+ init_storage_git_commits();
166995
167382
  init_sweep_coordinator();
166996
167383
  init_embedding_cache();
166997
167384
  init_embedding_identity();
@@ -167272,58 +167659,6 @@ var init_models_dev_cache = __esm(() => {
167272
167659
  init_logger();
167273
167660
  });
167274
167661
 
167275
- // src/shared/rpc-notifications.ts
167276
- var exports_rpc_notifications = {};
167277
- __export(exports_rpc_notifications, {
167278
- pushNotification: () => pushNotification,
167279
- isTuiConnected: () => isTuiConnected,
167280
- drainNotifications: () => drainNotifications
167281
- });
167282
- function pushNotification(type, payload, sessionId) {
167283
- queue2.push({ id: nextNotificationId++, type, payload, sessionId });
167284
- if (queue2.length > 100) {
167285
- const newestPerSession = new Map;
167286
- for (const n of queue2) {
167287
- const prev = newestPerSession.get(n.sessionId);
167288
- if (prev === undefined || n.id > prev) {
167289
- newestPerSession.set(n.sessionId, n.id);
167290
- }
167291
- }
167292
- const mustKeep = new Set(newestPerSession.values());
167293
- const byNewest = [...queue2].sort((a, b) => b.id - a.id);
167294
- const kept = [];
167295
- for (const n of byNewest) {
167296
- if (kept.length < 50 || mustKeep.has(n.id))
167297
- kept.push(n);
167298
- }
167299
- queue2 = kept.sort((a, b) => a.id - b.id);
167300
- }
167301
- }
167302
- function drainNotifications(lastReceivedId = 0, sessionId) {
167303
- const now = Date.now();
167304
- lastDrainAtAny = now;
167305
- if (sessionId !== undefined)
167306
- lastDrainAtBySession.set(sessionId, now);
167307
- const matchesClient = (notification) => sessionId === undefined || notification.sessionId === undefined || notification.sessionId === sessionId;
167308
- if (lastReceivedId > 0) {
167309
- queue2 = queue2.filter((notification) => !(notification.id <= lastReceivedId && matchesClient(notification)));
167310
- }
167311
- return queue2.filter((notification) => notification.id > lastReceivedId && matchesClient(notification));
167312
- }
167313
- function isTuiConnected(sessionId) {
167314
- const now = Date.now();
167315
- if (sessionId !== undefined) {
167316
- const at = lastDrainAtBySession.get(sessionId) ?? 0;
167317
- return at > 0 && now - at < TUI_CONNECTED_WINDOW_MS;
167318
- }
167319
- return lastDrainAtAny > 0 && now - lastDrainAtAny < TUI_CONNECTED_WINDOW_MS;
167320
- }
167321
- var queue2, nextNotificationId = 1, lastDrainAtBySession, lastDrainAtAny = 0, TUI_CONNECTED_WINDOW_MS = 3000;
167322
- var init_rpc_notifications = __esm(() => {
167323
- queue2 = [];
167324
- lastDrainAtBySession = new Map;
167325
- });
167326
-
167327
167662
  // src/features/magic-context/compartment-embedding.ts
167328
167663
  async function embedAndStoreCompartmentChunks(db, sessionId, projectPath, compartments) {
167329
167664
  if (compartments.length === 0)
@@ -169268,7 +169603,7 @@ ${prepared.block}
169268
169603
  if (!firstMessage || !textPart || isDroppedPlaceholder(textPart.text)) {
169269
169604
  messages.unshift({
169270
169605
  info: { role: "user", sessionID: sessionId },
169271
- parts: [{ type: "text", text: historyBlock }]
169606
+ parts: [{ type: "text", text: historyBlock, synthetic: true }]
169272
169607
  });
169273
169608
  } else {
169274
169609
  textPart.text = `${historyBlock}
@@ -170158,10 +170493,16 @@ function softRefreshCachedM1(options) {
170158
170493
  function prependM0M1Messages(sessionId, messages, m0Text, m1Text) {
170159
170494
  messages.unshift({
170160
170495
  info: { role: "user", sessionID: sessionId },
170161
- parts: [{ type: "text", text: m0Text.length > 0 ? m0Text : M0_EMPTY_BODY }]
170496
+ parts: [
170497
+ {
170498
+ type: "text",
170499
+ text: m0Text.length > 0 ? m0Text : M0_EMPTY_BODY,
170500
+ synthetic: true
170501
+ }
170502
+ ]
170162
170503
  }, {
170163
170504
  info: { role: "user", sessionID: sessionId },
170164
- parts: [{ type: "text", text: m1Text }]
170505
+ parts: [{ type: "text", text: m1Text, synthetic: true }]
170165
170506
  });
170166
170507
  }
170167
170508
  function renderFreshM0NonPersisted(options) {
@@ -170869,7 +171210,7 @@ function buildToolArcs(messages) {
170869
171210
  }
170870
171211
  return arcs.sort((a, b) => a.invOrdinal - b.invOrdinal || (a.resOrdinal ?? Number.MAX_SAFE_INTEGER) - (b.resOrdinal ?? Number.MAX_SAFE_INTEGER));
170871
171212
  }
170872
- function fenceBoundaryForToolArcs(candidate, arcs, lastCompartmentEndOrdinal) {
171213
+ function fenceBoundaryForToolArcs(candidate, arcs, lastCompartmentEndOrdinal, recentOpenArcCutoff) {
170873
171214
  let boundary = candidate;
170874
171215
  for (const arc of arcs) {
170875
171216
  if (arc.resOrdinal !== null) {
@@ -170878,6 +171219,8 @@ function fenceBoundaryForToolArcs(candidate, arcs, lastCompartmentEndOrdinal) {
170878
171219
  }
170879
171220
  continue;
170880
171221
  }
171222
+ if (arc.invOrdinal < recentOpenArcCutoff)
171223
+ continue;
170881
171224
  if (arc.invOrdinal >= lastCompartmentEndOrdinal + 1 && arc.invOrdinal < boundary) {
170882
171225
  return arc.invOrdinal;
170883
171226
  }
@@ -171117,7 +171460,7 @@ function semanticSnapBoundary(args) {
171117
171460
  return snapped;
171118
171461
  }
171119
171462
  function applyHeadCap(args) {
171120
- const { index, protectedTailStart, offset, arcs, capTokens } = args;
171463
+ const { index, protectedTailStart, offset, arcs, capTokens, recentOpenArcCutoff } = args;
171121
171464
  if (offset >= protectedTailStart)
171122
171465
  return { eligibleEndOrdinal: offset, oversizeAtomicUnit: false };
171123
171466
  let end = index.findHeadEndForCap(offset, protectedTailStart, capTokens);
@@ -171125,7 +171468,7 @@ function applyHeadCap(args) {
171125
171468
  for (const arc of arcs) {
171126
171469
  const resOrdinal = arc.resOrdinal;
171127
171470
  if (resOrdinal === null) {
171128
- if (arc.invOrdinal >= offset && arc.invOrdinal < end) {
171471
+ if (arc.invOrdinal >= recentOpenArcCutoff && arc.invOrdinal >= offset && arc.invOrdinal < end) {
171129
171472
  end = Math.min(end, arc.invOrdinal);
171130
171473
  }
171131
171474
  continue;
@@ -171192,7 +171535,14 @@ function resolveProtectedTailBoundary(ctx) {
171192
171535
  }
171193
171536
  if (ctx.mode === "manual-full-recomp") {
171194
171537
  const arcs2 = buildToolArcs(messages);
171195
- const firstOpenArc = arcs2.find((arc) => arc.resOrdinal === null && arc.invOrdinal >= offset);
171538
+ const recompTarget = deriveProtectedTailTokenTarget({
171539
+ contextLimit: ctx.contextLimit,
171540
+ executeThresholdPercentage: ctx.executeThresholdPercentage,
171541
+ usagePercentage: 0,
171542
+ triggerBudget: ctx.triggerBudget
171543
+ });
171544
+ const recentOpenArcCutoff2 = index.findSuffixStartForTokens(recompTarget.N);
171545
+ const firstOpenArc = arcs2.find((arc) => arc.resOrdinal === null && arc.invOrdinal >= offset && arc.invOrdinal >= recentOpenArcCutoff2);
171196
171546
  const protectedTailStart2 = firstOpenArc?.invOrdinal ?? rawMessageCount + 1;
171197
171547
  const rawRangeFingerprint2 = computeRawRangeFingerprint(messages, offset, protectedTailStart2);
171198
171548
  return {
@@ -171234,13 +171584,14 @@ function resolveProtectedTailBoundary(ctx) {
171234
171584
  const scaledN = ctx.emergencyTailScale ? Math.max(1, Math.floor(target.N * ctx.emergencyTailScale)) : target.N;
171235
171585
  const arcs = buildToolArcs(messages);
171236
171586
  let boundary = index.findSuffixStartForTokens(scaledN);
171587
+ const recentOpenArcCutoff = boundary;
171237
171588
  let boundaryReason = boundary === 1 ? "whole-session-smaller-than-tail" : "size-walk";
171238
171589
  const tokenAtBoundary = index.tokenForOrdinal(boundary);
171239
171590
  if (boundary <= rawMessageCount && tokenAtBoundary > Math.max(2 * scaledN, 64000) && boundary < rawMessageCount) {
171240
171591
  boundary += 1;
171241
171592
  boundaryReason = "huge-message-exception";
171242
171593
  }
171243
- boundary = fenceBoundaryForToolArcs(boundary, arcs, ctx.lastCompartmentEndOrdinal);
171594
+ boundary = fenceBoundaryForToolArcs(boundary, arcs, ctx.lastCompartmentEndOrdinal, recentOpenArcCutoff);
171244
171595
  const snapped = semanticSnapBoundary({
171245
171596
  messages,
171246
171597
  index,
@@ -171250,7 +171601,7 @@ function resolveProtectedTailBoundary(ctx) {
171250
171601
  });
171251
171602
  if (snapped !== boundary)
171252
171603
  boundaryReason = "semantic-snap";
171253
- boundary = fenceBoundaryForToolArcs(snapped, arcs, ctx.lastCompartmentEndOrdinal);
171604
+ boundary = fenceBoundaryForToolArcs(snapped, arcs, ctx.lastCompartmentEndOrdinal, recentOpenArcCutoff);
171254
171605
  let runtimeFloor = offset;
171255
171606
  if (ctx.migrationFloorActive)
171256
171607
  runtimeFloor = Math.max(runtimeFloor, ctx.priorBoundaryOrdinal);
@@ -171286,7 +171637,8 @@ function resolveProtectedTailBoundary(ctx) {
171286
171637
  offset,
171287
171638
  arcs,
171288
171639
  lastCompartmentEndOrdinal: ctx.lastCompartmentEndOrdinal,
171289
- capTokens: perRunCap
171640
+ capTokens: perRunCap,
171641
+ recentOpenArcCutoff
171290
171642
  });
171291
171643
  const rawRangeFingerprint = computeRawRangeFingerprint(messages, offset, head.eligibleEndOrdinal);
171292
171644
  return {
@@ -177079,6 +177431,11 @@ async function runManagedRecomp(ctx, sessionId, options) {
177079
177431
  try {
177080
177432
  const message = await executeContextRecomp(buildRecompDeps(ctx, sessionId), options);
177081
177433
  const terminalPhase = isRecompSkip(message) ? "skipped" : isRecompFailure(message) ? "failed" : "done";
177434
+ if (terminalPhase === "done") {
177435
+ try {
177436
+ clearEmergencyRecovery(ctx.db, sessionId);
177437
+ } catch {}
177438
+ }
177082
177439
  setRecompTerminal(ctx.liveSessionState, sessionId, terminalPhase, extractRecompReason(message));
177083
177440
  return message;
177084
177441
  } catch (error51) {
@@ -177177,6 +177534,7 @@ var RECOMP_DONE_GRACE_MS = 30000;
177177
177534
  var init_recomp_orchestrator = __esm(async () => {
177178
177535
  init_compartment_storage();
177179
177536
  init_project_identity2();
177537
+ init_storage_meta_persisted();
177180
177538
  await __promiseAll([
177181
177539
  init_memory_migration(),
177182
177540
  init_compartment_runner()
@@ -177237,15 +177595,15 @@ function shouldShowAnnouncement() {
177237
177595
  }
177238
177596
  return state.version !== ANNOUNCEMENT_VERSION;
177239
177597
  }
177240
- var ANNOUNCEMENT_VERSION = "0.24.0", ANNOUNCEMENT_FEATURES, ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU", STATE_FILENAME = "last_announced_version";
177598
+ var ANNOUNCEMENT_VERSION = "0.25.0", ANNOUNCEMENT_FEATURES, ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU", STATE_FILENAME = "last_announced_version";
177241
177599
  var init_announcement = __esm(() => {
177242
177600
  init_data_path();
177243
177601
  ANNOUNCEMENT_FEATURES = [
177244
- "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).",
177245
- "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.",
177246
- "Pi: fixed sessions overflowing the model context while still showing moderate usage Pi now sheds context before a tool-heavy turn overflows.",
177247
- "Fewer prompt-cache busts: doc edits, processed screenshots, and a rebuild-then-bust-again case no longer re-bill large prompt prefixes.",
177248
- "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)."
177602
+ "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.",
177603
+ "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.",
177604
+ "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.",
177605
+ "Pi: fixed /ctx-dream (was failing with 'Unknown named parameter') and local-embedding load failures on Windows/Desktop (#151, #128).",
177606
+ "Runaway background agents on weak/local models are now capped and force-stopped (#154, #152). Plus several prompt-cache busts removed."
177249
177607
  ];
177250
177608
  });
177251
177609
 
@@ -178040,9 +178398,9 @@ function getMagicContextBuiltinCommands() {
178040
178398
  template: "ctx-dream",
178041
178399
  description: "Run the hidden dreamer maintenance pass for this project now"
178042
178400
  },
178043
- "ctx-embed-history": {
178044
- template: "ctx-embed-history",
178045
- description: "Embed all of this session's history compartments for semantic search, in one pass"
178401
+ "ctx-embed": {
178402
+ template: "ctx-embed",
178403
+ description: "Embedding status, or start/pause history compartment embedding (start | pause)"
178046
178404
  }
178047
178405
  };
178048
178406
  }
@@ -179510,7 +179868,7 @@ function enqueueDream(db, projectIdentity, reason, force = false) {
179510
179868
  return db.transaction(() => {
179511
179869
  if (!hasActiveDreamLease(db)) {
179512
179870
  const staleThresholdMs = force ? 2 * 60 * 1000 : 120 * 60 * 1000;
179513
- db.prepare("DELETE FROM dream_queue WHERE project_path = ? AND started_at IS NOT NULL AND started_at < ?").run([projectIdentity, now - staleThresholdMs]);
179871
+ db.prepare("DELETE FROM dream_queue WHERE project_path = ? AND started_at IS NOT NULL AND started_at < ?").run(projectIdentity, now - staleThresholdMs);
179514
179872
  }
179515
179873
  const existing = db.prepare("SELECT id FROM dream_queue WHERE project_path = ?").get(projectIdentity);
179516
179874
  if (existing) {
@@ -181205,120 +181563,7 @@ ${body}` : subject;
181205
181563
  init_logger();
181206
181564
  init_embedding();
181207
181565
  init_storage_git_commit_embeddings();
181208
-
181209
- // src/features/magic-context/git-commits/storage-git-commits.ts
181210
- init_logger();
181211
- var insertStatements = new WeakMap;
181212
- var existingShasStatements = new WeakMap;
181213
- var projectCountStatements = new WeakMap;
181214
- var evictStatements = new WeakMap;
181215
- var evictOverflowStatements = new WeakMap;
181216
- var latestCommitTimeStatements = new WeakMap;
181217
- function getInsertStatement(db) {
181218
- let stmt = insertStatements.get(db);
181219
- if (!stmt) {
181220
- stmt = db.prepare(`INSERT INTO git_commits (sha, project_path, short_sha, message, author, committed_at, indexed_at)
181221
- VALUES (?, ?, ?, ?, ?, ?, ?)
181222
- ON CONFLICT(sha) DO UPDATE SET
181223
- project_path = excluded.project_path,
181224
- short_sha = excluded.short_sha,
181225
- message = excluded.message,
181226
- author = excluded.author,
181227
- committed_at = excluded.committed_at,
181228
- indexed_at = excluded.indexed_at
181229
- WHERE git_commits.message != excluded.message`);
181230
- insertStatements.set(db, stmt);
181231
- }
181232
- return stmt;
181233
- }
181234
- function getExistingShasStatement(db) {
181235
- let stmt = existingShasStatements.get(db);
181236
- if (!stmt) {
181237
- stmt = db.prepare("SELECT sha FROM git_commits WHERE project_path = ?");
181238
- existingShasStatements.set(db, stmt);
181239
- }
181240
- return stmt;
181241
- }
181242
- function getProjectCountStatement(db) {
181243
- let stmt = projectCountStatements.get(db);
181244
- if (!stmt) {
181245
- stmt = db.prepare("SELECT COUNT(*) AS count FROM git_commits WHERE project_path = ?");
181246
- projectCountStatements.set(db, stmt);
181247
- }
181248
- return stmt;
181249
- }
181250
- function getLatestCommitTimeStatement(db) {
181251
- let stmt = latestCommitTimeStatements.get(db);
181252
- if (!stmt) {
181253
- stmt = db.prepare("SELECT MAX(committed_at) AS latest FROM git_commits WHERE project_path = ?");
181254
- latestCommitTimeStatements.set(db, stmt);
181255
- }
181256
- return stmt;
181257
- }
181258
- function getEvictOverflowStatement(db) {
181259
- let stmt = evictOverflowStatements.get(db);
181260
- if (!stmt) {
181261
- stmt = db.prepare(`DELETE FROM git_commits
181262
- WHERE rowid IN (
181263
- SELECT rowid FROM git_commits
181264
- WHERE project_path = ?
181265
- ORDER BY committed_at DESC, sha DESC
181266
- LIMIT -1 OFFSET ?
181267
- )`);
181268
- evictOverflowStatements.set(db, stmt);
181269
- }
181270
- return stmt;
181271
- }
181272
- function upsertCommits(db, projectPath, commits) {
181273
- if (commits.length === 0)
181274
- return { inserted: 0, updated: 0 };
181275
- const existing = new Set;
181276
- for (const row of getExistingShasStatement(db).all(projectPath)) {
181277
- existing.add(row.sha);
181278
- }
181279
- let inserted = 0;
181280
- let updated = 0;
181281
- const now = Date.now();
181282
- const insertStmt = getInsertStatement(db);
181283
- db.transaction(() => {
181284
- for (const commit of commits) {
181285
- const result = insertStmt.run(commit.sha, projectPath, commit.shortSha, commit.message, commit.author, commit.committedAtMs, now);
181286
- if (result.changes > 0) {
181287
- if (existing.has(commit.sha)) {
181288
- updated++;
181289
- } else {
181290
- inserted++;
181291
- existing.add(commit.sha);
181292
- }
181293
- }
181294
- }
181295
- })();
181296
- return { inserted, updated };
181297
- }
181298
- function getCommitCount(db, projectPath) {
181299
- const row = getProjectCountStatement(db).get(projectPath);
181300
- return row?.count ?? 0;
181301
- }
181302
- function getLatestIndexedCommitTimeMs(db, projectPath) {
181303
- const row = getLatestCommitTimeStatement(db).get(projectPath);
181304
- return row?.latest ?? null;
181305
- }
181306
- function enforceProjectCap(db, projectPath, maxCommits) {
181307
- if (maxCommits <= 0)
181308
- return 0;
181309
- const count = getCommitCount(db, projectPath);
181310
- if (count <= maxCommits)
181311
- return 0;
181312
- getEvictOverflowStatement(db).run(projectPath, maxCommits);
181313
- const after = getCommitCount(db, projectPath);
181314
- const evicted = Math.max(0, count - after);
181315
- if (evicted > 0) {
181316
- log(`[git-commits] evicted ${evicted} oldest commits for project ${projectPath} (cap=${maxCommits}, was=${count})`);
181317
- }
181318
- return evicted;
181319
- }
181320
-
181321
- // src/features/magic-context/git-commits/indexer.ts
181566
+ init_storage_git_commits();
181322
181567
  var MS_PER_DAY = 24 * 60 * 60 * 1000;
181323
181568
  var EMBED_BATCH_SIZE = 16;
181324
181569
  var EMBED_MAX_PER_SWEEP = 500;
@@ -181556,6 +181801,7 @@ function searchGitCommitsSync(db, projectPath, query, options) {
181556
181801
 
181557
181802
  // src/features/magic-context/git-commits/index.ts
181558
181803
  init_storage_git_commit_embeddings();
181804
+ init_storage_git_commits();
181559
181805
  init_sweep_coordinator();
181560
181806
 
181561
181807
  // src/plugin/dream-timer.ts
@@ -182980,7 +183226,7 @@ function createMagicContextCommandHandler(deps) {
182980
183226
  const isAugCommand = (command) => command === "ctx-aug";
182981
183227
  const isDreamCommand = (command) => command === "ctx-dream";
182982
183228
  const isSessionUpgradeCommand = (command) => command === "ctx-session-upgrade";
182983
- const isEmbedHistoryCommand = (command) => command === "ctx-embed-history";
183229
+ const isEmbedCommand = (command) => command === "ctx-embed";
182984
183230
  return {
182985
183231
  "command.execute.before": async (input, _output, _params) => {
182986
183232
  const isStatus = isStatusCommand(input.command);
@@ -182989,8 +183235,8 @@ function createMagicContextCommandHandler(deps) {
182989
183235
  const isAug = isAugCommand(input.command);
182990
183236
  const isDream = isDreamCommand(input.command);
182991
183237
  const isSessionUpgrade = isSessionUpgradeCommand(input.command);
182992
- const isEmbedHistory = isEmbedHistoryCommand(input.command);
182993
- if (!isStatus && !isFlush && !isRecomp && !isAug && !isDream && !isSessionUpgrade && !isEmbedHistory) {
183238
+ const isEmbed = isEmbedCommand(input.command);
183239
+ if (!isStatus && !isFlush && !isRecomp && !isAug && !isDream && !isSessionUpgrade && !isEmbed) {
182994
183240
  return;
182995
183241
  }
182996
183242
  const sessionId = input.sessionID;
@@ -183003,15 +183249,50 @@ function createMagicContextCommandHandler(deps) {
183003
183249
  await executeDreaming(deps, sessionId);
183004
183250
  return;
183005
183251
  }
183006
- if (isEmbedHistory) {
183007
- const summary = deps.executeEmbedHistory ? await deps.executeEmbedHistory(sessionId) : "Semantic embedding is not configured for this project, so there is nothing to embed.";
183008
- await deps.sendNotification(sessionId, summary, {});
183009
- throwSentinel(input.command);
183252
+ if (isEmbed) {
183253
+ const sub = input.arguments.trim().toLowerCase();
183254
+ if (sub === "pause") {
183255
+ const summary = deps.pauseEmbedDrain ? deps.pauseEmbedDrain(sessionId) : "Embedding pause is unavailable.";
183256
+ if (isTuiConnected(sessionId)) {
183257
+ pushNotification("action", { action: "show-result-dialog", title: "Embed", message: summary }, sessionId);
183258
+ } else {
183259
+ await deps.sendNotification(sessionId, summary, {});
183260
+ }
183261
+ throwSentinel(input.command);
183262
+ }
183263
+ if (sub === "start") {
183264
+ const summary = deps.executeEmbedHistory ? await deps.executeEmbedHistory(sessionId) : "Semantic embedding is not configured for this project, so there is nothing to embed.";
183265
+ if (isTuiConnected(sessionId)) {
183266
+ pushNotification("action", { action: "show-result-dialog", title: "Embed", message: summary }, sessionId);
183267
+ } else {
183268
+ await deps.sendNotification(sessionId, summary, {});
183269
+ }
183270
+ throwSentinel(input.command);
183271
+ }
183272
+ if (sub !== "") {
183273
+ await deps.sendNotification(sessionId, "Usage: `/ctx-embed` (status), `/ctx-embed start`, or `/ctx-embed pause`.", {});
183274
+ throwSentinel(input.command);
183275
+ }
183276
+ if (isTuiConnected(sessionId)) {
183277
+ pushNotification("action", { action: "show-embed-dialog" }, sessionId);
183278
+ sessionLog(sessionId, "command ctx-embed: pushed show-embed-dialog to TUI");
183279
+ throwSentinel(input.command);
183280
+ }
183281
+ result = deps.getEmbedStatusText ? `## Embedding Status
183282
+
183283
+ ${deps.getEmbedStatusText(sessionId)}` : `## Embedding Status
183284
+
183285
+ Embedding status is unavailable.`;
183010
183286
  }
183011
183287
  if (isFlush) {
183012
183288
  result = executeFlush(deps.db, sessionId);
183013
183289
  clearCachedM0M1(deps.db, sessionId);
183014
183290
  deps.onFlush?.(sessionId);
183291
+ if (isTuiConnected(sessionId)) {
183292
+ pushNotification("action", { action: "show-flush-dialog", message: result }, sessionId);
183293
+ sessionLog(sessionId, "command ctx-flush: pushed show-flush-dialog to TUI");
183294
+ throwSentinel(input.command);
183295
+ }
183015
183296
  }
183016
183297
  if (isStatus) {
183017
183298
  if (isTuiConnected(sessionId)) {
@@ -183138,6 +183419,34 @@ ${snap.error}`;
183138
183419
  // src/hooks/magic-context/hook.ts
183139
183420
  init_derive_budgets();
183140
183421
 
183422
+ // src/hooks/magic-context/embed-session-state.ts
183423
+ var embedPauseBySession = new Set;
183424
+ var embedRunStateBySession = new Map;
183425
+ var autoEmbedAttemptedBySession = new Set;
183426
+ function getEmbedDrainUiStatus(sessionId, progress) {
183427
+ if (embedPauseBySession.has(sessionId)) {
183428
+ return { status: "paused" };
183429
+ }
183430
+ if (progress?.kind === "embed" && progress.phase === "recomp") {
183431
+ return { status: "running" };
183432
+ }
183433
+ if (progress?.kind === "embed" && (progress.phase === "failed" || progress.phase === "skipped") && progress.message) {
183434
+ if (/provider/i.test(progress.message)) {
183435
+ return { status: "stopped", detail: progress.message };
183436
+ }
183437
+ }
183438
+ return { status: "idle" };
183439
+ }
183440
+ function clearEmbedSessionState(sessionId) {
183441
+ embedPauseBySession.delete(sessionId);
183442
+ const ctrl = embedRunStateBySession.get(sessionId);
183443
+ if (ctrl) {
183444
+ ctrl.abort();
183445
+ embedRunStateBySession.delete(sessionId);
183446
+ }
183447
+ autoEmbedAttemptedBySession.delete(sessionId);
183448
+ }
183449
+
183141
183450
  // src/features/magic-context/message-index-async.ts
183142
183451
  init_logger();
183143
183452
  await init_message_index();
@@ -183375,8 +183684,8 @@ var CHANNEL1_SENTINEL = "<system-reminder>";
183375
183684
  var TOKENS_PER_BYTE = 0.25;
183376
183685
  var CHANNEL1_FLOOR_TOKENS = 1e4;
183377
183686
  var CHANNEL1_REFIRE_FLOOR_TOKENS = 1e4;
183378
- function channel1RefireTokens(historyBudgetTokens) {
183379
- const scaled = Math.round(0.05 * Math.max(0, historyBudgetTokens));
183687
+ function channel1RefireTokens(workingWindowTokens) {
183688
+ const scaled = Math.round(0.05 * Math.max(0, workingWindowTokens));
183380
183689
  return Math.max(CHANNEL1_REFIRE_FLOOR_TOKENS, scaled);
183381
183690
  }
183382
183691
  var S_GENTLE = 0.2;
@@ -183446,7 +183755,7 @@ function computeTailTokenEstimate(messages) {
183446
183755
  };
183447
183756
  }
183448
183757
  function decideChannel1(input) {
183449
- const { undroppedTokens, pressure, historyBudgetTokens, hasRecentReduce } = input;
183758
+ const { undroppedTokens, pressure, workingWindowTokens, hasRecentReduce } = input;
183450
183759
  const resetCycle = hasRecentReduce || undroppedTokens < input.lastNudgeUndropped;
183451
183760
  const lastNudge = resetCycle ? 0 : input.lastNudgeUndropped;
183452
183761
  const lastLevel = resetCycle ? "" : input.lastNudgeLevel;
@@ -183461,7 +183770,7 @@ function decideChannel1(input) {
183461
183770
  return quiet();
183462
183771
  if (undroppedTokens < CHANNEL1_FLOOR_TOKENS)
183463
183772
  return quiet();
183464
- const budget = historyBudgetTokens > 0 ? historyBudgetTokens : undroppedTokens || 1;
183773
+ const budget = workingWindowTokens > 0 ? workingWindowTokens : undroppedTokens || 1;
183465
183774
  const severity = undroppedTokens / budget * pressure;
183466
183775
  if (severity < S_GENTLE)
183467
183776
  return quiet();
@@ -183473,7 +183782,7 @@ function decideChannel1(input) {
183473
183782
  else
183474
183783
  level = "gentle";
183475
183784
  if (lastLevel === "") {
183476
- if (undroppedTokens < lastNudge + channel1RefireTokens(historyBudgetTokens)) {
183785
+ if (undroppedTokens < lastNudge + channel1RefireTokens(workingWindowTokens)) {
183477
183786
  return quiet();
183478
183787
  }
183479
183788
  } else if (LEVEL_RANK[level] <= LEVEL_RANK[lastLevel]) {
@@ -183498,6 +183807,13 @@ function computePressure(input) {
183498
183807
  function approxThousands(tokens) {
183499
183808
  return `${Math.round(tokens / 1000)}k`;
183500
183809
  }
183810
+ function formatOldestReclaimableHint(hint) {
183811
+ if (!hint || hint.length === 0)
183812
+ return "";
183813
+ const rendered = hint.slice(0, 4).map((tag) => `§${tag.tagNumber}§ ${tag.toolName ?? "tool"}`).join(" · ");
183814
+ return rendered.length > 0 ? `
183815
+ oldest reclaimable: ${rendered}.` : "";
183816
+ }
183501
183817
  var CHANNEL2_USABLE_FRACTION = 1 / 3;
183502
183818
  var CHANNEL2_MIN_RECLAIMABLE = 1e4;
183503
183819
  function shouldTriggerChannel2(input) {
@@ -183507,30 +183823,32 @@ function shouldTriggerChannel2(input) {
183507
183823
  return true;
183508
183824
  return input.reclaimableTokens >= input.usableTokens * CHANNEL2_USABLE_FRACTION;
183509
183825
  }
183510
- function buildChannel2Reminder(undroppedTokens) {
183826
+ function buildChannel2Reminder(undroppedTokens, hint) {
183511
183827
  const amount = approxThousands(undroppedTokens);
183828
+ const hintText = formatOldestReclaimableHint(hint);
183512
183829
  return `<system-reminder>
183513
- ` + `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.
183830
+ ` + `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}
183514
183831
  ` + `</system-reminder>`;
183515
183832
  }
183516
- function buildChannel1Reminder(level, undroppedTokens) {
183833
+ function buildChannel1Reminder(level, undroppedTokens, hint) {
183517
183834
  const amount = approxThousands(undroppedTokens);
183835
+ const hintText = formatOldestReclaimableHint(hint);
183518
183836
  let body;
183519
183837
  switch (level) {
183520
183838
  case "gentle":
183521
- 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.`;
183839
+ 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.`;
183522
183840
  break;
183523
183841
  case "firm":
183524
- body = `~${amount} tokens of unreduced tool output is accumulating. ` + `Drop what you have already processed with ctx_reduce before continuing.`;
183842
+ 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.`;
183525
183843
  break;
183526
183844
  case "urgent":
183527
- 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.`;
183845
+ 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.`;
183528
183846
  break;
183529
183847
  }
183530
183848
  return `
183531
183849
 
183532
183850
  <system-reminder>
183533
- ${body}
183851
+ ${body}${hintText}
183534
183852
  </system-reminder>`;
183535
183853
  }
183536
183854
 
@@ -183584,10 +183902,10 @@ async function maybeDeliverChannel2(sessionId, deps) {
183584
183902
  try {
183585
183903
  const client3 = getLiveServerClient(serverUrl, deps.directory);
183586
183904
  const promptContext = await resolvePromptContext(client3, sessionId);
183587
- const reminder = buildChannel2Reminder(deps.reclaimableTokens);
183905
+ const reminder = buildChannel2Reminder(deps.reclaimableTokens, deps.oldestReclaimableToolTags);
183588
183906
  const body = {
183589
183907
  noReply: false,
183590
- parts: [{ type: "text", text: reminder }]
183908
+ parts: [{ type: "text", text: reminder, synthetic: true }]
183591
183909
  };
183592
183910
  if (promptContext?.agent)
183593
183911
  body.agent = promptContext.agent;
@@ -184045,7 +184363,8 @@ function applyCavemanCleanup(sessionId, db, targets, tags, config2) {
184045
184363
  const result = {
184046
184364
  compressedToLite: 0,
184047
184365
  compressedToFull: 0,
184048
- compressedToUltra: 0
184366
+ compressedToUltra: 0,
184367
+ mutatedTextTags: 0
184049
184368
  };
184050
184369
  if (!config2.enabled)
184051
184370
  return result;
@@ -184086,7 +184405,9 @@ function applyCavemanCleanup(sessionId, db, targets, tags, config2) {
184086
184405
  const target = targets.get(tag.tagNumber);
184087
184406
  if (!target)
184088
184407
  continue;
184089
- target.setContent(compressed);
184408
+ const didMutate = target.setContent(compressed);
184409
+ if (didMutate)
184410
+ result.mutatedTextTags += 1;
184090
184411
  updateCavemanDepth(db, sessionId, tag.tagNumber, targetDepth);
184091
184412
  if (targetDepth === DEPTH_LITE)
184092
184413
  result.compressedToLite += 1;
@@ -184674,28 +184995,6 @@ function stripInlineThinking(messages, messageTagNumbers, clearReasoningAge) {
184674
184995
  }
184675
184996
  return stripped;
184676
184997
  }
184677
- function truncateErroredTools(messages, watermark, messageTagNumbers) {
184678
- let truncated = 0;
184679
- for (let i = 0;i < messages.length; i++) {
184680
- const maxTag = messageTagNumbers.get(messages[i]) ?? 0;
184681
- if (maxTag > watermark) {
184682
- continue;
184683
- }
184684
- for (const part of messages[i].parts) {
184685
- if (!isRecord(part) || part.type !== "tool" || !isRecord(part.state)) {
184686
- continue;
184687
- }
184688
- if (part.state.status !== "error") {
184689
- continue;
184690
- }
184691
- if (typeof part.state.error === "string" && part.state.error.length > 100) {
184692
- part.state.error = `${part.state.error.slice(0, 100)}... [truncated]`;
184693
- truncated++;
184694
- }
184695
- }
184696
- }
184697
- return truncated;
184698
- }
184699
184998
  var REASONING_IGNORED_PART_TYPES = new Set([
184700
184999
  "step-start",
184701
185000
  "step-finish",
@@ -185112,28 +185411,12 @@ function appendReminderToUserMessage(message, reminder) {
185112
185411
  }
185113
185412
 
185114
185413
  // src/hooks/magic-context/apply-operations.ts
185115
- init_tag_part_guards();
185116
185414
  await init_storage();
185117
- var USER_DROP_PREVIEW_CHARS = 250;
185118
185415
  var RECENT_TOOL_SKELETON_WINDOW = 20;
185119
- function buildReplacementContent(tagId, target) {
185120
- const role = target.message?.info.role;
185121
- if (role !== "user") {
185122
- return `[dropped §${tagId}§]`;
185123
- }
185124
- const currentContent = target.getContent?.() ?? "";
185125
- const originalText = stripTagPrefix(currentContent);
185126
- if (originalText.length <= USER_DROP_PREVIEW_CHARS) {
185127
- return `[truncated §${tagId}§]
185128
- ${originalText}`;
185129
- }
185130
- const hardCut = originalText.slice(0, USER_DROP_PREVIEW_CHARS);
185131
- const softCutIndex = hardCut.search(/\s\S*$/);
185132
- const preview = softCutIndex > USER_DROP_PREVIEW_CHARS - 30 ? hardCut.slice(0, softCutIndex) : hardCut;
185133
- return `[truncated §${tagId}§]
185134
- ${preview}…`;
185135
- }
185136
- function applyPendingOperations(sessionId, db, targets, protectedTags = 0, preloadedTags, preloadedPendingOps) {
185416
+ function buildReplacementContent(tagId) {
185417
+ return `[dropped §${tagId}§]`;
185418
+ }
185419
+ function applyPendingOperations(sessionId, db, targets, protectedTags = 0, preloadedTags, preloadedPendingOps, syntheticPendingOps = []) {
185137
185420
  let didMutateMessage = false;
185138
185421
  db.transaction(() => {
185139
185422
  const tags = preloadedTags ?? getTagsBySession(db, sessionId);
@@ -185141,11 +185424,16 @@ function applyPendingOperations(sessionId, db, targets, protectedTags = 0, prelo
185141
185424
  const tagTypeById = new Map(tags.map((tag) => [tag.tagNumber, tag.type]));
185142
185425
  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;
185143
185426
  const pendingOps = preloadedPendingOps ?? getPendingOps(db, sessionId);
185427
+ const opsToApply = [
185428
+ ...pendingOps.map((op) => ({ op, synthetic: false })),
185429
+ ...syntheticPendingOps.map((op) => ({ op, synthetic: true }))
185430
+ ];
185144
185431
  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));
185145
- for (const pendingOp of pendingOps) {
185432
+ for (const { op: pendingOp, synthetic } of opsToApply) {
185146
185433
  const tagStatus = tagStatusById.get(pendingOp.tagId);
185147
185434
  if (tagStatus === "compacted" || tagStatus === "dropped") {
185148
- removePendingOp(db, sessionId, pendingOp.tagId);
185435
+ if (!synthetic)
185436
+ removePendingOp(db, sessionId, pendingOp.tagId);
185149
185437
  continue;
185150
185438
  }
185151
185439
  if (protectedTagIds.has(pendingOp.tagId)) {
@@ -185153,33 +185441,46 @@ function applyPendingOperations(sessionId, db, targets, protectedTags = 0, prelo
185153
185441
  }
185154
185442
  const target = targets.get(pendingOp.tagId);
185155
185443
  const isToolTag = tagTypeById.get(pendingOp.tagId) === "tool";
185444
+ if (synthetic) {
185445
+ if (!isToolTag || target?.canDrop?.() !== true)
185446
+ continue;
185447
+ }
185448
+ let shouldPersistDrop = false;
185156
185449
  if (isToolTag) {
185157
185450
  if (skeletonWindow.has(pendingOp.tagId)) {
185158
185451
  const truncResult = target?.truncate?.() ?? "absent";
185159
- if (truncResult === "incomplete") {
185452
+ if (truncResult === "incomplete" || synthetic && truncResult !== "truncated") {
185160
185453
  continue;
185161
185454
  }
185162
185455
  if (truncResult === "truncated") {
185163
185456
  didMutateMessage = true;
185164
185457
  }
185165
185458
  updateTagDropMode(db, sessionId, pendingOp.tagId, "truncated");
185459
+ shouldPersistDrop = true;
185166
185460
  } else {
185167
185461
  const dropResult = target?.drop?.() ?? "absent";
185168
- if (dropResult === "incomplete") {
185462
+ if (dropResult === "incomplete" || synthetic && dropResult !== "removed") {
185169
185463
  continue;
185170
185464
  }
185171
185465
  if (dropResult === "removed") {
185172
185466
  didMutateMessage = true;
185173
185467
  }
185174
185468
  updateTagDropMode(db, sessionId, pendingOp.tagId, "full");
185469
+ shouldPersistDrop = true;
185175
185470
  }
185176
185471
  } else if (target) {
185177
- const changed = target.setContent(buildReplacementContent(pendingOp.tagId, target));
185472
+ const changed = target.setContent(buildReplacementContent(pendingOp.tagId));
185178
185473
  if (changed)
185179
185474
  didMutateMessage = true;
185475
+ shouldPersistDrop = true;
185476
+ } else if (!synthetic) {
185477
+ shouldPersistDrop = true;
185180
185478
  }
185479
+ if (!shouldPersistDrop)
185480
+ continue;
185181
185481
  updateTagStatus(db, sessionId, pendingOp.tagId, "dropped");
185182
- removePendingOp(db, sessionId, pendingOp.tagId);
185482
+ if (!synthetic)
185483
+ removePendingOp(db, sessionId, pendingOp.tagId);
185183
185484
  }
185184
185485
  })();
185185
185486
  return didMutateMessage;
@@ -185203,7 +185504,7 @@ function applyFlushedStatuses(sessionId, db, targets, preloadedTags) {
185203
185504
  }
185204
185505
  }
185205
185506
  } else if (target) {
185206
- const changed = target.setContent(buildReplacementContent(tag.tagNumber, target));
185507
+ const changed = target.setContent(buildReplacementContent(tag.tagNumber));
185207
185508
  if (changed)
185208
185509
  didMutateMessage = true;
185209
185510
  }
@@ -185720,7 +186021,7 @@ function tagMessages(sessionId, messages, tagger, db, options = {}) {
185720
186021
  logTransformTiming(sessionId, "tag.saveSource", performance.now() - accSaveSource);
185721
186022
  for (const [compositeKey, tagId] of toolTagByCallId) {
185722
186023
  const thinkingParts = toolThinkingByCallId.get(compositeKey) ?? [];
185723
- targets.set(tagId, createToolDropTarget(compositeKey, thinkingParts, toolCallIndex, batch));
186024
+ targets.set(tagId, createToolDropTarget(compositeKey, thinkingParts, toolCallIndex, batch, tagId));
185724
186025
  }
185725
186026
  const hasRecentReduceCall = lastReduceMessageIndex >= 0 && messages.length - lastReduceMessageIndex <= RECENT_REDUCE_LOOKBACK;
185726
186027
  return {
@@ -187000,7 +187301,9 @@ function applyHeuristicCleanup(sessionId, db, targets, messageTagNumbers, config
187000
187301
  continue;
187001
187302
  updateTagDropMode(db, sessionId, tag.tagNumber, "full");
187002
187303
  updateTagStatus(db, sessionId, tag.tagNumber, "dropped");
187003
- deduplicatedTools++;
187304
+ if (result === "removed" || result === "truncated") {
187305
+ deduplicatedTools++;
187306
+ }
187004
187307
  }
187005
187308
  }
187006
187309
  })();
@@ -187009,6 +187312,7 @@ function applyHeuristicCleanup(sessionId, db, targets, messageTagNumbers, config
187009
187312
  sessionLog(sessionId, `heuristic cleanup: dropped ${droppedTools} tool tags, deduplicated ${deduplicatedTools} tool calls, dropped ${droppedInjections} system injections`);
187010
187313
  }
187011
187314
  let compressedTextTags = 0;
187315
+ let mutatedTextTags = 0;
187012
187316
  if (config2.caveman?.enabled) {
187013
187317
  const cavemanResult = applyCavemanCleanup(sessionId, db, targets, tags, {
187014
187318
  enabled: true,
@@ -187016,8 +187320,15 @@ function applyHeuristicCleanup(sessionId, db, targets, messageTagNumbers, config
187016
187320
  protectedTags: config2.protectedTags
187017
187321
  });
187018
187322
  compressedTextTags = cavemanResult.compressedToLite + cavemanResult.compressedToFull + cavemanResult.compressedToUltra;
187323
+ mutatedTextTags = cavemanResult.mutatedTextTags;
187019
187324
  }
187020
- return { droppedTools, deduplicatedTools, droppedInjections, compressedTextTags };
187325
+ return {
187326
+ droppedTools,
187327
+ deduplicatedTools,
187328
+ droppedInjections,
187329
+ compressedTextTags,
187330
+ mutatedTextTags
187331
+ };
187021
187332
  }
187022
187333
  function extractToolInfo(part) {
187023
187334
  if (part.type === "tool" && typeof part.tool === "string" && DEDUP_SAFE_TOOLS.has(part.tool)) {
@@ -187191,6 +187502,42 @@ function isTodoItem(value) {
187191
187502
  return typeof todo.content === "string" && typeof todo.status === "string" && (todo.priority === undefined || typeof todo.priority === "string");
187192
187503
  }
187193
187504
 
187505
+ // src/hooks/magic-context/tool-reclaim.ts
187506
+ await init_storage();
187507
+ function buildSyntheticToolReclaimOps(input) {
187508
+ const watermark = Math.max(0, input.watermark);
187509
+ if (watermark <= 0)
187510
+ return [];
187511
+ const realPendingTagIds = new Set((input.pendingOps ?? []).map((op) => op.tagId));
187512
+ const tags = getActiveTagsBySession(input.db, input.sessionId);
187513
+ const synthetic = [];
187514
+ for (const tag of tags) {
187515
+ if (tag.type !== "tool")
187516
+ continue;
187517
+ if (tag.status !== "active")
187518
+ continue;
187519
+ if (tag.tagNumber > watermark)
187520
+ continue;
187521
+ if (realPendingTagIds.has(tag.tagNumber))
187522
+ continue;
187523
+ if (input.targets.get(tag.tagNumber)?.canDrop?.() !== true)
187524
+ continue;
187525
+ synthetic.push({
187526
+ id: 0,
187527
+ sessionId: input.sessionId,
187528
+ tagId: tag.tagNumber,
187529
+ operation: "drop",
187530
+ queuedAt: 0
187531
+ });
187532
+ }
187533
+ return synthetic;
187534
+ }
187535
+ function advanceToolReclaimWatermarkToCurrentMax(db, sessionId) {
187536
+ const maxTagNumber = getMaxTagNumberBySession(db, sessionId);
187537
+ advanceToolReclaimWatermark(db, sessionId, maxTagNumber);
187538
+ return maxTagNumber;
187539
+ }
187540
+
187194
187541
  // src/hooks/magic-context/transform-postprocess-phase.ts
187195
187542
  var DEGRADE_CACHE_WARNING_THRESHOLD = 10;
187196
187543
  var degradedCacheCountBySession = new BoundedSessionMap(100);
@@ -187245,12 +187592,14 @@ async function runPostTransformPhase(args) {
187245
187592
  let deferredMaterializedSuccessfully = false;
187246
187593
  let heuristicsRanSuccessfully = false;
187247
187594
  let pendingOpsRanSuccessfully = false;
187595
+ let pendingOpsDidMutate = false;
187596
+ let heuristicOrReasoningDidMutate = false;
187248
187597
  try {
187249
187598
  if (shouldApplyPendingOps) {
187250
187599
  const applyReason = isExplicitFlush ? "explicit_flush" : deferredMaterialize ? "deferred_materialization" : `scheduler_execute (scheduler=${args.schedulerDecision})`;
187251
187600
  sessionLog(args.sessionId, `pending ops WILL APPLY — reason=${applyReason}, pendingOps=${pendingOps.length}, context=${args.contextUsage.percentage.toFixed(1)}%`);
187252
187601
  const tApply = performance.now();
187253
- applyPendingOperations(args.sessionId, args.db, args.targets, args.protectedTags, undefined, pendingOps);
187602
+ pendingOpsDidMutate = applyPendingOperations(args.sessionId, args.db, args.targets, args.protectedTags, undefined, pendingOps);
187254
187603
  logTransformTiming(args.sessionId, "applyPendingOperations", tApply);
187255
187604
  }
187256
187605
  if (shouldRunHeuristics) {
@@ -187268,7 +187617,8 @@ async function runPostTransformPhase(args) {
187268
187617
  } : undefined,
187269
187618
  caveman: cavemanConfig
187270
187619
  }, heuristicTags);
187271
- logTransformTiming(args.sessionId, "applyHeuristicCleanup", t5, `droppedTools=${cleanup.droppedTools} deduplicatedTools=${cleanup.deduplicatedTools} droppedInjections=${cleanup.droppedInjections} compressedTextTags=${cleanup.compressedTextTags}`);
187620
+ logTransformTiming(args.sessionId, "applyHeuristicCleanup", t5, `droppedTools=${cleanup.droppedTools} deduplicatedTools=${cleanup.deduplicatedTools} droppedInjections=${cleanup.droppedInjections} compressedTextTags=${cleanup.compressedTextTags} mutatedTextTags=${cleanup.mutatedTextTags}`);
187621
+ const heuristicMutationCount = cleanup.droppedTools + cleanup.deduplicatedTools + cleanup.droppedInjections + cleanup.mutatedTextTags;
187272
187622
  const t7 = performance.now();
187273
187623
  const clearedReasoning = clearOldReasoning(args.messages, args.reasoningByMessage, args.messageTagNumbers, args.clearReasoningAge);
187274
187624
  if (canUseEmptySentinels) {
@@ -187294,6 +187644,7 @@ async function runPostTransformPhase(args) {
187294
187644
  }
187295
187645
  }
187296
187646
  logTransformTiming(args.sessionId, "clearOldReasoning", t7);
187647
+ heuristicOrReasoningDidMutate = heuristicMutationCount + clearedReasoning + strippedInline > 0;
187297
187648
  if (pendingMaterializationAtPassStart) {
187298
187649
  args.pendingMaterializationSessions.delete(args.sessionId);
187299
187650
  }
@@ -187304,7 +187655,31 @@ async function runPostTransformPhase(args) {
187304
187655
  if (args.schedulerDecision === "execute" && !materializationRequested) {
187305
187656
  updateSessionMeta(args.db, args.sessionId, { lastResponseTime: Date.now() });
187306
187657
  }
187658
+ const toolReclaimExecutePass = args.schedulerDecision === "execute";
187659
+ const alreadyMutatingThisPass = pendingOpsDidMutate || heuristicOrReasoningDidMutate;
187660
+ let autoReclaimTargetCount = 0;
187661
+ let autoReclaimDidMutate = false;
187662
+ if (toolReclaimExecutePass && alreadyMutatingThisPass && !emergencyDropEligible) {
187663
+ const syntheticPendingOps = buildSyntheticToolReclaimOps({
187664
+ db: args.db,
187665
+ sessionId: args.sessionId,
187666
+ targets: args.targets,
187667
+ watermark: args.sessionMeta.toolReclaimWatermark ?? 0,
187668
+ pendingOps
187669
+ });
187670
+ autoReclaimTargetCount = syntheticPendingOps.length;
187671
+ if (syntheticPendingOps.length > 0) {
187672
+ autoReclaimDidMutate = applyPendingOperations(args.sessionId, args.db, args.targets, args.protectedTags, undefined, [], syntheticPendingOps);
187673
+ }
187674
+ }
187307
187675
  args.batch?.finalize();
187676
+ if (toolReclaimExecutePass) {
187677
+ const maxTagNumber = advanceToolReclaimWatermarkToCurrentMax(args.db, args.sessionId);
187678
+ args.sessionMeta.toolReclaimWatermark = Math.max(args.sessionMeta.toolReclaimWatermark ?? 0, maxTagNumber);
187679
+ }
187680
+ if (autoReclaimTargetCount > 0) {
187681
+ sessionLog(args.sessionId, `tool reclaim auto-drop: targets=${autoReclaimTargetCount} mutated=${autoReclaimDidMutate}`);
187682
+ }
187308
187683
  logTransformTiming(args.sessionId, "batchFinalize:heuristics", performance.now());
187309
187684
  if (args.sessionMeta.lastTransformError !== null) {
187310
187685
  updateSessionMeta(args.db, args.sessionId, { lastTransformError: null });
@@ -187316,11 +187691,6 @@ async function runPostTransformPhase(args) {
187316
187691
  deferredMaterializedSuccessfully = true;
187317
187692
  heuristicsRanSuccessfully = true;
187318
187693
  }
187319
- if (args.watermark > 0) {
187320
- const tWatermarkCleanup = performance.now();
187321
- truncateErroredTools(args.messages, args.watermark, args.messageTagNumbers);
187322
- logTransformTiming(args.sessionId, "watermarkCleanup", tWatermarkCleanup);
187323
- }
187324
187694
  if (shouldApplyPendingOps) {
187325
187695
  pendingOpsRanSuccessfully = true;
187326
187696
  }
@@ -188336,7 +188706,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
188336
188706
  let tailToolTokens;
188337
188707
  let liveTailTokens;
188338
188708
  try {
188339
- const agg = getActiveTagTokenAggregate(db, sessionId);
188709
+ const agg = getActiveTagTokenAggregate(db, sessionId, deps.protectedTags);
188340
188710
  tailToolTokens = agg.toolOutput;
188341
188711
  liveTailTokens = agg.conversation + agg.toolCall;
188342
188712
  } catch {
@@ -188347,6 +188717,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
188347
188717
  const executeThresholdTokens = Math.round((resolvedContextLimit ?? 0) * resolvedExecuteThresholdPct / 100);
188348
188718
  const usableTokens = Math.max(0, executeThresholdTokens - contextUsage.inputTokens + liveTailTokens);
188349
188719
  resetLastNudgeCycleIfTailShrank(db, sessionId, tailToolTokens);
188720
+ const oldestReclaimableToolTags = getOldestActiveUnprotectedToolTags(db, sessionId, deps.protectedTags);
188350
188721
  deps.channel1StateBySession.set(sessionId, {
188351
188722
  tailToolTokens,
188352
188723
  historyBudgetTokens: historyBudgetTokens ?? 0,
@@ -188355,9 +188726,10 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
188355
188726
  lastInputTokens: contextUsage.inputTokens,
188356
188727
  turnToolTokens: 0,
188357
188728
  usableTokens,
188358
- reducedSinceRefresh: false
188729
+ reducedSinceRefresh: false,
188730
+ oldestReclaimableToolTags
188359
188731
  });
188360
- const channel2MetricsKnown = fullFeatureMode && resolvedContextLimit !== undefined && resolvedContextLimit > 0 && resolvedExecuteThresholdPct > 0;
188732
+ const channel2MetricsKnown = resolvedContextLimit !== undefined && resolvedContextLimit > 0 && resolvedExecuteThresholdPct > 0;
188361
188733
  if (channel2MetricsKnown) {
188362
188734
  const channel2ShouldTrigger = shouldTriggerChannel2({
188363
188735
  reclaimableTokens: tailToolTokens,
@@ -188381,6 +188753,7 @@ Historian previously failed ${historianFailureState.failureCount} time(s), so Ma
188381
188753
  }
188382
188754
  const elapsed = (performance.now() - startTime).toFixed(1);
188383
188755
  sessionLog(sessionId, `transform completed in ${elapsed}ms (${messages.length} messages, ${targets.size} targets, watermark: ${watermark})`);
188756
+ deps.maybeAutoEmbedSession?.(sessionId);
188384
188757
  };
188385
188758
  }
188386
188759
  function resolveHistoryBudgetTokens(historyBudgetPercentage, contextUsage, executeThresholdPercentage, modelKey, executeThresholdTokens, resolvedContextLimit) {
@@ -188426,18 +188799,14 @@ function evictExpiredUsageEntries(contextUsageMap) {
188426
188799
  }
188427
188800
  async function deliverChannel2IfPending(deps, sessionId) {
188428
188801
  try {
188429
- try {
188430
- const meta3 = getOrCreateSessionMeta(deps.db, sessionId);
188431
- if (meta3.isSubagent)
188432
- return;
188433
- } catch {}
188434
188802
  const baseline = deps.channel1StateBySession?.get(sessionId);
188435
188803
  await maybeDeliverChannel2(sessionId, {
188436
188804
  db: deps.db,
188437
188805
  serverUrl: deps.serverUrl,
188438
188806
  directory: deps.directory ?? ".",
188439
188807
  reclaimableTokens: baseline ? baseline.tailToolTokens + baseline.turnToolTokens : undefined,
188440
- usableTokens: baseline?.usableTokens
188808
+ usableTokens: baseline?.usableTokens,
188809
+ oldestReclaimableToolTags: baseline?.oldestReclaimableToolTags
188441
188810
  });
188442
188811
  } catch (error51) {
188443
188812
  sessionLog(sessionId, "channel2 delivery wrapper failed (ignored):", error51);
@@ -188743,6 +189112,46 @@ function createEventHandler2(deps) {
188743
189112
  };
188744
189113
  }
188745
189114
 
189115
+ // src/hooks/magic-context/format-embed-status.ts
189116
+ function formatEmbedStatusText(coverage, drain) {
189117
+ if (!coverage.enabled) {
189118
+ return "Embedding is off (no provider configured).";
189119
+ }
189120
+ const lines = [];
189121
+ lines.push(`Embedding — model: ${coverage.model} (${coverage.provider})`);
189122
+ lines.push(`This session: ${coverage.session.embedded} / ${coverage.session.total} compartments embedded`);
189123
+ lines.push(`Project memories: ${coverage.memories.embedded} / ${coverage.memories.total} embedded`);
189124
+ if (coverage.commits.gitEnabled) {
189125
+ lines.push(`Git commits: ${coverage.commits.embedded} / ${coverage.commits.total}`);
189126
+ } else {
189127
+ lines.push("Git commits: 0 / 0 (git indexing off)");
189128
+ }
189129
+ let drainLine = "Drain: idle";
189130
+ switch (drain.status) {
189131
+ case "running": {
189132
+ const e = drain.embedded ?? coverage.session.embedded;
189133
+ const t = drain.total ?? coverage.session.total;
189134
+ const failedSuffix = drain.failed && drain.failed > 0 ? ` (${drain.failed} failed)` : "";
189135
+ drainLine = `Drain: running ${e}/${t}${failedSuffix}`;
189136
+ break;
189137
+ }
189138
+ case "paused": {
189139
+ const e = drain.embedded ?? coverage.session.embedded;
189140
+ const t = drain.total ?? coverage.session.total;
189141
+ drainLine = `Drain: paused ${e}/${t}`;
189142
+ break;
189143
+ }
189144
+ case "stopped":
189145
+ drainLine = "Drain: stopped (provider down)";
189146
+ break;
189147
+ default:
189148
+ drainLine = "Drain: idle";
189149
+ }
189150
+ lines.push(drainLine);
189151
+ return lines.join(`
189152
+ `);
189153
+ }
189154
+
188746
189155
  // src/hooks/magic-context/hook.ts
188747
189156
  await __promiseAll([
188748
189157
  init_inject_compartments(),
@@ -188947,10 +189356,11 @@ function maybeInjectChannel1Nudge(args, sessionId, tool, output) {
188947
189356
  contextLimit: state.contextLimit,
188948
189357
  executeThresholdPercentage: state.executeThresholdPercentage
188949
189358
  });
189359
+ const workingWindowTokens = Math.round(state.contextLimit * state.executeThresholdPercentage / 100);
188950
189360
  const decision = decideChannel1({
188951
189361
  undroppedTokens,
188952
189362
  pressure,
188953
- historyBudgetTokens: state.historyBudgetTokens,
189363
+ workingWindowTokens,
188954
189364
  lastNudgeUndropped: getLastNudgeUndropped(args.db, sessionId),
188955
189365
  lastNudgeLevel: getLastNudgeLevel(args.db, sessionId),
188956
189366
  hasRecentReduce: false
@@ -188959,7 +189369,7 @@ function maybeInjectChannel1Nudge(args, sessionId, tool, output) {
188959
189369
  setLastNudgeLevel(args.db, sessionId, decision.nextLastNudgeLevel);
188960
189370
  if (!decision.fire)
188961
189371
  return;
188962
- out.output += buildChannel1Reminder(decision.level, decision.undroppedTokens);
189372
+ out.output += buildChannel1Reminder(decision.level, decision.undroppedTokens, state.oldestReclaimableToolTags);
188963
189373
  sessionLog(sessionId, `channel1 nudge fired: level=${decision.level} undropped~${Math.round(decision.undroppedTokens / 1000)}k tool=${tool}`);
188964
189374
  }
188965
189375
  function createToolExecuteAfterHook(args) {
@@ -189031,9 +189441,7 @@ Context is managed for you entirely automatically — there's nothing to prune a
189031
189441
  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).`;
189032
189442
  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.`;
189033
189443
  var BASE_INTRO = (protectedTags) => `Messages and tool outputs are tagged with §N§ identifiers (e.g., §1§, §42§).
189034
- Use \`ctx_reduce\` to manage context size. It supports one operation:
189035
- - \`drop\`: Remove entirely (best for tool outputs you already acted on).
189036
- Syntax: "3-5", "1,2,9", or "1-5,8,12-15". Last ${protectedTags} tags are protected.
189444
+ 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".
189037
189445
  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.
189038
189446
  ${CTX_NOTE_GUIDANCE}
189039
189447
  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.
@@ -189052,7 +189460,7 @@ Use \`ctx_expand\` to recover the raw conversation behind a \`<compartment>\` su
189052
189460
  \`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.
189053
189461
  ${TOOL_HISTORY_GUIDANCE}
189054
189462
  NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
189055
- NEVER drop user messagesthey are short and will be summarized by compartmentalization automatically. Dropping them loses context the historian needs.
189463
+ 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\`.
189056
189464
  NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
189057
189465
  Before your turn finishes, consider using \`ctx_reduce\` to drop large tool outputs you no longer need.`;
189058
189466
  var BASE_INTRO_NO_REDUCE = () => `${CTX_NOTE_GUIDANCE}
@@ -189473,29 +189881,55 @@ function createMagicContextHook(deps) {
189473
189881
  ensureProjectRegistered: ensureProjectRegisteredFromOpenCodeDirectory,
189474
189882
  getNotificationParams: (sid) => getLiveNotificationParams(sid, liveModelBySession, variantBySession, agentBySession)
189475
189883
  });
189476
- const executeEmbedHistory = async (sessionId) => {
189884
+ const executeEmbedHistory = async (sessionId, options) => {
189477
189885
  if (deps.config.memory?.enabled === false) {
189478
189886
  return "Memory is disabled for this project, so there is no semantic embedding to backfill.";
189479
189887
  }
189480
189888
  const directory = sessionDirectoryBySession.get(sessionId) ?? deps.directory;
189889
+ const active = embedRunStateBySession.get(sessionId);
189890
+ if (active && !active.signal.aborted && !options?.signal) {
189891
+ return "Embedding is already running for this session.";
189892
+ }
189481
189893
  await ensureProjectRegisteredFromOpenCodeDirectory(directory, db);
189482
189894
  const sessionProjectIdentity = resolveProjectIdentity(directory);
189483
- setRecompStarting({ recompProgressBySession }, sessionId, "Embedding history…", "embed");
189484
- const outcome = await embedSessionCompartmentChunks(db, sessionProjectIdentity, sessionId, {
189485
- onProgress: ({ embedded, total }) => {
189486
- const cur = recompProgressBySession.get(sessionId);
189487
- if (!cur || cur.phase !== "recomp")
189488
- return;
189489
- recompProgressBySession.set(sessionId, {
189490
- ...cur,
189491
- processedMessages: embedded,
189492
- totalMessages: total,
189493
- updatedAt: Date.now()
189494
- });
189895
+ embedPauseBySession.delete(sessionId);
189896
+ const prior = embedRunStateBySession.get(sessionId);
189897
+ if (prior)
189898
+ prior.abort();
189899
+ const controller = new AbortController;
189900
+ embedRunStateBySession.set(sessionId, controller);
189901
+ const signal = options?.signal ?? controller.signal;
189902
+ if (!options?.silent) {
189903
+ setRecompStarting({ recompProgressBySession }, sessionId, "Embedding history…", "embed");
189904
+ }
189905
+ let runFailed = 0;
189906
+ let outcome;
189907
+ try {
189908
+ outcome = await embedSessionCompartmentChunks(db, sessionProjectIdentity, sessionId, {
189909
+ signal,
189910
+ onProgress: ({ embedded, total }) => {
189911
+ const cur = recompProgressBySession.get(sessionId);
189912
+ if (!cur || cur.phase !== "recomp")
189913
+ return;
189914
+ recompProgressBySession.set(sessionId, {
189915
+ ...cur,
189916
+ processedMessages: embedded,
189917
+ totalMessages: total,
189918
+ updatedAt: Date.now()
189919
+ });
189920
+ }
189921
+ });
189922
+ } finally {
189923
+ if (embedRunStateBySession.get(sessionId) === controller) {
189924
+ embedRunStateBySession.delete(sessionId);
189495
189925
  }
189496
- });
189926
+ }
189927
+ if ("failed" in outcome)
189928
+ runFailed = outcome.failed;
189497
189929
  const terminal = (phase, message) => {
189498
- setRecompTerminal({ recompProgressBySession }, sessionId, phase, message);
189930
+ if (!options?.silent) {
189931
+ setRecompTerminal({ recompProgressBySession }, sessionId, phase, message);
189932
+ }
189499
189933
  return message;
189500
189934
  };
189501
189935
  switch (outcome.status) {
@@ -189504,15 +189938,78 @@ function createMagicContextHook(deps) {
189504
189938
  case "disabled":
189505
189939
  return terminal("skipped", "No embedding provider is configured, so there is nothing to embed.");
189506
189940
  case "busy":
189507
- return terminal("skipped", `Embedding is already running for this project — ${outcome.total} compartment${outcome.total === 1 ? "" : "s"} still pending. Try again shortly.`);
189508
- case "aborted":
189509
- return terminal("done", `Embedded ${outcome.embedded} of ${outcome.total} compartments before stopping.`);
189941
+ return terminal("skipped", "Embedding is already running for this project. Try again shortly.");
189942
+ case "aborted": {
189943
+ const cov = getEmbeddingCoverageStatus(db, sessionProjectIdentity, sessionId);
189944
+ const msg = `Paused at ${cov.session.embedded}/${cov.session.total} compartments embedded.`;
189945
+ return terminal("skipped", msg);
189946
+ }
189510
189947
  case "stalled":
189511
- 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.`);
189948
+ 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.`);
189512
189949
  default:
189513
- return terminal("done", `Embedded ${outcome.embedded} compartment${outcome.embedded === 1 ? "" : "s"} of history for semantic search.`);
189950
+ return terminal("done", `Embedded ${outcome.embedded} compartment${outcome.embedded === 1 ? "" : "s"} of history for semantic search${runFailed > 0 ? ` (${runFailed} failed)` : ""}.`);
189514
189951
  }
189515
189952
  };
189953
+ const pauseEmbedDrain = (sessionId) => {
189954
+ embedPauseBySession.add(sessionId);
189955
+ const ctrl = embedRunStateBySession.get(sessionId);
189956
+ if (ctrl)
189957
+ ctrl.abort();
189958
+ const directory = sessionDirectoryBySession.get(sessionId) ?? deps.directory;
189959
+ const sessionProjectIdentity = resolveProjectIdentity(directory);
189960
+ const cov = getEmbeddingCoverageStatus(db, sessionProjectIdentity, sessionId);
189961
+ return `Paused at ${cov.session.embedded}/${cov.session.total} compartments embedded.`;
189962
+ };
189963
+ const getEmbedStatusText = (sessionId) => {
189964
+ const directory = sessionDirectoryBySession.get(sessionId) ?? deps.directory;
189965
+ const sessionProjectIdentity = resolveProjectIdentity(directory);
189966
+ const coverage = getEmbeddingCoverageStatus(db, sessionProjectIdentity, sessionId);
189967
+ const progress = recompProgressBySession.get(sessionId);
189968
+ const drainUi = getEmbedDrainUiStatus(sessionId, progress);
189969
+ return formatEmbedStatusText(coverage, {
189970
+ status: drainUi.status,
189971
+ embedded: progress?.processedMessages,
189972
+ total: progress?.totalMessages
189973
+ });
189974
+ };
189975
+ const maybeAutoEmbedSession = (sessionId) => {
189976
+ if (autoEmbedAttemptedBySession.has(sessionId))
189977
+ return;
189978
+ if (embedPauseBySession.has(sessionId))
189979
+ return;
189980
+ if (deps.config.memory?.enabled === false)
189981
+ return;
189982
+ autoEmbedAttemptedBySession.add(sessionId);
189983
+ const directory = sessionDirectoryBySession.get(sessionId) ?? deps.directory;
189984
+ (async () => {
189985
+ try {
189986
+ await new Promise((resolve7) => setTimeout(resolve7, 0));
189987
+ await ensureProjectRegisteredFromOpenCodeDirectory(directory, db);
189988
+ const sessionProjectIdentity = resolveProjectIdentity(directory);
189989
+ const coverage = getEmbeddingCoverageStatus(db, sessionProjectIdentity, sessionId);
189990
+ if (!coverage.enabled)
189991
+ return;
189992
+ const remaining = coverage.session.total - coverage.session.embedded;
189993
+ if (remaining <= 0)
189994
+ return;
189995
+ const notifyParams = getLiveNotificationParams(sessionId, liveModelBySession, variantBySession, agentBySession);
189996
+ if (!isTuiConnected(sessionId)) {
189997
+ const startMsg = `Embedding ${remaining} compartment${remaining === 1 ? "" : "s"} of history in the background…`;
189998
+ await sendIgnoredMessage(deps.client, sessionId, startMsg, {
189999
+ ...notifyParams
190000
+ });
190001
+ }
190002
+ const summary = await executeEmbedHistory(sessionId);
190003
+ if (!isTuiConnected(sessionId)) {
190004
+ await sendIgnoredMessage(deps.client, sessionId, summary, {
190005
+ ...notifyParams
190006
+ });
190007
+ }
190008
+ } catch (error51) {
190009
+ log("[magic-context] auto-embed drain failed:", error51);
190010
+ }
190011
+ })();
190012
+ };
189516
190013
  const sidekickRunnable = isSidekickRunnable(deps.config);
189517
190014
  const sidekickConfig = sidekickRunnable ? deps.config.sidekick : undefined;
189518
190015
  const transform2 = createTransform({
@@ -189574,7 +190071,8 @@ function createMagicContextHook(deps) {
189574
190071
  cavemanTextCompression: ctxReduceEnabled === false && deps.config.caveman_text_compression?.enabled === true ? {
189575
190072
  enabled: true,
189576
190073
  minChars: deps.config.caveman_text_compression.min_chars ?? 500
189577
- } : undefined
190074
+ } : undefined,
190075
+ maybeAutoEmbedSession
189578
190076
  });
189579
190077
  const eventHandler = createEventHandler2({
189580
190078
  contextUsageMap,
@@ -189603,6 +190101,7 @@ function createMagicContextHook(deps) {
189603
190101
  recompProgressBySession.delete(sessionId);
189604
190102
  internalChildSessions.delete(sessionId);
189605
190103
  channel1StateBySession.delete(sessionId);
190104
+ clearEmbedSessionState(sessionId);
189606
190105
  }
189607
190106
  });
189608
190107
  const runDreamQueueInBackground = () => {
@@ -189668,6 +190167,8 @@ function createMagicContextHook(deps) {
189668
190167
  executeRecomp: historianRunnable ? async (sessionId, options) => runManagedRecomp(buildManagedRecompCtx(sessionId), sessionId, options) : undefined,
189669
190168
  runUpgrade: historianRunnable ? async (sessionId) => runManagedUpgrade(buildManagedRecompCtx(sessionId), sessionId) : undefined,
189670
190169
  executeEmbedHistory,
190170
+ pauseEmbedDrain,
190171
+ getEmbedStatusText,
189671
190172
  sendNotification: async (sessionId, text, params) => {
189672
190173
  await sendIgnoredMessage(deps.client, sessionId, text, {
189673
190174
  ...getLiveNotificationParams(sessionId, liveModelBySession, variantBySession, agentBySession),
@@ -189879,6 +190380,7 @@ function truncateError(name2, code, message, maxLen = 240) {
189879
190380
 
189880
190381
  // src/plugin/rpc-handlers.ts
189881
190382
  init_project_identity();
190383
+ init_project_embedding_registry();
189882
190384
  init_tool_definition_tokens();
189883
190385
  await init_storage();
189884
190386
 
@@ -190599,6 +191101,26 @@ function buildStatusDetail(db, sessionId, directory, modelKey, config2, liveSess
190599
191101
  }
190600
191102
  return detail;
190601
191103
  }
191104
+ function buildEmbedDetail(db, sessionId, dir, liveSessionState) {
191105
+ const projectIdentity = resolveProjectIdentity(dir);
191106
+ const coverage = getEmbeddingCoverageStatus(db, projectIdentity, sessionId);
191107
+ const progress = liveSessionState.recompProgressBySession.get(sessionId);
191108
+ const drainUi = getEmbedDrainUiStatus(sessionId, progress);
191109
+ const statusText = formatEmbedStatusText(coverage, {
191110
+ status: drainUi.status,
191111
+ embedded: progress?.processedMessages,
191112
+ total: progress?.totalMessages
191113
+ });
191114
+ return {
191115
+ enabled: coverage.enabled,
191116
+ model: coverage.model,
191117
+ provider: coverage.provider,
191118
+ session: coverage.session,
191119
+ memories: coverage.memories,
191120
+ commits: coverage.commits,
191121
+ statusText
191122
+ };
191123
+ }
190602
191124
  function registerRpcHandlers(rpcServer, args) {
190603
191125
  const { directory, config: config2, liveSessionState } = args;
190604
191126
  const rawConfig = config2;
@@ -190621,6 +191143,19 @@ function registerRpcHandlers(rpcServer, args) {
190621
191143
  return { error: "unavailable" };
190622
191144
  return buildStatusDetail(db, sessionId, dir, modelKey, rawConfig, liveSessionState, injectionBudgetTokens);
190623
191145
  });
191146
+ rpcServer.handle("embed-detail", async (params) => {
191147
+ const sessionId = String(params.sessionId ?? "");
191148
+ const dir = String(params.directory ?? directory);
191149
+ const db = getDb();
191150
+ if (!db || !sessionId)
191151
+ return { error: "unavailable" };
191152
+ try {
191153
+ return buildEmbedDetail(db, sessionId, dir, liveSessionState);
191154
+ } catch (err) {
191155
+ log("[rpc] embed-detail error:", err);
191156
+ return { error: "unavailable" };
191157
+ }
191158
+ });
190624
191159
  rpcServer.handle("compartment-count", async (params) => {
190625
191160
  const sessionId = String(params.sessionId ?? "");
190626
191161
  const db = getDb();
@@ -190743,27 +191278,225 @@ Older parts of this session are summarized into <compartment> blocks inside <ses
190743
191278
 
190744
191279
  ctx_expand(start=120, end=245) ← the compartment's own start/end attributes
190745
191280
 
190746
- 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.`;
191281
+ 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.
191282
+
191283
+ Two recovery modes for finer detail:
191284
+ - 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.
191285
+ - 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.`;
190747
191286
  var CTX_EXPAND_TOKEN_BUDGET = 15000;
190748
191287
 
191288
+ // src/tools/ctx-expand/render.ts
191289
+ init_read_session_formatting();
191290
+ await init_read_session_chunk();
191291
+ function isRecord3(value) {
191292
+ return value !== null && typeof value === "object" && !Array.isArray(value);
191293
+ }
191294
+ function roleLabel(role) {
191295
+ if (role === "assistant")
191296
+ return "A (assistant)";
191297
+ if (role === "user")
191298
+ return "U (user)";
191299
+ return role;
191300
+ }
191301
+ function truncate2(value, max) {
191302
+ const t = value.trim();
191303
+ return t.length <= max ? t : `${t.slice(0, max)}…`;
191304
+ }
191305
+ function keyArg(input) {
191306
+ if (!input)
191307
+ return "";
191308
+ for (const k of ["filePath", "path", "pattern", "query", "symbol", "module", "action"]) {
191309
+ const v = input[k];
191310
+ if (typeof v === "string" && v.length > 0)
191311
+ return truncate2(v, 60);
191312
+ }
191313
+ if (typeof input.description === "string")
191314
+ return truncate2(input.description, 60);
191315
+ return "";
191316
+ }
191317
+ function asToolPart(part) {
191318
+ const type = typeof part.type === "string" ? part.type : "";
191319
+ if (type === "tool") {
191320
+ const state = isRecord3(part.state) ? part.state : null;
191321
+ const output = state && typeof state.output === "string" ? state.output : state && state.output != null ? JSON.stringify(state.output) : null;
191322
+ const metadata = state && isRecord3(state.metadata) ? state.metadata : null;
191323
+ const title = state && typeof state.title === "string" && state.title || metadata && typeof metadata.title === "string" && metadata.title || null;
191324
+ return {
191325
+ name: typeof part.tool === "string" ? part.tool : "tool",
191326
+ callId: typeof part.callID === "string" ? part.callID : "",
191327
+ title,
191328
+ input: state && isRecord3(state.input) ? state.input : null,
191329
+ output
191330
+ };
191331
+ }
191332
+ if (type === "tool_use") {
191333
+ return {
191334
+ name: typeof part.name === "string" ? part.name : "tool",
191335
+ callId: typeof part.id === "string" ? part.id : "",
191336
+ title: null,
191337
+ input: isRecord3(part.input) ? part.input : null,
191338
+ output: null
191339
+ };
191340
+ }
191341
+ if (type === "tool_result") {
191342
+ const content = part.content;
191343
+ const output = typeof content === "string" ? content : content != null ? JSON.stringify(content) : null;
191344
+ return {
191345
+ name: "tool_result",
191346
+ callId: typeof part.tool_use_id === "string" ? part.tool_use_id : "",
191347
+ title: null,
191348
+ input: null,
191349
+ output
191350
+ };
191351
+ }
191352
+ return null;
191353
+ }
191354
+ function textOf(part) {
191355
+ if (part.type === "text" && typeof part.text === "string")
191356
+ return part.text;
191357
+ return null;
191358
+ }
191359
+ function reasoningOf(part) {
191360
+ if ((part.type === "reasoning" || part.type === "thinking") && typeof part.text === "string") {
191361
+ return part.text;
191362
+ }
191363
+ return null;
191364
+ }
191365
+ function renderPartPreview(part) {
191366
+ if (!isRecord3(part))
191367
+ return null;
191368
+ const text = textOf(part);
191369
+ if (text !== null) {
191370
+ const t = truncate2(text, 200);
191371
+ return t.length > 0 ? ` • ${t}` : null;
191372
+ }
191373
+ const tool = asToolPart(part);
191374
+ if (tool) {
191375
+ const arg = keyArg(tool.input);
191376
+ const head = arg ? `${tool.name}(${arg})` : tool.name;
191377
+ return tool.output !== null ? ` • tool ${head} → output ~${estimateTokens(tool.output)} tok` : ` • tool ${head}`;
191378
+ }
191379
+ const reasoning = reasoningOf(part);
191380
+ if (reasoning !== null)
191381
+ return ` • [reasoning] ${truncate2(reasoning, 120)}`;
191382
+ const type = typeof part.type === "string" ? part.type : "part";
191383
+ if (type === "file")
191384
+ return " • [file]";
191385
+ if (type === "step-start" || type === "step-finish")
191386
+ return null;
191387
+ return ` • [${type}]`;
191388
+ }
191389
+ function renderPartFull(part) {
191390
+ if (!isRecord3(part))
191391
+ return null;
191392
+ const text = textOf(part);
191393
+ if (text !== null) {
191394
+ return text.trim().length > 0 ? ` [text]
191395
+ ${text}` : null;
191396
+ }
191397
+ const tool = asToolPart(part);
191398
+ if (tool) {
191399
+ const lines = [];
191400
+ const idSuffix = tool.callId ? ` #${tool.callId}` : "";
191401
+ lines.push(` [tool: ${tool.name}${idSuffix}]`);
191402
+ if (tool.title && tool.title.trim().length > 0) {
191403
+ lines.push(` description: ${tool.title.trim()}`);
191404
+ }
191405
+ if (tool.input)
191406
+ lines.push(` input: ${JSON.stringify(tool.input)}`);
191407
+ if (tool.output !== null)
191408
+ lines.push(` output:
191409
+ ${tool.output}`);
191410
+ return lines.join(`
191411
+ `);
191412
+ }
191413
+ const type = typeof part.type === "string" ? part.type : "part";
191414
+ if (type === "file") {
191415
+ const name2 = typeof part.filename === "string" && part.filename || typeof part.url === "string" && part.url || "";
191416
+ return ` [file]${name2 ? ` ${name2}` : ""}`;
191417
+ }
191418
+ return null;
191419
+ }
191420
+ function renderMessageByOrdinal(sessionId, ordinal) {
191421
+ const msg = readRawSessionMessages(sessionId).find((m) => m.ordinal === ordinal);
191422
+ if (!msg) {
191423
+ 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.`;
191424
+ }
191425
+ const rendered = msg.parts.map(renderPartFull).filter((l) => l !== null);
191426
+ const lines = [`[${msg.ordinal}] ${roleLabel(msg.role)} — full recovery:`, ""];
191427
+ if (rendered.length === 0) {
191428
+ lines.push(" (no recoverable content — message had only structural/reasoning parts)");
191429
+ } else {
191430
+ lines.push(...rendered);
191431
+ }
191432
+ return lines.join(`
191433
+ `);
191434
+ }
191435
+ function renderVerboseRange(sessionId, start, end, tokenBudget) {
191436
+ const messages = readRawSessionMessages(sessionId).filter((m) => m.ordinal >= start && m.ordinal <= end);
191437
+ const out = [];
191438
+ let usedTokens = 0;
191439
+ let lastOrdinal = start - 1;
191440
+ let truncated = false;
191441
+ for (const msg of messages) {
191442
+ const header = `[${msg.ordinal}] ${roleLabel(msg.role)}`;
191443
+ const partLines = msg.parts.map(renderPartPreview).filter((l) => l !== null);
191444
+ const block = partLines.length > 0 ? `${header}
191445
+ ${partLines.join(`
191446
+ `)}` : header;
191447
+ const blockTokens = estimateTokens(block);
191448
+ if (usedTokens + blockTokens > tokenBudget && out.length > 0) {
191449
+ truncated = true;
191450
+ break;
191451
+ }
191452
+ out.push(block);
191453
+ usedTokens += blockTokens;
191454
+ lastOrdinal = msg.ordinal;
191455
+ }
191456
+ return { text: out.join(`
191457
+
191458
+ `), lastOrdinal, truncated };
191459
+ }
191460
+
190749
191461
  // src/tools/ctx-expand/tools.ts
190750
191462
  function createCtxExpandTool(deps) {
190751
191463
  return tool({
190752
191464
  description: CTX_EXPAND_DESCRIPTION,
190753
191465
  args: {
190754
- 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`),
190755
- end: tool.schema.number().describe(`Last message ordinal to expand (inclusive) — a compartment's end="M" attribute`)
191466
+ 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`),
191467
+ end: tool.schema.number().optional().describe(`Last message ordinal to expand (inclusive) — a compartment's end="M" attribute`),
191468
+ 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."),
191469
+ 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.")
190756
191470
  },
190757
191471
  async execute(args, toolContext) {
190758
191472
  const sessionId = toolContext.sessionID;
191473
+ if (typeof args.message === "number" && args.message >= 1) {
191474
+ return renderMessageByOrdinal(sessionId, args.message);
191475
+ }
190759
191476
  if (!args.start || !args.end || args.start < 1 || args.end < args.start) {
190760
- return "Error: start and end must be positive integers with start <= end.";
191477
+ return "Error: provide either message=<ordinal>, or start and end (positive integers, start <= end).";
190761
191478
  }
190762
191479
  const lastCompartmentEnd = getLastCompartmentEndMessage(deps.db, sessionId);
190763
191480
  if (lastCompartmentEnd >= 0 && args.start > lastCompartmentEnd) {
190764
191481
  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.`;
190765
191482
  }
190766
191483
  const effectiveEnd = lastCompartmentEnd >= 0 ? Math.min(args.end, lastCompartmentEnd) : args.end;
191484
+ if (args.verbose === true) {
191485
+ const v = renderVerboseRange(sessionId, args.start, effectiveEnd, CTX_EXPAND_TOKEN_BUDGET);
191486
+ if (!v.text) {
191487
+ return `No messages found in range ${args.start}-${effectiveEnd}. The range may be outside this session's history.`;
191488
+ }
191489
+ const out = [
191490
+ `Messages ${args.start}-${v.lastOrdinal} (verbose). Recover any one in full with ctx_expand(message=<ordinal>):`,
191491
+ "",
191492
+ v.text
191493
+ ];
191494
+ if (v.truncated) {
191495
+ 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.`);
191496
+ }
191497
+ return out.join(`
191498
+ `);
191499
+ }
190767
191500
  const chunk = readSessionChunk(sessionId, CTX_EXPAND_TOKEN_BUDGET, args.start, effectiveEnd + 1);
190768
191501
  if (!chunk.text || chunk.messageCount === 0) {
190769
191502
  return `No messages found in range ${args.start}-${args.end}. The range may be outside this session's history.`;
@@ -191457,15 +192190,16 @@ function createCtxNoteTools(deps) {
191457
192190
  };
191458
192191
  }
191459
192192
  // src/tools/ctx-reduce/constants.ts
191460
- var CTX_REDUCE_DESCRIPTION = `Reduce context by dropping tagged content you no longer need.
191461
- Use §N§ identifiers visible in conversation. The \`drop\` param accepts ranges: "3-5", "1,2,9", "1-5,8".
191462
-
191463
- CRITICAL RULES:
191464
- - NEVER blanket-drop large ranges (e.g., "1-50"). Always review what each tag contains first.
191465
- - Only drop tool outputs you have already processed and no longer need.
191466
- - Protected tags are accepted but deferred until they leave the last protected range.
191467
- - Keep recent context only reduce OLD content that is no longer relevant to current work.
191468
- - Dropped content is gone forever.`;
192193
+ 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".
192194
+
192195
+ How it works:
192196
+ - 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.
192197
+ - The newest tags are protected: marking one just queues it until it ages out of the recent window, so marking recent output is harmless.
192198
+ - 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?".
192199
+
192200
+ 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.
192201
+ Keep: user messages, unresolved errors, raw evidence you haven't extracted yet, and outputs whose exact wording may matter later.
192202
+ Never blanket-mark large ranges (e.g. "1-50") — review what each tag holds first.`;
191469
192203
  // src/tools/ctx-reduce/tools.ts
191470
192204
  import { tool as tool4 } from "@opencode-ai/plugin";
191471
192205
 
@@ -192048,6 +192782,9 @@ class MagicContextRpcServer {
192048
192782
  }
192049
192783
 
192050
192784
  // src/index.ts
192785
+ var HISTORIAN_MAX_STEPS = 40;
192786
+ var SIDEKICK_MAX_STEPS = 40;
192787
+ var DREAMER_MAX_STEPS = 150;
192051
192788
  var plugin = async (ctx) => {
192052
192789
  const pluginConfig = loadPluginConfig(ctx.directory);
192053
192790
  setSqlitePragmaConfig({
@@ -192264,11 +193001,13 @@ var plugin = async (ctx) => {
192264
193001
  await hooks.magicContext?.["experimental.text.complete"]?.(input, output);
192265
193002
  },
192266
193003
  config: async (config2) => {
192267
- const buildHiddenAgentConfig = (agentId, prompt, allowedTools, overrides) => {
193004
+ const buildHiddenAgentConfig = (agentId, prompt, allowedTools, maxSteps, overrides) => {
192268
193005
  const { permission: overridePermission, ...restOverrides } = overrides ?? {};
192269
193006
  const basePermission = buildAllowOnlyPermission(allowedTools);
192270
193007
  return {
192271
193008
  prompt,
193009
+ steps: maxSteps,
193010
+ maxSteps,
192272
193011
  ...getAgentFallbackModels(agentId) ? { fallback_models: getAgentFallbackModels(agentId) } : {},
192273
193012
  ...restOverrides,
192274
193013
  permission: {
@@ -192309,10 +193048,10 @@ var plugin = async (ctx) => {
192309
193048
  })() : undefined;
192310
193049
  config2.agent = {
192311
193050
  ...config2.agent ?? {},
192312
- [DREAMER_AGENT]: buildHiddenAgentConfig(DREAMER_AGENT, DREAMER_SYSTEM_PROMPT, DREAMER_ALLOWED_TOOLS, dreamerAgentOverrides),
192313
- [HISTORIAN_AGENT]: buildHiddenAgentConfig(HISTORIAN_AGENT, COMPARTMENT_AGENT_SYSTEM_PROMPT, HISTORIAN_ALLOWED_TOOLS, historianAgentOverrides),
192314
- [HISTORIAN_EDITOR_AGENT]: buildHiddenAgentConfig(HISTORIAN_EDITOR_AGENT, HISTORIAN_EDITOR_SYSTEM_PROMPT, HISTORIAN_ALLOWED_TOOLS, historianAgentOverrides),
192315
- [SIDEKICK_AGENT]: buildHiddenAgentConfig(SIDEKICK_AGENT, SIDEKICK_SYSTEM_PROMPT, SIDEKICK_ALLOWED_TOOLS, sidekickAgentOverrides)
193051
+ [DREAMER_AGENT]: buildHiddenAgentConfig(DREAMER_AGENT, DREAMER_SYSTEM_PROMPT, DREAMER_ALLOWED_TOOLS, DREAMER_MAX_STEPS, dreamerAgentOverrides),
193052
+ [HISTORIAN_AGENT]: buildHiddenAgentConfig(HISTORIAN_AGENT, COMPARTMENT_AGENT_SYSTEM_PROMPT, HISTORIAN_ALLOWED_TOOLS, HISTORIAN_MAX_STEPS, historianAgentOverrides),
193053
+ [HISTORIAN_EDITOR_AGENT]: buildHiddenAgentConfig(HISTORIAN_EDITOR_AGENT, HISTORIAN_EDITOR_SYSTEM_PROMPT, HISTORIAN_ALLOWED_TOOLS, HISTORIAN_MAX_STEPS, historianAgentOverrides),
193054
+ [SIDEKICK_AGENT]: buildHiddenAgentConfig(SIDEKICK_AGENT, SIDEKICK_SYSTEM_PROMPT, SIDEKICK_ALLOWED_TOOLS, SIDEKICK_MAX_STEPS, sidekickAgentOverrides)
192316
193055
  };
192317
193056
  }
192318
193057
  };