@wolfx/pi-magic-context 0.24.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8275,6 +8275,14 @@ function buildNodeSqliteDatabaseClass(DatabaseSync) {
8275
8275
  }
8276
8276
  super(typeof filename === "string" ? filename : ":memory:", translated);
8277
8277
  }
8278
+ prepare(sql) {
8279
+ const stmt = super.prepare(sql);
8280
+ for (const method of ["run", "get", "all"]) {
8281
+ const original = stmt[method].bind(stmt);
8282
+ stmt[method] = (...args) => args.length === 1 && Array.isArray(args[0]) ? original(...args[0]) : original(...args);
8283
+ }
8284
+ return stmt;
8285
+ }
8278
8286
  transaction(fn) {
8279
8287
  const self = this;
8280
8288
  const wrapped = function(...args) {
@@ -141059,6 +141067,8 @@ function readRawSessionTailFromDb(db, sessionId, baseOrdinal, anchorMessageId) {
141059
141067
  var encoder = new TextEncoder;
141060
141068
  var TAG_PREFIX_REGEX = /^(?:§\d+§\s*)+/;
141061
141069
  var MALFORMED_TAG_PREFIX_REGEX = /^(?:§\d+">§(?:\d+§)?\s*)+/;
141070
+ var DANGLING_TAG_GLOBAL_REGEX = /\u00a7\d+(?!\.\d)[^\s\u00a7\w.]?/g;
141071
+ var DANGLING_TAG_PREFIX_REGEX = /^(?:\u00a7\d+(?!\.\d)[^\s\u00a7\w.]?\s*)+/;
141062
141072
  var COMPLETE_TAG_PAIR_GLOBAL_REGEX = /\u00a7\d+\u00a7/g;
141063
141073
  var MALFORMED_TAG_GLOBAL_REGEX = /\u00a7\d+">(?:\u00a7(?:\d+\u00a7)?)?/g;
141064
141074
  var STRAY_SECTION_CHAR_REGEX = /\u00a7/g;
@@ -141071,6 +141081,9 @@ function stripCompleteTagPairsGlobally(value) {
141071
141081
  function stripMalformedTagNotationGlobally(value) {
141072
141082
  return value.replace(MALFORMED_TAG_GLOBAL_REGEX, "");
141073
141083
  }
141084
+ function stripDanglingTagNotationGlobally(value) {
141085
+ return value.replace(DANGLING_TAG_GLOBAL_REGEX, "");
141086
+ }
141074
141087
  function stripTagSectionCharacters(value) {
141075
141088
  return value.replace(STRAY_SECTION_CHAR_REGEX, "");
141076
141089
  }
@@ -141078,6 +141091,7 @@ function stripPersistedAssistantText(value) {
141078
141091
  let text = stripWellFormedLeadingTagPrefix(value);
141079
141092
  text = stripCompleteTagPairsGlobally(text);
141080
141093
  text = stripMalformedTagNotationGlobally(text);
141094
+ text = stripDanglingTagNotationGlobally(text);
141081
141095
  text = stripTagSectionCharacters(text);
141082
141096
  return text.trim();
141083
141097
  }
@@ -141090,6 +141104,7 @@ function stripTagPrefix(value) {
141090
141104
  const prev = stripped;
141091
141105
  stripped = stripped.replace(MALFORMED_TAG_PREFIX_REGEX, "");
141092
141106
  stripped = stripped.replace(TAG_PREFIX_REGEX, "");
141107
+ stripped = stripped.replace(DANGLING_TAG_PREFIX_REGEX, "");
141093
141108
  if (stripped === prev)
141094
141109
  break;
141095
141110
  }
@@ -141957,6 +141972,7 @@ var SESSION_META_SELECT_COLUMNS = [
141957
141972
  "conversation_tokens",
141958
141973
  "tool_call_tokens",
141959
141974
  "cleared_reasoning_through_tag",
141975
+ "tool_reclaim_watermark",
141960
141976
  "last_todo_state",
141961
141977
  "cached_m0_bytes",
141962
141978
  "cached_m1_bytes",
@@ -142005,6 +142021,7 @@ var META_COLUMNS = {
142005
142021
  conversationTokens: "conversation_tokens",
142006
142022
  toolCallTokens: "tool_call_tokens",
142007
142023
  clearedReasoningThroughTag: "cleared_reasoning_through_tag",
142024
+ toolReclaimWatermark: "tool_reclaim_watermark",
142008
142025
  lastTodoState: "last_todo_state",
142009
142026
  cachedM0Bytes: "cached_m0_bytes",
142010
142027
  cachedM1Bytes: "cached_m1_bytes",
@@ -142073,7 +142090,7 @@ function isSessionMetaRow(row) {
142073
142090
  if (row === null || typeof row !== "object")
142074
142091
  return false;
142075
142092
  const r = row;
142076
- 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);
142093
+ 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);
142077
142094
  }
142078
142095
  function getDefaultSessionMeta(sessionId) {
142079
142096
  return {
@@ -142096,6 +142113,7 @@ function getDefaultSessionMeta(sessionId) {
142096
142113
  conversationTokens: 0,
142097
142114
  toolCallTokens: 0,
142098
142115
  clearedReasoningThroughTag: 0,
142116
+ toolReclaimWatermark: 0,
142099
142117
  lastTodoState: "",
142100
142118
  cachedM0Bytes: null,
142101
142119
  cachedM1Bytes: null,
@@ -142159,6 +142177,7 @@ function toSessionMeta(row) {
142159
142177
  conversationTokens: numOrZero(row.conversation_tokens),
142160
142178
  toolCallTokens: numOrZero(row.tool_call_tokens),
142161
142179
  clearedReasoningThroughTag: numOrZero(row.cleared_reasoning_through_tag),
142180
+ toolReclaimWatermark: numOrZero(row.tool_reclaim_watermark),
142162
142181
  lastTodoState: lastTodoStateRaw,
142163
142182
  cachedM0Bytes: toBufferOrNull(row.cached_m0_bytes),
142164
142183
  cachedM1Bytes: toBufferOrNull(row.cached_m1_bytes),
@@ -143598,6 +143617,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
143598
143617
  ensureColumn(db, "session_meta", "historian_last_failure_at", "INTEGER DEFAULT NULL");
143599
143618
  ensureColumn(db, "session_meta", "system_prompt_hash", "TEXT DEFAULT ''");
143600
143619
  ensureColumn(db, "session_meta", "cleared_reasoning_through_tag", "INTEGER DEFAULT 0");
143620
+ ensureColumn(db, "session_meta", "tool_reclaim_watermark", "INTEGER DEFAULT 0");
143601
143621
  ensureColumn(db, "session_meta", "stripped_placeholder_ids", "TEXT DEFAULT ''");
143602
143622
  ensureColumn(db, "session_meta", "stale_reduce_stripped_ids", "TEXT DEFAULT ''");
143603
143623
  ensureColumn(db, "session_meta", "processed_image_stripped_ids", "TEXT DEFAULT ''");
@@ -146217,6 +146237,7 @@ var SESSION_META_FALLBACK_SELECTS = {
146217
146237
  last_transform_error: "'' AS last_transform_error",
146218
146238
  system_prompt_hash: "'' AS system_prompt_hash",
146219
146239
  last_todo_state: "'' AS last_todo_state",
146240
+ tool_reclaim_watermark: "0 AS tool_reclaim_watermark",
146220
146241
  cached_m0_bytes: "NULL AS cached_m0_bytes",
146221
146242
  cached_m1_bytes: "NULL AS cached_m1_bytes",
146222
146243
  cached_m0_project_memory_epoch: "NULL AS cached_m0_project_memory_epoch",
@@ -146294,6 +146315,14 @@ function updateSessionMeta(db, sessionId, updates) {
146294
146315
  db.prepare(`UPDATE session_meta SET ${setClauses.join(", ")} WHERE session_id = ?`).run(...values, sessionId);
146295
146316
  })();
146296
146317
  }
146318
+ function advanceToolReclaimWatermark(db, sessionId, maxTagNumber) {
146319
+ if (maxTagNumber <= 0)
146320
+ return;
146321
+ db.transaction(() => {
146322
+ ensureSessionMetaRow(db, sessionId);
146323
+ db.prepare("UPDATE session_meta SET tool_reclaim_watermark = MAX(COALESCE(tool_reclaim_watermark, 0), ?) WHERE session_id = ?").run(maxTagNumber, sessionId);
146324
+ })();
146325
+ }
146297
146326
  // ../plugin/src/features/magic-context/storage-notes.ts
146298
146327
  var NOTE_TYPES = new Set(["session", "smart"]);
146299
146328
  var NOTE_STATUSES = new Set(["active", "pending", "ready", "dismissed"]);
@@ -146691,6 +146720,26 @@ function getActiveTagTokenAggregate(db, sessionId, protectedTags = 0) {
146691
146720
  nullCount: row?.null_count ?? 0
146692
146721
  };
146693
146722
  }
146723
+ function getOldestActiveUnprotectedToolTags(db, sessionId, protectedTags = 0, limit = 4) {
146724
+ if (limit <= 0)
146725
+ return [];
146726
+ const boundedLimit = Math.max(1, Math.min(10, Math.floor(limit)));
146727
+ const whereProtected = protectedTags > 0 ? `AND tag_number < (
146728
+ SELECT tag_number FROM tags
146729
+ WHERE session_id = ? AND status = 'active'
146730
+ ORDER BY tag_number DESC LIMIT 1 OFFSET ?
146731
+ )` : "";
146732
+ const params = protectedTags > 0 ? [sessionId, sessionId, protectedTags - 1, boundedLimit] : [sessionId, boundedLimit];
146733
+ const rows = db.prepare(`SELECT tag_number, tool_name
146734
+ FROM tags
146735
+ WHERE session_id = ? AND status = 'active' AND type = 'tool' ${whereProtected}
146736
+ ORDER BY tag_number ASC, id ASC
146737
+ LIMIT ?`).all(...params);
146738
+ return rows.filter((row) => typeof row.tag_number === "number").map((row) => ({
146739
+ tagNumber: row.tag_number,
146740
+ toolName: typeof row.tool_name === "string" ? row.tool_name : null
146741
+ }));
146742
+ }
146694
146743
  function getTriggerTagTokenUpperBound(db, sessionId) {
146695
146744
  const row = db.prepare(`SELECT
146696
146745
  COALESCE(SUM(COALESCE(token_count, 0) + COALESCE(input_token_count, 0) + COALESCE(reasoning_token_count, 0)), 0) AS bound,
@@ -147743,13 +147792,13 @@ async function maybeSendUpgradeReminder(deps, sessionId) {
147743
147792
  init_data_path();
147744
147793
  import * as fs2 from "node:fs";
147745
147794
  import * as path6 from "node:path";
147746
- var ANNOUNCEMENT_VERSION = "0.24.0";
147795
+ var ANNOUNCEMENT_VERSION = "0.25.0";
147747
147796
  var ANNOUNCEMENT_FEATURES = [
147748
- "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).",
147749
- "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.",
147750
- "Pi: fixed sessions overflowing the model context while still showing moderate usage Pi now sheds context before a tool-heavy turn overflows.",
147751
- "Fewer prompt-cache busts: doc edits, processed screenshots, and a rebuild-then-bust-again case no longer re-bill large prompt prefixes.",
147752
- "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)."
147797
+ "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.",
147798
+ "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.",
147799
+ "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.",
147800
+ "Pi: fixed /ctx-dream (was failing with 'Unknown named parameter') and local-embedding load failures on Windows/Desktop (#151, #128).",
147801
+ "Runaway background agents on weak/local models are now capped and force-stopped (#154, #152). Plus several prompt-cache busts removed."
147753
147802
  ];
147754
147803
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/F2uWxjGnU";
147755
147804
  var STATE_FILENAME = "last_announced_version";
@@ -148693,7 +148742,7 @@ function enqueueDream(db, projectIdentity, reason, force = false) {
148693
148742
  return db.transaction(() => {
148694
148743
  if (!hasActiveDreamLease(db)) {
148695
148744
  const staleThresholdMs = force ? 2 * 60 * 1000 : 120 * 60 * 1000;
148696
- db.prepare("DELETE FROM dream_queue WHERE project_path = ? AND started_at IS NOT NULL AND started_at < ?").run([projectIdentity, now - staleThresholdMs]);
148745
+ db.prepare("DELETE FROM dream_queue WHERE project_path = ? AND started_at IS NOT NULL AND started_at < ?").run(projectIdentity, now - staleThresholdMs);
148697
148746
  }
148698
148747
  const existing = db.prepare("SELECT id FROM dream_queue WHERE project_path = ?").get(projectIdentity);
148699
148748
  if (existing) {
@@ -148828,9 +148877,11 @@ async function promptWithTimeout(client, args, timeoutMs, signal) {
148828
148877
  });
148829
148878
  } catch (error) {
148830
148879
  if (signal?.aborted) {
148880
+ await abortChildRun(client, args.path.id);
148831
148881
  throw new Error("prompt aborted by external signal");
148832
148882
  }
148833
148883
  if (controller.signal.aborted) {
148884
+ await abortChildRun(client, args.path.id);
148834
148885
  throw new Error(`prompt timed out after ${timeoutMs}ms`);
148835
148886
  }
148836
148887
  throw error;
@@ -148839,6 +148890,13 @@ async function promptWithTimeout(client, args, timeoutMs, signal) {
148839
148890
  signal?.removeEventListener("abort", onExternalAbort);
148840
148891
  }
148841
148892
  }
148893
+ async function abortChildRun(client, sessionId) {
148894
+ try {
148895
+ await client.session.abort({ path: { id: sessionId } });
148896
+ } catch (error) {
148897
+ log(`[model-retry] child session abort failed for ${sessionId}: ${String(error)}`);
148898
+ }
148899
+ }
148842
148900
  function isNonRetryable(error, externalSignal) {
148843
148901
  if (externalSignal?.aborted)
148844
148902
  return true;
@@ -149695,6 +149753,20 @@ function getDistinctStoredModelIds(db, projectPath) {
149695
149753
  const rows = getDistinctStoredModelIdsStatement(db).all(projectPath);
149696
149754
  return new Set(rows.map((row) => typeof row.modelId === "string" ? row.modelId : null));
149697
149755
  }
149756
+ function getMemoryEmbedCoverage(db, projectPath, modelId) {
149757
+ const row = db.prepare(`SELECT
149758
+ COUNT(*) AS total,
149759
+ SUM(CASE WHEN EXISTS (
149760
+ SELECT 1 FROM memory_embeddings e
149761
+ WHERE e.memory_id = m.id AND e.model_id = ?
149762
+ ) THEN 1 ELSE 0 END) AS embedded
149763
+ FROM memories m
149764
+ WHERE m.project_path = ? AND m.status = 'active'`).get(modelId, projectPath);
149765
+ return {
149766
+ total: typeof row?.total === "number" ? row.total : 0,
149767
+ embedded: typeof row?.embedded === "number" ? row.embedded : 0
149768
+ };
149769
+ }
149698
149770
 
149699
149771
  // ../plugin/src/features/magic-context/memory/embedding-cache.ts
149700
149772
  var DEFAULT_EMBEDDING_CACHE_TTL_MS = 60000;
@@ -166736,6 +166808,28 @@ function countUnembeddedSessionCompartments(db, projectPath, sessionId, modelId)
166736
166808
  )`).get(projectPath, sessionId, projectPath, modelId);
166737
166809
  return typeof row?.n === "number" ? row.n : 0;
166738
166810
  }
166811
+ function countSessionCompartmentEmbedCoverage(db, projectPath, sessionId, modelId) {
166812
+ const row = db.prepare(`SELECT
166813
+ COUNT(*) AS total,
166814
+ SUM(CASE WHEN EXISTS (
166815
+ SELECT 1 FROM compartment_chunk_embeddings e
166816
+ WHERE e.compartment_id = c.id
166817
+ AND e.project_path = ?
166818
+ AND e.model_id = ?
166819
+ ) THEN 1 ELSE 0 END) AS embedded
166820
+ FROM compartments c
166821
+ JOIN session_projects sp
166822
+ ON sp.session_id = c.session_id
166823
+ AND sp.harness = c.harness
166824
+ AND sp.project_path = ?
166825
+ WHERE c.session_id = ?
166826
+ AND c.start_message IS NOT NULL
166827
+ AND c.end_message IS NOT NULL`).get(projectPath, modelId, projectPath, sessionId);
166828
+ return {
166829
+ total: typeof row?.total === "number" ? row.total : 0,
166830
+ embedded: typeof row?.embedded === "number" ? row.embedded : 0
166831
+ };
166832
+ }
166739
166833
 
166740
166834
  // ../plugin/src/features/magic-context/memory/cosine-similarity.ts
166741
166835
  function cosineSimilarity(a, b) {
@@ -166878,6 +166972,19 @@ async function withQuietConsole(fn) {
166878
166972
  console.error = origError;
166879
166973
  }
166880
166974
  }
166975
+ var nativeRuntimeMissing = false;
166976
+ function isNativeRuntimeMissingError(error51) {
166977
+ const message = error51 instanceof Error ? error51.message : String(error51 ?? "");
166978
+ const lower = message.toLowerCase();
166979
+ const code = error51?.code;
166980
+ const name2 = error51?.name;
166981
+ if (code === "ERR_DLOPEN_FAILED" && lower.includes("onnxruntime")) {
166982
+ return true;
166983
+ }
166984
+ if (!lower.includes("onnxruntime-node"))
166985
+ return false;
166986
+ return code === "ERR_MODULE_NOT_FOUND" || name2 === "ResolveMessage" || lower.includes("cannot find package") || lower.includes("cannot find module") || lower.includes("err_module_not_found");
166987
+ }
166881
166988
  function isTransientLoadError(error51) {
166882
166989
  const message = error51 instanceof Error ? error51.message : String(error51 ?? "");
166883
166990
  if (!message)
@@ -166942,6 +167049,9 @@ class LocalEmbeddingProvider {
166942
167049
  if (this.pipeline) {
166943
167050
  return true;
166944
167051
  }
167052
+ if (nativeRuntimeMissing) {
167053
+ return false;
167054
+ }
166945
167055
  if (this.initPromise) {
166946
167056
  await this.initPromise;
166947
167057
  return this.pipeline !== null;
@@ -167008,7 +167118,12 @@ class LocalEmbeddingProvider {
167008
167118
  await releaseLock();
167009
167119
  }
167010
167120
  } catch (error51) {
167011
- log("[magic-context] embedding model failed to load:", error51);
167121
+ if (isNativeRuntimeMissingError(error51)) {
167122
+ nativeRuntimeMissing = true;
167123
+ log("[magic-context] local embedding runtime is not installed (onnxruntime-node missing from this install). Local embeddings are disabled. Fix: reinstall the plugin (run `npx @wolfx/magic-context@latest doctor --force`), or configure an `openai-compatible`/`ollama` embedding endpoint instead. Existing memories are unaffected.");
167124
+ } else {
167125
+ log("[magic-context] embedding model failed to load:", error51);
167126
+ }
167012
167127
  this.pipeline = null;
167013
167128
  } finally {
167014
167129
  this.initPromise = null;
@@ -167522,6 +167637,118 @@ function getDistinctCommitEmbeddingModelIds(db, projectPath) {
167522
167637
  return new Set(rows.map((row) => typeof row.modelId === "string" ? row.modelId : null));
167523
167638
  }
167524
167639
 
167640
+ // ../plugin/src/features/magic-context/git-commits/storage-git-commits.ts
167641
+ init_logger();
167642
+ var insertStatements = new WeakMap;
167643
+ var existingShasStatements = new WeakMap;
167644
+ var projectCountStatements = new WeakMap;
167645
+ var evictStatements = new WeakMap;
167646
+ var evictOverflowStatements = new WeakMap;
167647
+ var latestCommitTimeStatements = new WeakMap;
167648
+ function getInsertStatement(db) {
167649
+ let stmt = insertStatements.get(db);
167650
+ if (!stmt) {
167651
+ stmt = db.prepare(`INSERT INTO git_commits (sha, project_path, short_sha, message, author, committed_at, indexed_at)
167652
+ VALUES (?, ?, ?, ?, ?, ?, ?)
167653
+ ON CONFLICT(sha) DO UPDATE SET
167654
+ project_path = excluded.project_path,
167655
+ short_sha = excluded.short_sha,
167656
+ message = excluded.message,
167657
+ author = excluded.author,
167658
+ committed_at = excluded.committed_at,
167659
+ indexed_at = excluded.indexed_at
167660
+ WHERE git_commits.message != excluded.message`);
167661
+ insertStatements.set(db, stmt);
167662
+ }
167663
+ return stmt;
167664
+ }
167665
+ function getExistingShasStatement(db) {
167666
+ let stmt = existingShasStatements.get(db);
167667
+ if (!stmt) {
167668
+ stmt = db.prepare("SELECT sha FROM git_commits WHERE project_path = ?");
167669
+ existingShasStatements.set(db, stmt);
167670
+ }
167671
+ return stmt;
167672
+ }
167673
+ function getProjectCountStatement(db) {
167674
+ let stmt = projectCountStatements.get(db);
167675
+ if (!stmt) {
167676
+ stmt = db.prepare("SELECT COUNT(*) AS count FROM git_commits WHERE project_path = ?");
167677
+ projectCountStatements.set(db, stmt);
167678
+ }
167679
+ return stmt;
167680
+ }
167681
+ function getLatestCommitTimeStatement(db) {
167682
+ let stmt = latestCommitTimeStatements.get(db);
167683
+ if (!stmt) {
167684
+ stmt = db.prepare("SELECT MAX(committed_at) AS latest FROM git_commits WHERE project_path = ?");
167685
+ latestCommitTimeStatements.set(db, stmt);
167686
+ }
167687
+ return stmt;
167688
+ }
167689
+ function getEvictOverflowStatement(db) {
167690
+ let stmt = evictOverflowStatements.get(db);
167691
+ if (!stmt) {
167692
+ stmt = db.prepare(`DELETE FROM git_commits
167693
+ WHERE rowid IN (
167694
+ SELECT rowid FROM git_commits
167695
+ WHERE project_path = ?
167696
+ ORDER BY committed_at DESC, sha DESC
167697
+ LIMIT -1 OFFSET ?
167698
+ )`);
167699
+ evictOverflowStatements.set(db, stmt);
167700
+ }
167701
+ return stmt;
167702
+ }
167703
+ function upsertCommits(db, projectPath, commits) {
167704
+ if (commits.length === 0)
167705
+ return { inserted: 0, updated: 0 };
167706
+ const existing = new Set;
167707
+ for (const row of getExistingShasStatement(db).all(projectPath)) {
167708
+ existing.add(row.sha);
167709
+ }
167710
+ let inserted = 0;
167711
+ let updated = 0;
167712
+ const now = Date.now();
167713
+ const insertStmt = getInsertStatement(db);
167714
+ db.transaction(() => {
167715
+ for (const commit of commits) {
167716
+ const result = insertStmt.run(commit.sha, projectPath, commit.shortSha, commit.message, commit.author, commit.committedAtMs, now);
167717
+ if (result.changes > 0) {
167718
+ if (existing.has(commit.sha)) {
167719
+ updated++;
167720
+ } else {
167721
+ inserted++;
167722
+ existing.add(commit.sha);
167723
+ }
167724
+ }
167725
+ }
167726
+ })();
167727
+ return { inserted, updated };
167728
+ }
167729
+ function getCommitCount(db, projectPath) {
167730
+ const row = getProjectCountStatement(db).get(projectPath);
167731
+ return row?.count ?? 0;
167732
+ }
167733
+ function getLatestIndexedCommitTimeMs(db, projectPath) {
167734
+ const row = getLatestCommitTimeStatement(db).get(projectPath);
167735
+ return row?.latest ?? null;
167736
+ }
167737
+ function enforceProjectCap(db, projectPath, maxCommits) {
167738
+ if (maxCommits <= 0)
167739
+ return 0;
167740
+ const count = getCommitCount(db, projectPath);
167741
+ if (count <= maxCommits)
167742
+ return 0;
167743
+ getEvictOverflowStatement(db).run(projectPath, maxCommits);
167744
+ const after = getCommitCount(db, projectPath);
167745
+ const evicted = Math.max(0, count - after);
167746
+ if (evicted > 0) {
167747
+ log(`[git-commits] evicted ${evicted} oldest commits for project ${projectPath} (cap=${maxCommits}, was=${count})`);
167748
+ }
167749
+ return evicted;
167750
+ }
167751
+
167525
167752
  // ../plugin/src/features/magic-context/git-commits/sweep-coordinator.ts
167526
167753
  var GIT_SWEEP_COOLDOWN_MS = 10 * 60 * 1000;
167527
167754
  var GIT_SWEEP_LEASE_TTL_MS = 5 * 60 * 1000;
@@ -167716,8 +167943,12 @@ function repairMisScopedCompartmentChunkEmbeddingsForProject(db, projectPath) {
167716
167943
  var OFF_PROVIDER_IDENTITY = "embedding-provider:off";
167717
167944
  var SWEEP_MAX_WALL_CLOCK_MS = 10 * 60 * 1000;
167718
167945
  var CHUNK_DRAIN_BATCH_SIZE = 8;
167719
- var MAX_WINDOWS_PER_EMBED_CALL = 16;
167946
+ var MAX_WINDOWS_PER_EMBED_CALL = 2;
167720
167947
  var SESSION_EMBED_LEASE_RENEWAL_MS = 60 * 1000;
167948
+ var EMBED_SLICE_RETRY_ATTEMPTS = 3;
167949
+ var EMBED_SLICE_RETRY_BASE_MS = 250;
167950
+ var EMBED_SLOW_FAILURE_NO_RETRY_MS = 1e4;
167951
+ var MAX_CONSECUTIVE_FAILED_BATCHES = 3;
167721
167952
  var projectRegistrations = new Map;
167722
167953
  var loadUnembeddedMemoriesStatements = new WeakMap;
167723
167954
  var globalRegistrationGeneration = 0;
@@ -167817,7 +168048,9 @@ function snapshotFor(registration) {
167817
168048
  enabled,
167818
168049
  gitCommitEnabled,
167819
168050
  modelId: registration.observationMode || !providerIsOn ? "off" : registration.modelId,
167820
- chunkModelId: registration.observationMode || !providerIsOn ? "off" : registration.chunkModelId
168051
+ chunkModelId: registration.observationMode || !providerIsOn ? "off" : registration.chunkModelId,
168052
+ model: registration.observationMode || !providerIsOn ? "off" : ("model" in registration.config) && registration.config.model.trim() ? registration.config.model.trim() : registration.modelId,
168053
+ provider: registration.observationMode || !providerIsOn ? "off" : registration.config.provider ?? "local"
167821
168054
  };
167822
168055
  }
167823
168056
  function disposeProvider(provider) {
@@ -168028,8 +168261,9 @@ async function embedUnembeddedMemoriesForProject(db, projectIdentity, batchSize
168028
168261
  }
168029
168262
  async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates, signal) {
168030
168263
  const noWork = [];
168264
+ const failed = [];
168031
168265
  if (candidates.length === 0)
168032
- return { embedded: 0, noWork };
168266
+ return { embedded: 0, noWork, failed };
168033
168267
  const maxInputTokens = getProjectEmbeddingMaxInputTokens(projectIdentity);
168034
168268
  const prepared = [];
168035
168269
  for (const candidate of candidates) {
@@ -168046,7 +168280,7 @@ async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates
168046
168280
  prepared.push({ candidate, windows });
168047
168281
  }
168048
168282
  if (prepared.length === 0)
168049
- return { embedded: 0, noWork };
168283
+ return { embedded: 0, noWork, failed };
168050
168284
  let embedded = 0;
168051
168285
  let i = 0;
168052
168286
  while (i < prepared.length) {
@@ -168063,35 +168297,60 @@ async function embedCandidateChunkBatch(db, projectIdentity, modelId, candidates
168063
168297
  const texts = [];
168064
168298
  for (const item of slice)
168065
168299
  texts.push(...item.windows.map((w) => w.text));
168066
- try {
168067
- const result = await embedBatchForProject(projectIdentity, texts, signal);
168068
- if (!result)
168069
- continue;
168300
+ const persistedIds = new Set;
168301
+ for (let attempt = 0;attempt < EMBED_SLICE_RETRY_ATTEMPTS; attempt++) {
168070
168302
  if (signal?.aborted)
168071
168303
  break;
168072
- let offset = 0;
168073
- for (const item of slice) {
168074
- const vectors = result.vectors.slice(offset, offset + item.windows.length);
168075
- offset += item.windows.length;
168076
- if (vectors.length !== item.windows.length || vectors.some((v) => !v)) {
168077
- continue;
168304
+ let result = null;
168305
+ const attemptStart = Date.now();
168306
+ try {
168307
+ result = await embedBatchForProject(projectIdentity, texts, signal);
168308
+ } catch (error51) {
168309
+ log("[magic-context] failed to proactively embed compartment chunks:", error51);
168310
+ }
168311
+ if (signal?.aborted)
168312
+ break;
168313
+ if (result) {
168314
+ let offset = 0;
168315
+ for (const item of slice) {
168316
+ const vectors = result.vectors.slice(offset, offset + item.windows.length);
168317
+ offset += item.windows.length;
168318
+ if (persistedIds.has(item.candidate.id))
168319
+ continue;
168320
+ if (vectors.length !== item.windows.length || vectors.some((v) => !v)) {
168321
+ continue;
168322
+ }
168323
+ const rows = item.windows.map((window, index) => ({
168324
+ compartmentId: item.candidate.id,
168325
+ sessionId: item.candidate.sessionId,
168326
+ projectPath: projectIdentity,
168327
+ window,
168328
+ modelId,
168329
+ vector: vectors[index]
168330
+ }));
168331
+ replaceCompartmentChunkEmbeddings(db, rows);
168332
+ persistedIds.add(item.candidate.id);
168078
168333
  }
168079
- const rows = item.windows.map((window, index) => ({
168080
- compartmentId: item.candidate.id,
168081
- sessionId: item.candidate.sessionId,
168082
- projectPath: projectIdentity,
168083
- window,
168084
- modelId,
168085
- vector: vectors[index]
168086
- }));
168087
- replaceCompartmentChunkEmbeddings(db, rows);
168088
- embedded += 1;
168089
168334
  }
168090
- } catch (error51) {
168091
- log("[magic-context] failed to proactively embed compartment chunks:", error51);
168335
+ if (persistedIds.size === slice.length)
168336
+ break;
168337
+ if (persistedIds.size > 0)
168338
+ break;
168339
+ if (Date.now() - attemptStart >= EMBED_SLOW_FAILURE_NO_RETRY_MS)
168340
+ break;
168341
+ if (attempt < EMBED_SLICE_RETRY_ATTEMPTS - 1) {
168342
+ await new Promise((resolve4) => setTimeout(resolve4, EMBED_SLICE_RETRY_BASE_MS * 2 ** attempt));
168343
+ }
168344
+ }
168345
+ embedded += persistedIds.size;
168346
+ if (!signal?.aborted) {
168347
+ for (const item of slice) {
168348
+ if (!persistedIds.has(item.candidate.id))
168349
+ failed.push(item.candidate.id);
168350
+ }
168092
168351
  }
168093
168352
  }
168094
- return { embedded, noWork };
168353
+ return { embedded, noWork, failed };
168095
168354
  }
168096
168355
  async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, options) {
168097
168356
  const snapshot = getProjectEmbeddingSnapshot(projectIdentity);
@@ -168114,9 +168373,11 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
168114
168373
  renewal.unref?.();
168115
168374
  const batchSize = Math.max(1, options?.batchSize ?? CHUNK_DRAIN_BATCH_SIZE);
168116
168375
  const skipIds = [];
168376
+ const failedIds = [];
168117
168377
  let embedded = 0;
168118
168378
  let aborted2 = false;
168119
- let providerStalled = false;
168379
+ let providerDown = false;
168380
+ let consecutiveFailedBatches = 0;
168120
168381
  try {
168121
168382
  options?.onProgress?.({ embedded, total });
168122
168383
  for (;; ) {
@@ -168124,15 +168385,26 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
168124
168385
  aborted2 = true;
168125
168386
  break;
168126
168387
  }
168127
- const candidates = loadUnembeddedSessionChunkCandidates(db, projectIdentity, sessionId, snapshot.chunkModelId, batchSize, skipIds);
168388
+ const candidates = loadUnembeddedSessionChunkCandidates(db, projectIdentity, sessionId, snapshot.chunkModelId, batchSize, [...skipIds, ...failedIds]);
168128
168389
  if (candidates.length === 0)
168129
168390
  break;
168130
- const { embedded: n, noWork } = await embedCandidateChunkBatch(db, projectIdentity, snapshot.chunkModelId, candidates, options?.signal);
168391
+ const {
168392
+ embedded: n,
168393
+ noWork,
168394
+ failed
168395
+ } = await embedCandidateChunkBatch(db, projectIdentity, snapshot.chunkModelId, candidates, options?.signal);
168131
168396
  for (const id of noWork)
168132
168397
  skipIds.push(id);
168398
+ for (const id of failed)
168399
+ failedIds.push(id);
168133
168400
  if (n === 0 && noWork.length === 0) {
168134
- providerStalled = true;
168135
- break;
168401
+ consecutiveFailedBatches += 1;
168402
+ if (consecutiveFailedBatches >= MAX_CONSECUTIVE_FAILED_BATCHES) {
168403
+ providerDown = true;
168404
+ break;
168405
+ }
168406
+ } else {
168407
+ consecutiveFailedBatches = 0;
168136
168408
  }
168137
168409
  embedded += n;
168138
168410
  options?.onProgress?.({ embedded: Math.min(embedded, total), total });
@@ -168140,16 +168412,50 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
168140
168412
  }
168141
168413
  } finally {
168142
168414
  clearInterval(renewal);
168143
- releaseGitSweepLease(db, projectIdentity, holderId);
168415
+ try {
168416
+ releaseGitSweepLease(db, projectIdentity, holderId);
168417
+ } catch (error51) {
168418
+ log("[magic-context] embed drain: lease release failed (will TTL-expire):", error51);
168419
+ }
168144
168420
  }
168145
168421
  if (aborted2)
168146
- return { status: "aborted", embedded, total };
168147
- if (providerStalled) {
168422
+ return { status: "aborted", embedded, total, failed: failedIds.length };
168423
+ if (providerDown || failedIds.length > 0) {
168148
168424
  const remaining = Math.max(0, countUnembeddedSessionCompartments(db, projectIdentity, sessionId, snapshot.chunkModelId) - skipIds.length);
168149
- if (remaining > 0)
168150
- return { status: "stalled", embedded, total, remaining };
168425
+ if (remaining > 0) {
168426
+ return { status: "stalled", embedded, total, remaining, failed: failedIds.length };
168427
+ }
168151
168428
  }
168152
- return { status: "done", embedded, total };
168429
+ return { status: "done", embedded, total, failed: failedIds.length };
168430
+ }
168431
+ function getEmbeddingCoverageStatus(db, projectIdentity, sessionId) {
168432
+ const snapshot = getProjectEmbeddingSnapshot(projectIdentity);
168433
+ if (!snapshot?.enabled || snapshot.chunkModelId === "off") {
168434
+ return {
168435
+ enabled: false,
168436
+ model: snapshot?.model ?? "off",
168437
+ provider: snapshot?.provider ?? "off",
168438
+ session: { embedded: 0, total: 0 },
168439
+ memories: { embedded: 0, total: 0 },
168440
+ commits: { embedded: 0, total: 0, gitEnabled: false }
168441
+ };
168442
+ }
168443
+ const session = countSessionCompartmentEmbedCoverage(db, projectIdentity, sessionId, snapshot.chunkModelId);
168444
+ const memories = getMemoryEmbedCoverage(db, projectIdentity, snapshot.modelId);
168445
+ const gitEnabled = snapshot.gitCommitEnabled;
168446
+ const commits = gitEnabled ? {
168447
+ embedded: countEmbeddedCommits(db, projectIdentity),
168448
+ total: getCommitCount(db, projectIdentity),
168449
+ gitEnabled: true
168450
+ } : { embedded: 0, total: 0, gitEnabled: false };
168451
+ return {
168452
+ enabled: true,
168453
+ model: snapshot.model,
168454
+ provider: snapshot.provider,
168455
+ session,
168456
+ memories,
168457
+ commits
168458
+ };
168153
168459
  }
168154
168460
 
168155
168461
  // ../plugin/src/features/magic-context/memory/embedding.ts
@@ -168198,118 +168504,6 @@ async function embedText(text, signal) {
168198
168504
  }
168199
168505
  var SWEEP_MAX_WALL_CLOCK_MS2 = 10 * 60 * 1000;
168200
168506
 
168201
- // ../plugin/src/features/magic-context/git-commits/storage-git-commits.ts
168202
- init_logger();
168203
- var insertStatements = new WeakMap;
168204
- var existingShasStatements = new WeakMap;
168205
- var projectCountStatements = new WeakMap;
168206
- var evictStatements = new WeakMap;
168207
- var evictOverflowStatements = new WeakMap;
168208
- var latestCommitTimeStatements = new WeakMap;
168209
- function getInsertStatement(db) {
168210
- let stmt = insertStatements.get(db);
168211
- if (!stmt) {
168212
- stmt = db.prepare(`INSERT INTO git_commits (sha, project_path, short_sha, message, author, committed_at, indexed_at)
168213
- VALUES (?, ?, ?, ?, ?, ?, ?)
168214
- ON CONFLICT(sha) DO UPDATE SET
168215
- project_path = excluded.project_path,
168216
- short_sha = excluded.short_sha,
168217
- message = excluded.message,
168218
- author = excluded.author,
168219
- committed_at = excluded.committed_at,
168220
- indexed_at = excluded.indexed_at
168221
- WHERE git_commits.message != excluded.message`);
168222
- insertStatements.set(db, stmt);
168223
- }
168224
- return stmt;
168225
- }
168226
- function getExistingShasStatement(db) {
168227
- let stmt = existingShasStatements.get(db);
168228
- if (!stmt) {
168229
- stmt = db.prepare("SELECT sha FROM git_commits WHERE project_path = ?");
168230
- existingShasStatements.set(db, stmt);
168231
- }
168232
- return stmt;
168233
- }
168234
- function getProjectCountStatement(db) {
168235
- let stmt = projectCountStatements.get(db);
168236
- if (!stmt) {
168237
- stmt = db.prepare("SELECT COUNT(*) AS count FROM git_commits WHERE project_path = ?");
168238
- projectCountStatements.set(db, stmt);
168239
- }
168240
- return stmt;
168241
- }
168242
- function getLatestCommitTimeStatement(db) {
168243
- let stmt = latestCommitTimeStatements.get(db);
168244
- if (!stmt) {
168245
- stmt = db.prepare("SELECT MAX(committed_at) AS latest FROM git_commits WHERE project_path = ?");
168246
- latestCommitTimeStatements.set(db, stmt);
168247
- }
168248
- return stmt;
168249
- }
168250
- function getEvictOverflowStatement(db) {
168251
- let stmt = evictOverflowStatements.get(db);
168252
- if (!stmt) {
168253
- stmt = db.prepare(`DELETE FROM git_commits
168254
- WHERE rowid IN (
168255
- SELECT rowid FROM git_commits
168256
- WHERE project_path = ?
168257
- ORDER BY committed_at DESC, sha DESC
168258
- LIMIT -1 OFFSET ?
168259
- )`);
168260
- evictOverflowStatements.set(db, stmt);
168261
- }
168262
- return stmt;
168263
- }
168264
- function upsertCommits(db, projectPath, commits) {
168265
- if (commits.length === 0)
168266
- return { inserted: 0, updated: 0 };
168267
- const existing = new Set;
168268
- for (const row of getExistingShasStatement(db).all(projectPath)) {
168269
- existing.add(row.sha);
168270
- }
168271
- let inserted = 0;
168272
- let updated = 0;
168273
- const now = Date.now();
168274
- const insertStmt = getInsertStatement(db);
168275
- db.transaction(() => {
168276
- for (const commit of commits) {
168277
- const result = insertStmt.run(commit.sha, projectPath, commit.shortSha, commit.message, commit.author, commit.committedAtMs, now);
168278
- if (result.changes > 0) {
168279
- if (existing.has(commit.sha)) {
168280
- updated++;
168281
- } else {
168282
- inserted++;
168283
- existing.add(commit.sha);
168284
- }
168285
- }
168286
- }
168287
- })();
168288
- return { inserted, updated };
168289
- }
168290
- function getCommitCount(db, projectPath) {
168291
- const row = getProjectCountStatement(db).get(projectPath);
168292
- return row?.count ?? 0;
168293
- }
168294
- function getLatestIndexedCommitTimeMs(db, projectPath) {
168295
- const row = getLatestCommitTimeStatement(db).get(projectPath);
168296
- return row?.latest ?? null;
168297
- }
168298
- function enforceProjectCap(db, projectPath, maxCommits) {
168299
- if (maxCommits <= 0)
168300
- return 0;
168301
- const count = getCommitCount(db, projectPath);
168302
- if (count <= maxCommits)
168303
- return 0;
168304
- getEvictOverflowStatement(db).run(projectPath, maxCommits);
168305
- const after = getCommitCount(db, projectPath);
168306
- const evicted = Math.max(0, count - after);
168307
- if (evicted > 0) {
168308
- log(`[git-commits] evicted ${evicted} oldest commits for project ${projectPath} (cap=${maxCommits}, was=${count})`);
168309
- }
168310
- return evicted;
168311
- }
168312
-
168313
168507
  // ../plugin/src/features/magic-context/git-commits/indexer.ts
168314
168508
  var MS_PER_DAY = 24 * 60 * 60 * 1000;
168315
168509
  var EMBED_BATCH_SIZE = 16;
@@ -169843,87 +170037,229 @@ function registerCtxDreamCommand(pi, deps) {
169843
170037
  });
169844
170038
  }
169845
170039
 
169846
- // src/commands/ctx-embed-history.ts
169847
- function registerCtxEmbedHistoryCommand(pi, deps) {
169848
- pi.registerCommand("ctx-embed-history", {
169849
- description: "Embed all of this session's history compartments for semantic search, in one pass",
169850
- handler: async (_args, ctx) => {
170040
+ // ../plugin/src/hooks/magic-context/embed-session-state.ts
170041
+ var embedPauseBySession = new Set;
170042
+ var embedRunStateBySession = new Map;
170043
+ var autoEmbedAttemptedBySession = new Set;
170044
+
170045
+ // ../plugin/src/hooks/magic-context/format-embed-status.ts
170046
+ function formatEmbedStatusText(coverage, drain) {
170047
+ if (!coverage.enabled) {
170048
+ return "Embedding is off (no provider configured).";
170049
+ }
170050
+ const lines = [];
170051
+ lines.push(`Embedding — model: ${coverage.model} (${coverage.provider})`);
170052
+ lines.push(`This session: ${coverage.session.embedded} / ${coverage.session.total} compartments embedded`);
170053
+ lines.push(`Project memories: ${coverage.memories.embedded} / ${coverage.memories.total} embedded`);
170054
+ if (coverage.commits.gitEnabled) {
170055
+ lines.push(`Git commits: ${coverage.commits.embedded} / ${coverage.commits.total}`);
170056
+ } else {
170057
+ lines.push("Git commits: 0 / 0 (git indexing off)");
170058
+ }
170059
+ let drainLine = "Drain: idle";
170060
+ switch (drain.status) {
170061
+ case "running": {
170062
+ const e = drain.embedded ?? coverage.session.embedded;
170063
+ const t = drain.total ?? coverage.session.total;
170064
+ const failedSuffix = drain.failed && drain.failed > 0 ? ` (${drain.failed} failed)` : "";
170065
+ drainLine = `Drain: running ${e}/${t}${failedSuffix}`;
170066
+ break;
170067
+ }
170068
+ case "paused": {
170069
+ const e = drain.embedded ?? coverage.session.embedded;
170070
+ const t = drain.total ?? coverage.session.total;
170071
+ drainLine = `Drain: paused ${e}/${t}`;
170072
+ break;
170073
+ }
170074
+ case "stopped":
170075
+ drainLine = "Drain: stopped (provider down)";
170076
+ break;
170077
+ default:
170078
+ drainLine = "Drain: idle";
170079
+ }
170080
+ lines.push(drainLine);
170081
+ return lines.join(`
170082
+ `);
170083
+ }
170084
+
170085
+ // src/commands/ctx-embed.ts
170086
+ function clearPiEmbedSessionState(sessionId) {
170087
+ embedPauseBySession.delete(sessionId);
170088
+ const ctrl = embedRunStateBySession.get(sessionId);
170089
+ if (ctrl) {
170090
+ ctrl.abort();
170091
+ embedRunStateBySession.delete(sessionId);
170092
+ }
170093
+ autoEmbedAttemptedBySession.delete(sessionId);
170094
+ }
170095
+ async function runEmbedDrain(db, projectIdentity, sessionId) {
170096
+ const activeCtrl = embedRunStateBySession.get(sessionId);
170097
+ if (activeCtrl && !activeCtrl.signal.aborted) {
170098
+ return {
170099
+ text: `## /ctx-embed
170100
+
170101
+ Embedding is already running for this session.`,
170102
+ level: "info"
170103
+ };
170104
+ }
170105
+ embedPauseBySession.delete(sessionId);
170106
+ const prior = embedRunStateBySession.get(sessionId);
170107
+ if (prior)
170108
+ prior.abort();
170109
+ const controller = new AbortController;
170110
+ embedRunStateBySession.set(sessionId, controller);
170111
+ let outcome;
170112
+ try {
170113
+ outcome = await embedSessionCompartmentChunks(db, projectIdentity, sessionId, {
170114
+ signal: controller.signal
170115
+ });
170116
+ } finally {
170117
+ if (embedRunStateBySession.get(sessionId) === controller) {
170118
+ embedRunStateBySession.delete(sessionId);
170119
+ }
170120
+ }
170121
+ switch (outcome.status) {
170122
+ case "nothing":
170123
+ return {
170124
+ text: `## /ctx-embed
170125
+
170126
+ All of this session's history is already embedded.`,
170127
+ level: "info"
170128
+ };
170129
+ case "disabled":
170130
+ return {
170131
+ text: `## /ctx-embed
170132
+
170133
+ No embedding provider is configured, so there is nothing to embed.`,
170134
+ level: "info"
170135
+ };
170136
+ case "busy":
170137
+ return {
170138
+ text: `## /ctx-embed
170139
+
170140
+ Embedding is already running for this project. Try again shortly.`,
170141
+ level: "info"
170142
+ };
170143
+ case "aborted": {
170144
+ const cov = getEmbeddingCoverageStatus(db, projectIdentity, sessionId);
170145
+ return {
170146
+ text: `## /ctx-embed
170147
+
170148
+ Paused at ${cov.session.embedded}/${cov.session.total} compartments embedded.`,
170149
+ level: "info"
170150
+ };
170151
+ }
170152
+ case "stalled":
170153
+ return {
170154
+ text: `## /ctx-embed
170155
+
170156
+ Embedded ${outcome.embedded} compartment${outcome.embedded === 1 ? "" : "s"}; ${outcome.remaining} could not be embedded (the provider returned no result). Run /ctx-embed start again to retry them.`,
170157
+ level: "info"
170158
+ };
170159
+ default:
170160
+ return {
170161
+ text: `## /ctx-embed
170162
+
170163
+ Embedded ${outcome.embedded} compartment${outcome.embedded === 1 ? "" : "s"} of history for semantic search.`,
170164
+ level: "success"
170165
+ };
170166
+ }
170167
+ }
170168
+ function registerCtxEmbedCommand(pi, deps) {
170169
+ pi.registerCommand("ctx-embed", {
170170
+ description: "Embedding status, or start/pause history compartment embedding (start | pause)",
170171
+ handler: async (args, ctx) => {
169851
170172
  const sessionId = resolveSessionId(ctx);
169852
170173
  if (!sessionId) {
169853
170174
  sendCtxStatusMessage(pi, {
169854
- title: "/ctx-embed-history",
169855
- text: `## /ctx-embed-history
170175
+ title: "/ctx-embed",
170176
+ text: `## /ctx-embed
169856
170177
 
169857
170178
  No active Pi session is available.`,
169858
170179
  level: "error"
169859
170180
  });
169860
170181
  return;
169861
170182
  }
170183
+ const project = deps.resolveProject?.(ctx) ?? {
170184
+ projectDir: deps.projectDir,
170185
+ projectIdentity: deps.projectIdentity
170186
+ };
170187
+ const sub = args.trim().toLowerCase();
170188
+ if (sub === "pause") {
170189
+ embedPauseBySession.add(sessionId);
170190
+ const ctrl = embedRunStateBySession.get(sessionId);
170191
+ if (ctrl)
170192
+ ctrl.abort();
170193
+ const cov = getEmbeddingCoverageStatus(deps.db, project.projectIdentity, sessionId);
170194
+ sendCtxStatusMessage(pi, {
170195
+ title: "/ctx-embed",
170196
+ text: `## /ctx-embed
170197
+
170198
+ Paused at ${cov.session.embedded}/${cov.session.total} compartments embedded.`,
170199
+ level: "info"
170200
+ });
170201
+ return;
170202
+ }
169862
170203
  if (deps.memoryEnabled === false) {
169863
170204
  sendCtxStatusMessage(pi, {
169864
- title: "/ctx-embed-history",
169865
- text: `## /ctx-embed-history
170205
+ title: "/ctx-embed",
170206
+ text: `## /ctx-embed
169866
170207
 
169867
170208
  Memory is disabled for this project, so there is no semantic embedding to backfill.`,
169868
170209
  level: "info"
169869
170210
  });
169870
170211
  return;
169871
170212
  }
169872
- const project = deps.resolveProject?.(ctx) ?? {
169873
- projectDir: deps.projectDir,
169874
- projectIdentity: deps.projectIdentity
169875
- };
169876
170213
  await ensureProjectRegisteredFromPiDirectory(project.projectDir, deps.db);
169877
- const outcome = await embedSessionCompartmentChunks(deps.db, project.projectIdentity, sessionId);
169878
- const { text, level } = (() => {
169879
- switch (outcome.status) {
169880
- case "nothing":
169881
- return {
169882
- text: `## /ctx-embed-history
169883
-
169884
- All of this session's history is already embedded.`,
169885
- level: "info"
169886
- };
169887
- case "disabled":
169888
- return {
169889
- text: `## /ctx-embed-history
169890
-
169891
- No embedding provider is configured, so there is nothing to embed.`,
169892
- level: "info"
169893
- };
169894
- case "busy":
169895
- return {
169896
- text: `## /ctx-embed-history
169897
-
169898
- Embedding is already running for this project — ${outcome.total} compartment${outcome.total === 1 ? "" : "s"} still pending. Try again shortly.`,
169899
- level: "info"
169900
- };
169901
- case "stalled":
169902
- return {
169903
- text: `## /ctx-embed-history
169904
-
169905
- Embedded ${outcome.embedded} compartment${outcome.embedded === 1 ? "" : "s"}; ${outcome.remaining} could not be embedded (the provider returned no result). Run /ctx-embed-history again to retry them.`,
169906
- level: "info"
169907
- };
169908
- default:
169909
- return {
169910
- text: `## /ctx-embed-history
170214
+ if (sub === "start") {
170215
+ const { text, level } = await runEmbedDrain(deps.db, project.projectIdentity, sessionId);
170216
+ sendCtxStatusMessage(pi, { title: "/ctx-embed", text, level });
170217
+ return;
170218
+ }
170219
+ if (sub !== "") {
170220
+ sendCtxStatusMessage(pi, {
170221
+ title: "/ctx-embed",
170222
+ text: "## /ctx-embed\n\nUsage: `/ctx-embed` (status), `/ctx-embed start`, or `/ctx-embed pause`.",
170223
+ level: "info"
170224
+ });
170225
+ return;
170226
+ }
170227
+ const coverage = getEmbeddingCoverageStatus(deps.db, project.projectIdentity, sessionId);
170228
+ const statusText = formatEmbedStatusText(coverage, { status: "idle" });
170229
+ sendCtxStatusMessage(pi, {
170230
+ title: "/ctx-embed",
170231
+ text: `## Embedding Status
169911
170232
 
169912
- Embedded ${outcome.embedded} compartment${outcome.embedded === 1 ? "" : "s"} of history for semantic search.`,
169913
- level: "success"
169914
- };
169915
- }
169916
- })();
169917
- sendCtxStatusMessage(pi, { title: "/ctx-embed-history", text, level }, {
169918
- sessionId,
169919
- projectIdentity: project.projectIdentity,
169920
- status: outcome.status,
169921
- embedded: outcome.embedded,
169922
- total: outcome.total
170233
+ ${statusText}`,
170234
+ level: "info"
169923
170235
  });
169924
170236
  }
169925
170237
  });
169926
170238
  }
170239
+ function maybeAutoEmbedPiSession(deps, sessionId, projectDir, projectIdentity, notify) {
170240
+ if (autoEmbedAttemptedBySession.has(sessionId))
170241
+ return;
170242
+ if (embedPauseBySession.has(sessionId))
170243
+ return;
170244
+ if (deps.memoryEnabled === false)
170245
+ return;
170246
+ autoEmbedAttemptedBySession.add(sessionId);
170247
+ (async () => {
170248
+ try {
170249
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
170250
+ await ensureProjectRegisteredFromPiDirectory(projectDir, deps.db);
170251
+ const coverage = getEmbeddingCoverageStatus(deps.db, projectIdentity, sessionId);
170252
+ if (!coverage.enabled)
170253
+ return;
170254
+ const remaining = coverage.session.total - coverage.session.embedded;
170255
+ if (remaining <= 0)
170256
+ return;
170257
+ notify(`Embedding ${remaining} compartment${remaining === 1 ? "" : "s"} of history in the background…`);
170258
+ const { text } = await runEmbedDrain(deps.db, projectIdentity, sessionId);
170259
+ notify(text.replace(/^## \/ctx-embed\n\n/, ""));
170260
+ } catch {}
170261
+ })();
170262
+ }
169927
170263
 
169928
170264
  // ../plugin/src/hooks/magic-context/execute-flush.ts
169929
170265
  init_logger();
@@ -170338,76 +170674,12 @@ function computePiWorkMetrics(sessionEntries) {
170338
170674
  return { newWorkTokens, totalInputTokens };
170339
170675
  }
170340
170676
 
170341
- // ../plugin/src/hooks/magic-context/system-injection-stripper.ts
170342
- var SYSTEM_INJECTION_MARKERS = [
170343
- "<!-- OMO_INTERNAL_INITIATOR -->",
170344
- "[SYSTEM DIRECTIVE: MAGIC-CONTEXT",
170345
- "[SYSTEM DIRECTIVE: OH-MY-OPENCODE",
170346
- "[Category+Skill Reminder]",
170347
- "[EDIT ERROR - IMMEDIATE ACTION REQUIRED]",
170348
- "[task CALL FAILED - IMMEDIATE RETRY REQUIRED]",
170349
- "[EMERGENCY CONTEXT WINDOW WARNING]",
170350
- "Unstable background agent appears idle",
170351
- "**THE SUBAGENT JUST CLAIMED THIS TASK IS DONE."
170352
- ];
170353
- var SYSTEM_REMINDER_REGEX = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
170354
- var OMO_MARKER_REGEX = /<!-- OMO_INTERNAL_INITIATOR -->/g;
170355
- function stripSystemInjection(text) {
170356
- let hasInjection = false;
170357
- for (const marker of SYSTEM_INJECTION_MARKERS) {
170358
- if (text.includes(marker)) {
170359
- hasInjection = true;
170360
- break;
170361
- }
170362
- }
170363
- if (SYSTEM_REMINDER_REGEX.test(text))
170364
- hasInjection = true;
170365
- SYSTEM_REMINDER_REGEX.lastIndex = 0;
170366
- if (!hasInjection)
170367
- return null;
170368
- let cleaned = text;
170369
- cleaned = cleaned.replace(SYSTEM_REMINDER_REGEX, "");
170370
- cleaned = cleaned.replace(OMO_MARKER_REGEX, "");
170371
- cleaned = cleaned.replace(/\[SYSTEM DIRECTIVE: OH-MY-(?:OPENCODE|CLAUDE)[^\]]*\][\s\S]*?(?=\n\n(?!\s*[-*])|$)/g, "");
170372
- for (const marker of SYSTEM_INJECTION_MARKERS) {
170373
- if (marker.startsWith("<!-- ") || marker.startsWith("[SYSTEM DIRECTIVE"))
170374
- continue;
170375
- const idx = cleaned.indexOf(marker);
170376
- if (idx === -1)
170377
- continue;
170378
- const blockEnd = cleaned.indexOf(`
170379
-
170380
- `, idx + marker.length);
170381
- cleaned = blockEnd !== -1 ? cleaned.slice(0, idx) + cleaned.slice(blockEnd) : cleaned.slice(0, idx);
170382
- }
170383
- return cleaned.trim();
170384
- }
170385
-
170386
170677
  // ../plugin/src/hooks/magic-context/apply-operations.ts
170387
- var USER_DROP_PREVIEW_CHARS = 250;
170388
170678
  var RECENT_TOOL_SKELETON_WINDOW = 20;
170389
- function buildReplacementContent(tagId, target) {
170390
- const role = target.message?.info.role;
170391
- if (role !== "user") {
170392
- return `[dropped §${tagId}§]`;
170393
- }
170394
- const currentContent = target.getContent?.() ?? "";
170395
- const strippedInjection = stripSystemInjection(currentContent);
170396
- if (strippedInjection !== null && stripTagPrefix(strippedInjection).trim().length === 0) {
170397
- return `[dropped §${tagId}§]`;
170398
- }
170399
- const originalText = stripTagPrefix(currentContent);
170400
- if (originalText.length <= USER_DROP_PREVIEW_CHARS) {
170401
- return `[truncated §${tagId}§]
170402
- ${originalText}`;
170403
- }
170404
- const hardCut = originalText.slice(0, USER_DROP_PREVIEW_CHARS);
170405
- const softCutIndex = hardCut.search(/\s\S*$/);
170406
- const preview = softCutIndex > USER_DROP_PREVIEW_CHARS - 30 ? hardCut.slice(0, softCutIndex) : hardCut;
170407
- return `[truncated §${tagId}§]
170408
- ${preview}…`;
170409
- }
170410
- function applyPendingOperations(sessionId, db, targets, protectedTags = 0, preloadedTags, preloadedPendingOps) {
170679
+ function buildReplacementContent(tagId) {
170680
+ return `[dropped §${tagId}§]`;
170681
+ }
170682
+ function applyPendingOperations(sessionId, db, targets, protectedTags = 0, preloadedTags, preloadedPendingOps, syntheticPendingOps = []) {
170411
170683
  let didMutateMessage = false;
170412
170684
  db.transaction(() => {
170413
170685
  const tags = preloadedTags ?? getTagsBySession(db, sessionId);
@@ -170415,11 +170687,16 @@ function applyPendingOperations(sessionId, db, targets, protectedTags = 0, prelo
170415
170687
  const tagTypeById = new Map(tags.map((tag) => [tag.tagNumber, tag.type]));
170416
170688
  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;
170417
170689
  const pendingOps = preloadedPendingOps ?? getPendingOps(db, sessionId);
170690
+ const opsToApply = [
170691
+ ...pendingOps.map((op) => ({ op, synthetic: false })),
170692
+ ...syntheticPendingOps.map((op) => ({ op, synthetic: true }))
170693
+ ];
170418
170694
  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));
170419
- for (const pendingOp of pendingOps) {
170695
+ for (const { op: pendingOp, synthetic } of opsToApply) {
170420
170696
  const tagStatus = tagStatusById.get(pendingOp.tagId);
170421
170697
  if (tagStatus === "compacted" || tagStatus === "dropped") {
170422
- removePendingOp(db, sessionId, pendingOp.tagId);
170698
+ if (!synthetic)
170699
+ removePendingOp(db, sessionId, pendingOp.tagId);
170423
170700
  continue;
170424
170701
  }
170425
170702
  if (protectedTagIds.has(pendingOp.tagId)) {
@@ -170427,33 +170704,46 @@ function applyPendingOperations(sessionId, db, targets, protectedTags = 0, prelo
170427
170704
  }
170428
170705
  const target = targets.get(pendingOp.tagId);
170429
170706
  const isToolTag = tagTypeById.get(pendingOp.tagId) === "tool";
170707
+ if (synthetic) {
170708
+ if (!isToolTag || target?.canDrop?.() !== true)
170709
+ continue;
170710
+ }
170711
+ let shouldPersistDrop = false;
170430
170712
  if (isToolTag) {
170431
170713
  if (skeletonWindow.has(pendingOp.tagId)) {
170432
170714
  const truncResult = target?.truncate?.() ?? "absent";
170433
- if (truncResult === "incomplete") {
170715
+ if (truncResult === "incomplete" || synthetic && truncResult !== "truncated") {
170434
170716
  continue;
170435
170717
  }
170436
170718
  if (truncResult === "truncated") {
170437
170719
  didMutateMessage = true;
170438
170720
  }
170439
170721
  updateTagDropMode(db, sessionId, pendingOp.tagId, "truncated");
170722
+ shouldPersistDrop = true;
170440
170723
  } else {
170441
170724
  const dropResult = target?.drop?.() ?? "absent";
170442
- if (dropResult === "incomplete") {
170725
+ if (dropResult === "incomplete" || synthetic && dropResult !== "removed") {
170443
170726
  continue;
170444
170727
  }
170445
170728
  if (dropResult === "removed") {
170446
170729
  didMutateMessage = true;
170447
170730
  }
170448
170731
  updateTagDropMode(db, sessionId, pendingOp.tagId, "full");
170732
+ shouldPersistDrop = true;
170449
170733
  }
170450
170734
  } else if (target) {
170451
- const changed = target.setContent(buildReplacementContent(pendingOp.tagId, target));
170735
+ const changed = target.setContent(buildReplacementContent(pendingOp.tagId));
170452
170736
  if (changed)
170453
170737
  didMutateMessage = true;
170738
+ shouldPersistDrop = true;
170739
+ } else if (!synthetic) {
170740
+ shouldPersistDrop = true;
170454
170741
  }
170742
+ if (!shouldPersistDrop)
170743
+ continue;
170455
170744
  updateTagStatus(db, sessionId, pendingOp.tagId, "dropped");
170456
- removePendingOp(db, sessionId, pendingOp.tagId);
170745
+ if (!synthetic)
170746
+ removePendingOp(db, sessionId, pendingOp.tagId);
170457
170747
  }
170458
170748
  })();
170459
170749
  return didMutateMessage;
@@ -170477,7 +170767,7 @@ function applyFlushedStatuses(sessionId, db, targets, preloadedTags) {
170477
170767
  }
170478
170768
  }
170479
170769
  } else if (target) {
170480
- const changed = target.setContent(buildReplacementContent(tag.tagNumber, target));
170770
+ const changed = target.setContent(buildReplacementContent(tag.tagNumber));
170481
170771
  if (changed)
170482
170772
  didMutateMessage = true;
170483
170773
  }
@@ -170765,7 +171055,8 @@ function applyCavemanCleanup(sessionId, db, targets, tags, config2) {
170765
171055
  const result = {
170766
171056
  compressedToLite: 0,
170767
171057
  compressedToFull: 0,
170768
- compressedToUltra: 0
171058
+ compressedToUltra: 0,
171059
+ mutatedTextTags: 0
170769
171060
  };
170770
171061
  if (!config2.enabled)
170771
171062
  return result;
@@ -170806,7 +171097,9 @@ function applyCavemanCleanup(sessionId, db, targets, tags, config2) {
170806
171097
  const target = targets.get(tag.tagNumber);
170807
171098
  if (!target)
170808
171099
  continue;
170809
- target.setContent(compressed);
171100
+ const didMutate = target.setContent(compressed);
171101
+ if (didMutate)
171102
+ result.mutatedTextTags += 1;
170810
171103
  updateCavemanDepth(db, sessionId, tag.tagNumber, targetDepth);
170811
171104
  if (targetDepth === DEPTH_LITE)
170812
171105
  result.compressedToLite += 1;
@@ -171184,7 +171477,7 @@ function buildToolArcs(messages) {
171184
171477
  }
171185
171478
  return arcs.sort((a, b) => a.invOrdinal - b.invOrdinal || (a.resOrdinal ?? Number.MAX_SAFE_INTEGER) - (b.resOrdinal ?? Number.MAX_SAFE_INTEGER));
171186
171479
  }
171187
- function fenceBoundaryForToolArcs(candidate, arcs, lastCompartmentEndOrdinal) {
171480
+ function fenceBoundaryForToolArcs(candidate, arcs, lastCompartmentEndOrdinal, recentOpenArcCutoff) {
171188
171481
  let boundary = candidate;
171189
171482
  for (const arc of arcs) {
171190
171483
  if (arc.resOrdinal !== null) {
@@ -171193,6 +171486,8 @@ function fenceBoundaryForToolArcs(candidate, arcs, lastCompartmentEndOrdinal) {
171193
171486
  }
171194
171487
  continue;
171195
171488
  }
171489
+ if (arc.invOrdinal < recentOpenArcCutoff)
171490
+ continue;
171196
171491
  if (arc.invOrdinal >= lastCompartmentEndOrdinal + 1 && arc.invOrdinal < boundary) {
171197
171492
  return arc.invOrdinal;
171198
171493
  }
@@ -171430,7 +171725,7 @@ function semanticSnapBoundary(args) {
171430
171725
  return snapped;
171431
171726
  }
171432
171727
  function applyHeadCap(args) {
171433
- const { index, protectedTailStart, offset, arcs, capTokens } = args;
171728
+ const { index, protectedTailStart, offset, arcs, capTokens, recentOpenArcCutoff } = args;
171434
171729
  if (offset >= protectedTailStart)
171435
171730
  return { eligibleEndOrdinal: offset, oversizeAtomicUnit: false };
171436
171731
  let end = index.findHeadEndForCap(offset, protectedTailStart, capTokens);
@@ -171438,7 +171733,7 @@ function applyHeadCap(args) {
171438
171733
  for (const arc of arcs) {
171439
171734
  const resOrdinal = arc.resOrdinal;
171440
171735
  if (resOrdinal === null) {
171441
- if (arc.invOrdinal >= offset && arc.invOrdinal < end) {
171736
+ if (arc.invOrdinal >= recentOpenArcCutoff && arc.invOrdinal >= offset && arc.invOrdinal < end) {
171442
171737
  end = Math.min(end, arc.invOrdinal);
171443
171738
  }
171444
171739
  continue;
@@ -171505,7 +171800,14 @@ function resolveProtectedTailBoundary(ctx) {
171505
171800
  }
171506
171801
  if (ctx.mode === "manual-full-recomp") {
171507
171802
  const arcs2 = buildToolArcs(messages);
171508
- const firstOpenArc = arcs2.find((arc) => arc.resOrdinal === null && arc.invOrdinal >= offset);
171803
+ const recompTarget = deriveProtectedTailTokenTarget({
171804
+ contextLimit: ctx.contextLimit,
171805
+ executeThresholdPercentage: ctx.executeThresholdPercentage,
171806
+ usagePercentage: 0,
171807
+ triggerBudget: ctx.triggerBudget
171808
+ });
171809
+ const recentOpenArcCutoff2 = index.findSuffixStartForTokens(recompTarget.N);
171810
+ const firstOpenArc = arcs2.find((arc) => arc.resOrdinal === null && arc.invOrdinal >= offset && arc.invOrdinal >= recentOpenArcCutoff2);
171509
171811
  const protectedTailStart2 = firstOpenArc?.invOrdinal ?? rawMessageCount + 1;
171510
171812
  const rawRangeFingerprint2 = computeRawRangeFingerprint(messages, offset, protectedTailStart2);
171511
171813
  return {
@@ -171547,13 +171849,14 @@ function resolveProtectedTailBoundary(ctx) {
171547
171849
  const scaledN = ctx.emergencyTailScale ? Math.max(1, Math.floor(target.N * ctx.emergencyTailScale)) : target.N;
171548
171850
  const arcs = buildToolArcs(messages);
171549
171851
  let boundary = index.findSuffixStartForTokens(scaledN);
171852
+ const recentOpenArcCutoff = boundary;
171550
171853
  let boundaryReason = boundary === 1 ? "whole-session-smaller-than-tail" : "size-walk";
171551
171854
  const tokenAtBoundary = index.tokenForOrdinal(boundary);
171552
171855
  if (boundary <= rawMessageCount && tokenAtBoundary > Math.max(2 * scaledN, 64000) && boundary < rawMessageCount) {
171553
171856
  boundary += 1;
171554
171857
  boundaryReason = "huge-message-exception";
171555
171858
  }
171556
- boundary = fenceBoundaryForToolArcs(boundary, arcs, ctx.lastCompartmentEndOrdinal);
171859
+ boundary = fenceBoundaryForToolArcs(boundary, arcs, ctx.lastCompartmentEndOrdinal, recentOpenArcCutoff);
171557
171860
  const snapped = semanticSnapBoundary({
171558
171861
  messages,
171559
171862
  index,
@@ -171563,7 +171866,7 @@ function resolveProtectedTailBoundary(ctx) {
171563
171866
  });
171564
171867
  if (snapped !== boundary)
171565
171868
  boundaryReason = "semantic-snap";
171566
- boundary = fenceBoundaryForToolArcs(snapped, arcs, ctx.lastCompartmentEndOrdinal);
171869
+ boundary = fenceBoundaryForToolArcs(snapped, arcs, ctx.lastCompartmentEndOrdinal, recentOpenArcCutoff);
171567
171870
  let runtimeFloor = offset;
171568
171871
  if (ctx.migrationFloorActive)
171569
171872
  runtimeFloor = Math.max(runtimeFloor, ctx.priorBoundaryOrdinal);
@@ -171599,7 +171902,8 @@ function resolveProtectedTailBoundary(ctx) {
171599
171902
  offset,
171600
171903
  arcs,
171601
171904
  lastCompartmentEndOrdinal: ctx.lastCompartmentEndOrdinal,
171602
- capTokens: perRunCap
171905
+ capTokens: perRunCap,
171906
+ recentOpenArcCutoff
171603
171907
  });
171604
171908
  const rawRangeFingerprint = computeRawRangeFingerprint(messages, offset, head.eligibleEndOrdinal);
171605
171909
  return {
@@ -172073,6 +172377,13 @@ function computePressure(input) {
172073
172377
  function approxThousands(tokens) {
172074
172378
  return `${Math.round(tokens / 1000)}k`;
172075
172379
  }
172380
+ function formatOldestReclaimableHint(hint) {
172381
+ if (!hint || hint.length === 0)
172382
+ return "";
172383
+ const rendered = hint.slice(0, 4).map((tag) => `§${tag.tagNumber}§ ${tag.toolName ?? "tool"}`).join(" · ");
172384
+ return rendered.length > 0 ? `
172385
+ oldest reclaimable: ${rendered}.` : "";
172386
+ }
172076
172387
  var CHANNEL2_USABLE_FRACTION = 1 / 3;
172077
172388
  var CHANNEL2_MIN_RECLAIMABLE = 1e4;
172078
172389
  function shouldTriggerChannel2(input) {
@@ -172082,14 +172393,16 @@ function shouldTriggerChannel2(input) {
172082
172393
  return true;
172083
172394
  return input.reclaimableTokens >= input.usableTokens * CHANNEL2_USABLE_FRACTION;
172084
172395
  }
172085
- function buildChannel2Reminder(undroppedTokens) {
172396
+ function buildChannel2Reminder(undroppedTokens, hint) {
172086
172397
  const amount = approxThousands(undroppedTokens);
172398
+ const hintText = formatOldestReclaimableHint(hint);
172087
172399
  return `<system-reminder>
172088
- ` + `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.
172400
+ ` + `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}
172089
172401
  ` + `</system-reminder>`;
172090
172402
  }
172091
- function buildChannel1Reminder(level, undroppedTokens) {
172403
+ function buildChannel1Reminder(level, undroppedTokens, hint) {
172092
172404
  const amount = approxThousands(undroppedTokens);
172405
+ const hintText = formatOldestReclaimableHint(hint);
172093
172406
  let body;
172094
172407
  switch (level) {
172095
172408
  case "gentle":
@@ -172105,7 +172418,7 @@ function buildChannel1Reminder(level, undroppedTokens) {
172105
172418
  return `
172106
172419
 
172107
172420
  <system-reminder>
172108
- ${body}
172421
+ ${body}${hintText}
172109
172422
  </system-reminder>`;
172110
172423
  }
172111
172424
 
@@ -172653,6 +172966,41 @@ function renderMemoryBlockV2(memories, wrapper = "project-memory", renderOptions
172653
172966
  `);
172654
172967
  }
172655
172968
 
172969
+ // ../plugin/src/hooks/magic-context/tool-reclaim.ts
172970
+ function buildSyntheticToolReclaimOps(input) {
172971
+ const watermark = Math.max(0, input.watermark);
172972
+ if (watermark <= 0)
172973
+ return [];
172974
+ const realPendingTagIds = new Set((input.pendingOps ?? []).map((op) => op.tagId));
172975
+ const tags = getActiveTagsBySession(input.db, input.sessionId);
172976
+ const synthetic = [];
172977
+ for (const tag of tags) {
172978
+ if (tag.type !== "tool")
172979
+ continue;
172980
+ if (tag.status !== "active")
172981
+ continue;
172982
+ if (tag.tagNumber > watermark)
172983
+ continue;
172984
+ if (realPendingTagIds.has(tag.tagNumber))
172985
+ continue;
172986
+ if (input.targets.get(tag.tagNumber)?.canDrop?.() !== true)
172987
+ continue;
172988
+ synthetic.push({
172989
+ id: 0,
172990
+ sessionId: input.sessionId,
172991
+ tagId: tag.tagNumber,
172992
+ operation: "drop",
172993
+ queuedAt: 0
172994
+ });
172995
+ }
172996
+ return synthetic;
172997
+ }
172998
+ function advanceToolReclaimWatermarkToCurrentMax(db, sessionId) {
172999
+ const maxTagNumber = getMaxTagNumberBySession(db, sessionId);
173000
+ advanceToolReclaimWatermark(db, sessionId, maxTagNumber);
173001
+ return maxTagNumber;
173002
+ }
173003
+
172656
173004
  // src/context-handler.ts
172657
173005
  init_logger();
172658
173006
 
@@ -173063,7 +173411,7 @@ function tagToolPart(args) {
173063
173411
  const tagged = prependTag(tagId, text);
173064
173412
  args.part.setText(tagged);
173065
173413
  }
173066
- args.targets.set(tagId, buildToolTarget(args.part, args.message));
173414
+ args.targets.set(tagId, buildToolTarget(args.part, args.message, tagId));
173067
173415
  }
173068
173416
  function setToolContentOrText(part, content) {
173069
173417
  try {
@@ -173103,7 +173451,7 @@ function buildAggregateTarget(tagId, occurrences) {
173103
173451
  return any2 ? "removed" : "absent";
173104
173452
  },
173105
173453
  truncate() {
173106
- const sentinel = "[truncated]";
173454
+ const sentinel = `[dropped §${tagId}§]`;
173107
173455
  let any2 = false;
173108
173456
  for (const occ of occurrences) {
173109
173457
  if (setToolContentOrText(occ.part, sentinel)) {
@@ -173135,7 +173483,7 @@ function buildTextTarget(part, message) {
173135
173483
  }
173136
173484
  };
173137
173485
  }
173138
- function buildToolTarget(part, message) {
173486
+ function buildToolTarget(part, message, tagId) {
173139
173487
  return {
173140
173488
  setContent(content) {
173141
173489
  return setToolContentOrText(part, content);
@@ -173144,11 +173492,11 @@ function buildToolTarget(part, message) {
173144
173492
  return part.getText() ?? null;
173145
173493
  },
173146
173494
  drop() {
173147
- const replaced = part.replaceWithSentinel(`[dropped §${part.id ?? "?"}§]`);
173495
+ const replaced = part.replaceWithSentinel(`[dropped §${tagId}§]`);
173148
173496
  return replaced ? "removed" : "absent";
173149
173497
  },
173150
173498
  truncate() {
173151
- const ok = setToolContentOrText(part, "[truncated]");
173499
+ const ok = setToolContentOrText(part, `[dropped §${tagId}§]`);
173152
173500
  return ok ? "truncated" : "absent";
173153
173501
  },
173154
173502
  message: {
@@ -174433,9 +174781,10 @@ function maybeChannel1ReminderForToolResult(args) {
174433
174781
  return null;
174434
174782
  return {
174435
174783
  type: "text",
174436
- text: buildChannel1Reminder(decision.level, decision.undroppedTokens)
174784
+ text: buildChannel1Reminder(decision.level, decision.undroppedTokens, state.oldestReclaimableToolTags)
174437
174785
  };
174438
174786
  }
174787
+ var CHANNEL2_NUDGE_CUSTOM_TYPE = "magic-context:ceiling-nudge";
174439
174788
  function maybeDeliverChannel2Pi(pi, db, sessionId, deliverAs = "followUp") {
174440
174789
  let state;
174441
174790
  try {
@@ -174463,9 +174812,12 @@ function maybeDeliverChannel2Pi(pi, db, sessionId, deliverAs = "followUp") {
174463
174812
  if (!casChannel2NudgeState(db, sessionId, "pending", "claimed"))
174464
174813
  return false;
174465
174814
  try {
174466
- pi.sendUserMessage(buildChannel2Reminder(undropped), {
174467
- deliverAs
174468
- });
174815
+ pi.sendMessage({
174816
+ customType: CHANNEL2_NUDGE_CUSTOM_TYPE,
174817
+ content: buildChannel2Reminder(undropped, baseline.oldestReclaimableToolTags),
174818
+ display: false,
174819
+ details: { kind: "channel-2-ceiling-nudge" }
174820
+ }, { deliverAs });
174469
174821
  } catch (error51) {
174470
174822
  try {
174471
174823
  casChannel2NudgeState(db, sessionId, "claimed", "pending");
@@ -174657,6 +175009,51 @@ function planEmergencyDrop(input) {
174657
175009
  };
174658
175010
  }
174659
175011
 
175012
+ // ../plugin/src/hooks/magic-context/system-injection-stripper.ts
175013
+ var SYSTEM_INJECTION_MARKERS = [
175014
+ "<!-- OMO_INTERNAL_INITIATOR -->",
175015
+ "[SYSTEM DIRECTIVE: MAGIC-CONTEXT",
175016
+ "[SYSTEM DIRECTIVE: OH-MY-OPENCODE",
175017
+ "[Category+Skill Reminder]",
175018
+ "[EDIT ERROR - IMMEDIATE ACTION REQUIRED]",
175019
+ "[task CALL FAILED - IMMEDIATE RETRY REQUIRED]",
175020
+ "[EMERGENCY CONTEXT WINDOW WARNING]",
175021
+ "Unstable background agent appears idle",
175022
+ "**THE SUBAGENT JUST CLAIMED THIS TASK IS DONE."
175023
+ ];
175024
+ var SYSTEM_REMINDER_REGEX = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
175025
+ var OMO_MARKER_REGEX = /<!-- OMO_INTERNAL_INITIATOR -->/g;
175026
+ function stripSystemInjection(text) {
175027
+ let hasInjection = false;
175028
+ for (const marker of SYSTEM_INJECTION_MARKERS) {
175029
+ if (text.includes(marker)) {
175030
+ hasInjection = true;
175031
+ break;
175032
+ }
175033
+ }
175034
+ if (SYSTEM_REMINDER_REGEX.test(text))
175035
+ hasInjection = true;
175036
+ SYSTEM_REMINDER_REGEX.lastIndex = 0;
175037
+ if (!hasInjection)
175038
+ return null;
175039
+ let cleaned = text;
175040
+ cleaned = cleaned.replace(SYSTEM_REMINDER_REGEX, "");
175041
+ cleaned = cleaned.replace(OMO_MARKER_REGEX, "");
175042
+ cleaned = cleaned.replace(/\[SYSTEM DIRECTIVE: OH-MY-(?:OPENCODE|CLAUDE)[^\]]*\][\s\S]*?(?=\n\n(?!\s*[-*])|$)/g, "");
175043
+ for (const marker of SYSTEM_INJECTION_MARKERS) {
175044
+ if (marker.startsWith("<!-- ") || marker.startsWith("[SYSTEM DIRECTIVE"))
175045
+ continue;
175046
+ const idx = cleaned.indexOf(marker);
175047
+ if (idx === -1)
175048
+ continue;
175049
+ const blockEnd = cleaned.indexOf(`
175050
+
175051
+ `, idx + marker.length);
175052
+ cleaned = blockEnd !== -1 ? cleaned.slice(0, idx) + cleaned.slice(blockEnd) : cleaned.slice(0, idx);
175053
+ }
175054
+ return cleaned.trim();
175055
+ }
175056
+
174660
175057
  // src/heuristic-cleanup-pi.ts
174661
175058
  init_logger();
174662
175059
  var DEDUP_SAFE_TOOLS = new Set([
@@ -174815,10 +175212,14 @@ function applyPiHeuristicCleanup(sessionId, db, targets, piMessages, config2, pr
174815
175212
  if (!matched)
174816
175213
  continue;
174817
175214
  const target = targets.get(tag.tagNumber);
174818
- target?.drop?.();
175215
+ const result = target?.drop?.() ?? "absent";
175216
+ if (result === "incomplete")
175217
+ continue;
174819
175218
  updateTagDropMode(db, sessionId, tag.tagNumber, "full");
174820
175219
  updateTagStatus(db, sessionId, tag.tagNumber, "dropped");
174821
- droppedStaleReduceCalls++;
175220
+ if (result === "removed" || result === "truncated") {
175221
+ droppedStaleReduceCalls++;
175222
+ }
174822
175223
  }
174823
175224
  })();
174824
175225
  }
@@ -174890,7 +175291,9 @@ function applyPiHeuristicCleanup(sessionId, db, targets, piMessages, config2, pr
174890
175291
  continue;
174891
175292
  updateTagDropMode(db, sessionId, tag.tagNumber, "full");
174892
175293
  updateTagStatus(db, sessionId, tag.tagNumber, "dropped");
174893
- deduplicatedTools++;
175294
+ if (result === "removed" || result === "truncated") {
175295
+ deduplicatedTools++;
175296
+ }
174894
175297
  }
174895
175298
  }
174896
175299
  })();
@@ -174899,6 +175302,7 @@ function applyPiHeuristicCleanup(sessionId, db, targets, piMessages, config2, pr
174899
175302
  sessionLog(sessionId, `heuristic cleanup: dropped ${droppedTools} tool tags, stale ctx_reduce=${droppedStaleReduceCalls}, deduplicated ${deduplicatedTools} tool calls, dropped ${droppedInjections} system injections`);
174900
175303
  }
174901
175304
  let compressedTextTags = 0;
175305
+ let mutatedTextTags = 0;
174902
175306
  if (config2.caveman?.enabled) {
174903
175307
  const cavemanResult = applyCavemanCleanup(sessionId, db, targets, tags, {
174904
175308
  enabled: true,
@@ -174906,13 +175310,15 @@ function applyPiHeuristicCleanup(sessionId, db, targets, piMessages, config2, pr
174906
175310
  protectedTags: config2.protectedTags
174907
175311
  });
174908
175312
  compressedTextTags = cavemanResult.compressedToLite + cavemanResult.compressedToFull + cavemanResult.compressedToUltra;
175313
+ mutatedTextTags = cavemanResult.mutatedTextTags;
174909
175314
  }
174910
175315
  return {
174911
175316
  droppedTools,
174912
175317
  deduplicatedTools,
174913
175318
  droppedInjections,
174914
175319
  droppedStaleReduceCalls,
174915
- compressedTextTags
175320
+ compressedTextTags,
175321
+ mutatedTextTags
174916
175322
  };
174917
175323
  }
174918
175324
  function buildMessageIdToMaxTagFromTargets(targets) {
@@ -180289,9 +180695,7 @@ Context is managed for you entirely automatically — there's nothing to prune a
180289
180695
  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).`;
180290
180696
  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.`;
180291
180697
  var BASE_INTRO = (protectedTags) => `Messages and tool outputs are tagged with §N§ identifiers (e.g., §1§, §42§).
180292
- Use \`ctx_reduce\` to manage context size. It supports one operation:
180293
- - \`drop\`: Remove entirely (best for tool outputs you already acted on).
180294
- Syntax: "3-5", "1,2,9", or "1-5,8,12-15". Last ${protectedTags} tags are protected.
180698
+ 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".
180295
180699
  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.
180296
180700
  ${CTX_NOTE_GUIDANCE}
180297
180701
  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.
@@ -180310,7 +180714,7 @@ Use \`ctx_expand\` to recover the raw conversation behind a \`<compartment>\` su
180310
180714
  \`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.
180311
180715
  ${TOOL_HISTORY_GUIDANCE}
180312
180716
  NEVER drop large ranges blindly (e.g., "1-50"). Review each tag before deciding.
180313
- NEVER drop user messagesthey are short and will be summarized by compartmentalization automatically. Dropping them loses context the historian needs.
180717
+ 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\`.
180314
180718
  NEVER drop assistant text messages unless they are exceptionally large. Your conversation messages are lightweight; only large tool outputs are worth dropping.
180315
180719
  Before your turn finishes, consider using \`ctx_reduce\` to drop large tool outputs you no longer need.`;
180316
180720
  var BASE_INTRO_NO_REDUCE = () => `${CTX_NOTE_GUIDANCE}
@@ -181787,6 +182191,7 @@ function registerPiContextHandler(pi, baseOptions) {
181787
182191
  const executeThresholdTokensPi = Math.round((usageContextLimit ?? 0) * resolvedExecuteThresholdPct / 100);
181788
182192
  const usableTokensPi = Math.max(0, executeThresholdTokensPi - usageInputTokens + liveTailTokens);
181789
182193
  resetLastNudgeCycleIfTailShrank(options.db, sessionId, tailToolTokens);
182194
+ const oldestReclaimableToolTags = getOldestActiveUnprotectedToolTags(options.db, sessionId, options.protectedTags ?? 20);
181790
182195
  setPiChannel1Baseline(sessionId, {
181791
182196
  tailToolTokens,
181792
182197
  historyBudgetTokens: historyBudgetTokens ?? 0,
@@ -181795,7 +182200,8 @@ function registerPiContextHandler(pi, baseOptions) {
181795
182200
  lastInputTokens: usageInputTokens,
181796
182201
  turnToolTokens: 0,
181797
182202
  usableTokens: usableTokensPi,
181798
- reducedSinceRefresh: false
182203
+ reducedSinceRefresh: false,
182204
+ oldestReclaimableToolTags
181799
182205
  });
181800
182206
  if (usageContextLimit && usageContextLimit > 0 && resolvedExecuteThresholdPct > 0) {
181801
182207
  const channel2ShouldTrigger = shouldTriggerChannel2({
@@ -181823,6 +182229,7 @@ function registerPiContextHandler(pi, baseOptions) {
181823
182229
  logTransformTiming(sessionId, "postTransformPhase", tPostTransform);
181824
182230
  sessionLog(sessionId, `transform completed in ${(performance.now() - transformStartTime).toFixed(1)}ms (${outputMessages.length} messages, ${result.targetCount} targets, watermark: ${result.reasoningWatermark})`);
181825
182231
  clearLastTransformErrorIfSet(options.db, sessionId);
182232
+ options.maybeAutoEmbedSession?.(sessionId, projectDirectory, projectIdentity);
181826
182233
  return { messages: outputMessages };
181827
182234
  } catch (err) {
181828
182235
  const message = err instanceof Error ? err.message : String(err);
@@ -182169,6 +182576,8 @@ async function runPipeline(args) {
182169
182576
  let historyWasConsumedThisPass = false;
182170
182577
  let materializationSatisfiedThisPass = false;
182171
182578
  let pendingOpsAppliedThisPass = false;
182579
+ let pendingOpsDidMutate = false;
182580
+ let heuristicOrReasoningDidMutate = false;
182172
182581
  let suppressDeferredHistoryDrain = false;
182173
182582
  let casLost = false;
182174
182583
  const deferredHistoryWasPendingAtPassStart = deferredHistoryRefreshSessions.has(args.sessionId);
@@ -182254,7 +182663,7 @@ async function runPipeline(args) {
182254
182663
  sessionLog(args.sessionId, `pending ops WILL APPLY — reason=${applyReason}, pendingOps=${pendingOps.length}, context=${args.contextUsage.percentage.toFixed(1)}%`);
182255
182664
  try {
182256
182665
  const tApplyPending = performance.now();
182257
- applyPendingOperations(args.sessionId, args.db, targets, args.protectedTags, undefined, pendingOps);
182666
+ pendingOpsDidMutate = applyPendingOperations(args.sessionId, args.db, targets, args.protectedTags, undefined, pendingOps);
182258
182667
  logTransformTiming(args.sessionId, "applyPendingOperations", tApplyPending);
182259
182668
  executedWorkThisPass = true;
182260
182669
  materializationSatisfiedThisPass = true;
@@ -182338,6 +182747,9 @@ async function runPipeline(args) {
182338
182747
  } : undefined,
182339
182748
  caveman: args.heuristics.caveman
182340
182749
  }, activeTags, stableIdResolver);
182750
+ const heuristicMutationCount = heuristicsResult.droppedTools + heuristicsResult.deduplicatedTools + heuristicsResult.droppedInjections + heuristicsResult.droppedStaleReduceCalls + heuristicsResult.mutatedTextTags;
182751
+ if (heuristicMutationCount > 0)
182752
+ heuristicOrReasoningDidMutate = true;
182341
182753
  heuristicsExecuted = true;
182342
182754
  executedWorkThisPass = true;
182343
182755
  if (hasPendingMaterializeSignal) {
@@ -182346,7 +182758,7 @@ async function runPipeline(args) {
182346
182758
  if (currentTurnId !== null) {
182347
182759
  lastHeuristicsTurnIdBySession.set(args.sessionId, currentTurnId);
182348
182760
  }
182349
- logTransformTiming(args.sessionId, "applyHeuristicCleanup", tHeuristic, `droppedTools=${heuristicsResult.droppedTools} deduplicatedTools=${heuristicsResult.deduplicatedTools} droppedInjections=${heuristicsResult.droppedInjections} compressedTextTags=${heuristicsResult.compressedTextTags}`);
182761
+ logTransformTiming(args.sessionId, "applyHeuristicCleanup", tHeuristic, `droppedTools=${heuristicsResult.droppedTools} deduplicatedTools=${heuristicsResult.deduplicatedTools} droppedInjections=${heuristicsResult.droppedInjections} staleReduce=${heuristicsResult.droppedStaleReduceCalls} compressedTextTags=${heuristicsResult.compressedTextTags} mutatedTextTags=${heuristicsResult.mutatedTextTags}`);
182350
182762
  } catch (err) {
182351
182763
  sessionLog(args.sessionId, `heuristic cleanup failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
182352
182764
  }
@@ -182383,6 +182795,9 @@ async function runPipeline(args) {
182383
182795
  }
182384
182796
  logTransformTiming(args.sessionId, "clearOldReasoning", tClearReasoning);
182385
182797
  logTransformTiming(args.sessionId, "watermarkCleanup", tClearReasoning);
182798
+ if (clearOutcome.cleared > 0 || stripOutcome.stripped > 0) {
182799
+ heuristicOrReasoningDidMutate = true;
182800
+ }
182386
182801
  if (combinedWatermark > prevWatermark || clearOutcome.cleared > 0 || stripOutcome.stripped > 0) {
182387
182802
  executedWorkThisPass = true;
182388
182803
  }
@@ -182390,7 +182805,32 @@ async function runPipeline(args) {
182390
182805
  sessionLog(args.sessionId, `reasoning clearing failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
182391
182806
  }
182392
182807
  }
182808
+ const toolReclaimExecutePass = args.schedulerDecision === "execute";
182809
+ const alreadyMutatingThisPass = pendingOpsDidMutate || heuristicOrReasoningDidMutate;
182810
+ const emergencyDropEligible = args.forceMaterialization === true || args.contextUsage.percentage >= FORCE_MATERIALIZATION_PERCENTAGE;
182811
+ let autoReclaimTargetCount = 0;
182812
+ let autoReclaimDidMutate = false;
182813
+ if (toolReclaimExecutePass && alreadyMutatingThisPass && !emergencyDropEligible) {
182814
+ const reclaimMeta = getOrCreateSessionMeta(args.db, args.sessionId);
182815
+ const syntheticPendingOps = buildSyntheticToolReclaimOps({
182816
+ db: args.db,
182817
+ sessionId: args.sessionId,
182818
+ targets,
182819
+ watermark: reclaimMeta.toolReclaimWatermark ?? 0,
182820
+ pendingOps
182821
+ });
182822
+ autoReclaimTargetCount = syntheticPendingOps.length;
182823
+ if (syntheticPendingOps.length > 0) {
182824
+ autoReclaimDidMutate = applyPendingOperations(args.sessionId, args.db, targets, args.protectedTags, undefined, [], syntheticPendingOps);
182825
+ }
182826
+ }
182393
182827
  transcript.commit();
182828
+ if (toolReclaimExecutePass) {
182829
+ advanceToolReclaimWatermarkToCurrentMax(args.db, args.sessionId);
182830
+ }
182831
+ if (autoReclaimTargetCount > 0) {
182832
+ sessionLog(args.sessionId, `tool reclaim auto-drop: targets=${autoReclaimTargetCount} mutated=${autoReclaimDidMutate}`);
182833
+ }
182394
182834
  const postCommitStableIdByRef = new Map;
182395
182835
  const postCommitEntryIdByRef = new Map;
182396
182836
  for (let i = 0;i < args.messages.length; i++) {
@@ -182674,6 +183114,7 @@ function clearContextHandlerSession(sessionId) {
182674
183114
  rawMessageProviderUnregistersBySession.delete(sessionId);
182675
183115
  }
182676
183116
  clearSessionTracking(sessionId);
183117
+ clearPiEmbedSessionState(sessionId);
182677
183118
  }
182678
183119
 
182679
183120
  // src/commands/ctx-flush.ts
@@ -185176,7 +185617,7 @@ function formatThresholdPercent(value) {
185176
185617
  // package.json
185177
185618
  var package_default = {
185178
185619
  name: "@wolfx/pi-magic-context",
185179
- version: "0.24.1",
185620
+ version: "0.25.0",
185180
185621
  type: "module",
185181
185622
  description: "Pi coding agent extension for Magic Context — cross-session memory and context management",
185182
185623
  main: "dist/index.js",
@@ -185230,8 +185671,8 @@ var package_default = {
185230
185671
  typescript: "^5.8.0"
185231
185672
  },
185232
185673
  peerDependencies: {
185233
- "@earendil-works/pi-coding-agent": "^0.74.0",
185234
- "@earendil-works/pi-tui": "^0.74.0"
185674
+ "@earendil-works/pi-coding-agent": "*",
185675
+ "@earendil-works/pi-tui": "*"
185235
185676
  },
185236
185677
  exports: {
185237
185678
  ".": {
@@ -185816,9 +186257,184 @@ Older parts of this session are summarized into <compartment> blocks inside <ses
185816
186257
 
185817
186258
  ctx_expand(start=120, end=245) ← the compartment's own start/end attributes
185818
186259
 
185819
- 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.`;
186260
+ 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.
186261
+
186262
+ Two recovery modes for finer detail:
186263
+ - 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.
186264
+ - 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.`;
185820
186265
  var CTX_EXPAND_TOKEN_BUDGET = 15000;
185821
186266
 
186267
+ // ../plugin/src/tools/ctx-expand/render.ts
186268
+ function isRecord3(value) {
186269
+ return value !== null && typeof value === "object" && !Array.isArray(value);
186270
+ }
186271
+ function roleLabel(role) {
186272
+ if (role === "assistant")
186273
+ return "A (assistant)";
186274
+ if (role === "user")
186275
+ return "U (user)";
186276
+ return role;
186277
+ }
186278
+ function truncate2(value, max) {
186279
+ const t = value.trim();
186280
+ return t.length <= max ? t : `${t.slice(0, max)}…`;
186281
+ }
186282
+ function keyArg(input) {
186283
+ if (!input)
186284
+ return "";
186285
+ for (const k of ["filePath", "path", "pattern", "query", "symbol", "module", "action"]) {
186286
+ const v = input[k];
186287
+ if (typeof v === "string" && v.length > 0)
186288
+ return truncate2(v, 60);
186289
+ }
186290
+ if (typeof input.description === "string")
186291
+ return truncate2(input.description, 60);
186292
+ return "";
186293
+ }
186294
+ function asToolPart(part) {
186295
+ const type = typeof part.type === "string" ? part.type : "";
186296
+ if (type === "tool") {
186297
+ const state = isRecord3(part.state) ? part.state : null;
186298
+ const output = state && typeof state.output === "string" ? state.output : state && state.output != null ? JSON.stringify(state.output) : null;
186299
+ const metadata = state && isRecord3(state.metadata) ? state.metadata : null;
186300
+ const title = state && typeof state.title === "string" && state.title || metadata && typeof metadata.title === "string" && metadata.title || null;
186301
+ return {
186302
+ name: typeof part.tool === "string" ? part.tool : "tool",
186303
+ callId: typeof part.callID === "string" ? part.callID : "",
186304
+ title,
186305
+ input: state && isRecord3(state.input) ? state.input : null,
186306
+ output
186307
+ };
186308
+ }
186309
+ if (type === "tool_use") {
186310
+ return {
186311
+ name: typeof part.name === "string" ? part.name : "tool",
186312
+ callId: typeof part.id === "string" ? part.id : "",
186313
+ title: null,
186314
+ input: isRecord3(part.input) ? part.input : null,
186315
+ output: null
186316
+ };
186317
+ }
186318
+ if (type === "tool_result") {
186319
+ const content = part.content;
186320
+ const output = typeof content === "string" ? content : content != null ? JSON.stringify(content) : null;
186321
+ return {
186322
+ name: "tool_result",
186323
+ callId: typeof part.tool_use_id === "string" ? part.tool_use_id : "",
186324
+ title: null,
186325
+ input: null,
186326
+ output
186327
+ };
186328
+ }
186329
+ return null;
186330
+ }
186331
+ function textOf(part) {
186332
+ if (part.type === "text" && typeof part.text === "string")
186333
+ return part.text;
186334
+ return null;
186335
+ }
186336
+ function reasoningOf(part) {
186337
+ if ((part.type === "reasoning" || part.type === "thinking") && typeof part.text === "string") {
186338
+ return part.text;
186339
+ }
186340
+ return null;
186341
+ }
186342
+ function renderPartPreview(part) {
186343
+ if (!isRecord3(part))
186344
+ return null;
186345
+ const text = textOf(part);
186346
+ if (text !== null) {
186347
+ const t = truncate2(text, 200);
186348
+ return t.length > 0 ? ` • ${t}` : null;
186349
+ }
186350
+ const tool = asToolPart(part);
186351
+ if (tool) {
186352
+ const arg = keyArg(tool.input);
186353
+ const head = arg ? `${tool.name}(${arg})` : tool.name;
186354
+ return tool.output !== null ? ` • tool ${head} → output ~${estimateTokens(tool.output)} tok` : ` • tool ${head}`;
186355
+ }
186356
+ const reasoning = reasoningOf(part);
186357
+ if (reasoning !== null)
186358
+ return ` • [reasoning] ${truncate2(reasoning, 120)}`;
186359
+ const type = typeof part.type === "string" ? part.type : "part";
186360
+ if (type === "file")
186361
+ return " • [file]";
186362
+ if (type === "step-start" || type === "step-finish")
186363
+ return null;
186364
+ return ` • [${type}]`;
186365
+ }
186366
+ function renderPartFull(part) {
186367
+ if (!isRecord3(part))
186368
+ return null;
186369
+ const text = textOf(part);
186370
+ if (text !== null) {
186371
+ return text.trim().length > 0 ? ` [text]
186372
+ ${text}` : null;
186373
+ }
186374
+ const tool = asToolPart(part);
186375
+ if (tool) {
186376
+ const lines = [];
186377
+ const idSuffix = tool.callId ? ` #${tool.callId}` : "";
186378
+ lines.push(` [tool: ${tool.name}${idSuffix}]`);
186379
+ if (tool.title && tool.title.trim().length > 0) {
186380
+ lines.push(` description: ${tool.title.trim()}`);
186381
+ }
186382
+ if (tool.input)
186383
+ lines.push(` input: ${JSON.stringify(tool.input)}`);
186384
+ if (tool.output !== null)
186385
+ lines.push(` output:
186386
+ ${tool.output}`);
186387
+ return lines.join(`
186388
+ `);
186389
+ }
186390
+ const type = typeof part.type === "string" ? part.type : "part";
186391
+ if (type === "file") {
186392
+ const name2 = typeof part.filename === "string" && part.filename || typeof part.url === "string" && part.url || "";
186393
+ return ` [file]${name2 ? ` ${name2}` : ""}`;
186394
+ }
186395
+ return null;
186396
+ }
186397
+ function renderMessageByOrdinal(sessionId, ordinal) {
186398
+ const msg = readRawSessionMessages(sessionId).find((m) => m.ordinal === ordinal);
186399
+ if (!msg) {
186400
+ 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.`;
186401
+ }
186402
+ const rendered = msg.parts.map(renderPartFull).filter((l) => l !== null);
186403
+ const lines = [`[${msg.ordinal}] ${roleLabel(msg.role)} — full recovery:`, ""];
186404
+ if (rendered.length === 0) {
186405
+ lines.push(" (no recoverable content — message had only structural/reasoning parts)");
186406
+ } else {
186407
+ lines.push(...rendered);
186408
+ }
186409
+ return lines.join(`
186410
+ `);
186411
+ }
186412
+ function renderVerboseRange(sessionId, start, end, tokenBudget) {
186413
+ const messages = readRawSessionMessages(sessionId).filter((m) => m.ordinal >= start && m.ordinal <= end);
186414
+ const out = [];
186415
+ let usedTokens = 0;
186416
+ let lastOrdinal = start - 1;
186417
+ let truncated = false;
186418
+ for (const msg of messages) {
186419
+ const header = `[${msg.ordinal}] ${roleLabel(msg.role)}`;
186420
+ const partLines = msg.parts.map(renderPartPreview).filter((l) => l !== null);
186421
+ const block = partLines.length > 0 ? `${header}
186422
+ ${partLines.join(`
186423
+ `)}` : header;
186424
+ const blockTokens = estimateTokens(block);
186425
+ if (usedTokens + blockTokens > tokenBudget && out.length > 0) {
186426
+ truncated = true;
186427
+ break;
186428
+ }
186429
+ out.push(block);
186430
+ usedTokens += blockTokens;
186431
+ lastOrdinal = msg.ordinal;
186432
+ }
186433
+ return { text: out.join(`
186434
+
186435
+ `), lastOrdinal, truncated };
186436
+ }
186437
+
185822
186438
  // ../../node_modules/.bun/typebox@1.1.38/node_modules/typebox/build/system/memory/memory.mjs
185823
186439
  var exports_memory = {};
185824
186440
  __export(exports_memory, {
@@ -189884,12 +190500,18 @@ __export(exports_typebox, {
189884
190500
  });
189885
190501
  // src/tools/ctx-expand.ts
189886
190502
  var ParamsSchema = exports_typebox.Object({
189887
- start: exports_typebox.Number({
190503
+ start: exports_typebox.Optional(exports_typebox.Number({
189888
190504
  description: "Start message ordinal (from compartment start attribute)"
189889
- }),
189890
- end: exports_typebox.Number({
190505
+ })),
190506
+ end: exports_typebox.Optional(exports_typebox.Number({
189891
190507
  description: "End message ordinal (from compartment end attribute)"
189892
- })
190508
+ })),
190509
+ verbose: exports_typebox.Optional(exports_typebox.Boolean({
190510
+ description: "With start/end: list each message separately with its ordinal [N] and per-part preview, so you can recover one in full by ordinal."
190511
+ })),
190512
+ message: exports_typebox.Optional(exports_typebox.Number({
190513
+ description: "Full untruncated recovery of ONE message by its ordinal (text + every tool call's full input/output). Recovers a tool output you dropped with ctx_reduce."
190514
+ }))
189893
190515
  });
189894
190516
  function ok(text) {
189895
190517
  return { content: [{ type: "text", text }], details: undefined };
@@ -189908,22 +190530,41 @@ function createCtxExpandTool(deps) {
189908
190530
  description: CTX_EXPAND_DESCRIPTION,
189909
190531
  parameters: ParamsSchema,
189910
190532
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
189911
- if (typeof params.start !== "number" || typeof params.end !== "number" || params.start < 1 || params.end < params.start) {
189912
- return err("Error: start and end must be positive integers with start <= end.");
189913
- }
189914
190533
  const sessionId = ctx.sessionManager.getSessionId();
189915
190534
  if (!sessionId) {
189916
190535
  return err("Error: no active Pi session.");
189917
190536
  }
189918
- const lastCompartmentEnd = getLastCompartmentEndMessage(deps.db, sessionId);
189919
- if (lastCompartmentEnd >= 0 && params.start > lastCompartmentEnd) {
189920
- return ok(`Range ${params.start}-${params.end} is entirely within the live tail (after the last compacted message ${lastCompartmentEnd}); those messages are already visible in context.`);
189921
- }
189922
- const effectiveEnd = lastCompartmentEnd >= 0 ? Math.min(params.end, lastCompartmentEnd) : params.end;
189923
190537
  const unregister = setRawMessageProvider(sessionId, {
189924
190538
  readMessages: () => readPiSessionMessages(ctx)
189925
190539
  });
189926
190540
  try {
190541
+ if (typeof params.message === "number" && params.message >= 1) {
190542
+ return ok(renderMessageByOrdinal(sessionId, params.message));
190543
+ }
190544
+ if (typeof params.start !== "number" || typeof params.end !== "number" || params.start < 1 || params.end < params.start) {
190545
+ return err("Error: provide either message=<ordinal>, or start and end (positive integers, start <= end).");
190546
+ }
190547
+ const lastCompartmentEnd = getLastCompartmentEndMessage(deps.db, sessionId);
190548
+ if (lastCompartmentEnd >= 0 && params.start > lastCompartmentEnd) {
190549
+ return ok(`Range ${params.start}-${params.end} is entirely within the live tail (after the last compacted message ${lastCompartmentEnd}); those messages are already visible in context.`);
190550
+ }
190551
+ const effectiveEnd = lastCompartmentEnd >= 0 ? Math.min(params.end, lastCompartmentEnd) : params.end;
190552
+ if (params.verbose === true) {
190553
+ const v = renderVerboseRange(sessionId, params.start, effectiveEnd, CTX_EXPAND_TOKEN_BUDGET);
190554
+ if (!v.text) {
190555
+ return ok(`No messages found in range ${params.start}-${effectiveEnd}. The range may be outside this session's history.`);
190556
+ }
190557
+ const out = [
190558
+ `Messages ${params.start}-${v.lastOrdinal} (verbose). Recover any one in full with ctx_expand(message=<ordinal>):`,
190559
+ "",
190560
+ v.text
190561
+ ];
190562
+ if (v.truncated) {
190563
+ 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.`);
190564
+ }
190565
+ return ok(out.join(`
190566
+ `));
190567
+ }
189927
190568
  const chunk = readSessionChunk(sessionId, CTX_EXPAND_TOKEN_BUDGET, params.start, effectiveEnd + 1);
189928
190569
  if (!chunk.text || chunk.messageCount === 0) {
189929
190570
  return ok(`No messages found in range ${params.start}-${params.end}. The range may be outside this session's history.`);
@@ -190642,15 +191283,16 @@ function parseInteger(str) {
190642
191283
  }
190643
191284
 
190644
191285
  // ../plugin/src/tools/ctx-reduce/constants.ts
190645
- var CTX_REDUCE_DESCRIPTION = `Reduce context by dropping tagged content you no longer need.
190646
- Use §N§ identifiers visible in conversation. The \`drop\` param accepts ranges: "3-5", "1,2,9", "1-5,8".
191286
+ 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".
191287
+
191288
+ How it works:
191289
+ - 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.
191290
+ - The newest tags are protected: marking one just queues it until it ages out of the recent window, so marking recent output is harmless.
191291
+ - 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?".
190647
191292
 
190648
- CRITICAL RULES:
190649
- - NEVER blanket-drop large ranges (e.g., "1-50"). Always review what each tag contains first.
190650
- - Only drop tool outputs you have already processed and no longer need.
190651
- - Protected tags are accepted but deferred until they leave the last protected range.
190652
- - Keep recent context — only reduce OLD content that is no longer relevant to current work.
190653
- - Dropped content is gone forever.`;
191293
+ 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.
191294
+ Keep: user messages, unresolved errors, raw evidence you haven't extracted yet, and outputs whose exact wording may matter later.
191295
+ Never blanket-mark large ranges (e.g. "1-50") review what each tag holds first.`;
190654
191296
 
190655
191297
  // src/tools/ctx-reduce.ts
190656
191298
  var ParamsSchema4 = exports_typebox.Object({
@@ -191251,7 +191893,21 @@ async function src_default2(pi) {
191251
191893
  },
191252
191894
  historian: hist,
191253
191895
  autoSearch: auto,
191254
- resolveForProject: resolveContextOptionsForProject
191896
+ resolveForProject: resolveContextOptionsForProject,
191897
+ maybeAutoEmbedSession: (sessionId, dir, identity) => {
191898
+ maybeAutoEmbedPiSession({
191899
+ db: database,
191900
+ projectDir: dir,
191901
+ projectIdentity: identity,
191902
+ memoryEnabled: cfg.memory.enabled
191903
+ }, sessionId, dir, identity, (text) => {
191904
+ pi.sendMessage({
191905
+ customType: "ctx-status",
191906
+ content: text,
191907
+ display: true
191908
+ }, { triggerTurn: false });
191909
+ });
191910
+ }
191255
191911
  });
191256
191912
  function resolveContextOptionsForProject(dir) {
191257
191913
  const cached2 = contextOptionsByDir.get(dir);
@@ -191333,14 +191989,14 @@ async function src_default2(pi) {
191333
191989
  onProjectSeen: (identity) => seenDreamerProjectIdentities.add(identity)
191334
191990
  });
191335
191991
  info("registered /ctx-dream");
191336
- registerCtxEmbedHistoryCommand(pi, {
191992
+ registerCtxEmbedCommand(pi, {
191337
191993
  db,
191338
191994
  projectDir,
191339
191995
  projectIdentity,
191340
191996
  memoryEnabled: config2.memory.enabled,
191341
191997
  resolveProject: resolveCurrentProject
191342
191998
  });
191343
- info("registered /ctx-embed-history");
191999
+ info("registered /ctx-embed");
191344
192000
  const dreamerConfig = resolveDreamerFromConfig(config2);
191345
192001
  if (dreamerConfig) {
191346
192002
  registerPiDreamerProject({