@cortexkit/pi-magic-context 0.31.2 → 0.31.4

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.
@@ -8198,6 +8198,7 @@ function healNullTextColumns(db) {
8198
8198
  ["cache_ttl", ""],
8199
8199
  ["last_nudge_band", ""],
8200
8200
  ["last_nudge_level", ""],
8201
+ ["channel2_nudge_claim_token", ""],
8201
8202
  ["last_transform_error", ""],
8202
8203
  ["nudge_anchor_message_id", ""],
8203
8204
  ["nudge_anchor_text", ""],
@@ -8261,6 +8262,7 @@ function healNullIntegerColumns(db) {
8261
8262
  }
8262
8263
 
8263
8264
  // ../plugin/src/features/magic-context/workspaces.ts
8265
+ init_logger();
8264
8266
  import { createHash as createHash2 } from "node:crypto";
8265
8267
 
8266
8268
  // ../plugin/src/features/magic-context/memory/constants.ts
@@ -8615,6 +8617,7 @@ function storedPathBelongsToIdentity(storedProjectPath, projectIdentity) {
8615
8617
  }
8616
8618
  // ../plugin/src/features/magic-context/workspaces.ts
8617
8619
  var VALID_SHARE_CATEGORIES = new Set(V2_MEMORY_CATEGORIES);
8620
+ var DEFAULT_WORKSPACE_SHARE_CATEGORIES = ["CONSTRAINTS"];
8618
8621
  function tableExists(db, tableName) {
8619
8622
  const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ? LIMIT 1").get(tableName);
8620
8623
  return Boolean(row);
@@ -8629,23 +8632,39 @@ function uniqueSorted(values) {
8629
8632
  function placeholders(values) {
8630
8633
  return values.map(() => "?").join(", ");
8631
8634
  }
8635
+ function defaultWorkspaceShareCategories() {
8636
+ return [...DEFAULT_WORKSPACE_SHARE_CATEGORIES];
8637
+ }
8638
+ function warnInvalidShareCategories(reason, raw) {
8639
+ log("[magic-context] WARN: invalid workspace share_categories; sharing no foreign memory categories", {
8640
+ reason,
8641
+ raw
8642
+ });
8643
+ }
8632
8644
  function normalizeShareCategories(raw) {
8633
- if (raw === null || raw === undefined)
8634
- return null;
8635
- if (typeof raw !== "string")
8636
- return null;
8645
+ if (raw === null || raw === undefined) {
8646
+ return defaultWorkspaceShareCategories();
8647
+ }
8648
+ if (typeof raw !== "string") {
8649
+ warnInvalidShareCategories("not a string", raw);
8650
+ return [];
8651
+ }
8637
8652
  let parsed;
8638
8653
  try {
8639
8654
  parsed = JSON.parse(raw);
8640
8655
  } catch {
8641
- return null;
8656
+ warnInvalidShareCategories("malformed JSON", raw);
8657
+ return [];
8658
+ }
8659
+ if (!Array.isArray(parsed)) {
8660
+ warnInvalidShareCategories("not a JSON array", raw);
8661
+ return [];
8642
8662
  }
8643
- if (!Array.isArray(parsed))
8644
- return null;
8645
8663
  const categories = [];
8646
8664
  for (const value of parsed) {
8647
8665
  if (typeof value !== "string" || !VALID_SHARE_CATEGORIES.has(value)) {
8648
- return null;
8666
+ warnInvalidShareCategories("unknown category", raw);
8667
+ return [];
8649
8668
  }
8650
8669
  if (!categories.includes(value))
8651
8670
  categories.push(value);
@@ -8654,16 +8673,33 @@ function normalizeShareCategories(raw) {
8654
8673
  }
8655
8674
  function selectWorkspaceShareCategories(db, identities) {
8656
8675
  const candidates = uniqueSorted(identities.filter((identity) => identity.length > 0));
8657
- if (candidates.length === 0 || !tableExists(db, "workspace_members") || !tableExists(db, "workspaces") || !columnExists(db, "workspaces", "share_categories")) {
8676
+ if (candidates.length === 0 || !tableExists(db, "workspace_members")) {
8658
8677
  return null;
8659
8678
  }
8679
+ const hasMembership = Boolean(db.prepare(`SELECT 1
8680
+ FROM workspace_members
8681
+ WHERE project_path IN (${placeholders(candidates)})
8682
+ LIMIT 1`).get(...candidates));
8683
+ if (!hasMembership)
8684
+ return null;
8685
+ if (!tableExists(db, "workspaces")) {
8686
+ log("[magic-context] WARN: workspace member has no workspaces table; sharing no foreign memory categories");
8687
+ return [];
8688
+ }
8689
+ if (!columnExists(db, "workspaces", "share_categories")) {
8690
+ return defaultWorkspaceShareCategories();
8691
+ }
8660
8692
  const row = db.prepare(`SELECT workspace.share_categories AS shareCategories
8661
8693
  FROM workspace_members AS member
8662
8694
  JOIN workspaces AS workspace ON workspace.id = member.workspace_id
8663
8695
  WHERE member.project_path IN (${placeholders(candidates)})
8664
8696
  ORDER BY workspace.id ASC
8665
8697
  LIMIT 1`).get(...candidates);
8666
- return normalizeShareCategories(row?.shareCategories ?? null);
8698
+ if (!row) {
8699
+ log("[magic-context] WARN: workspace member has no workspace share_categories row; sharing no foreign memory categories");
8700
+ return [];
8701
+ }
8702
+ return normalizeShareCategories(row.shareCategories);
8667
8703
  }
8668
8704
  function resolveWorkspaceShareCategories(db, projectIdentity) {
8669
8705
  return selectWorkspaceShareCategories(db, [projectIdentity]);
@@ -8768,7 +8804,7 @@ function computeWorkspaceEpochFingerprint(db, identities) {
8768
8804
  const hash = createHash2("sha256");
8769
8805
  hash.update("share_categories", "utf8");
8770
8806
  hash.update("\x00");
8771
- hash.update(shareCategories === null ? "ALL" : JSON.stringify(shareCategories), "utf8");
8807
+ hash.update(shareCategories === null ? "NO_WORKSPACE" : JSON.stringify(shareCategories), "utf8");
8772
8808
  hash.update(`
8773
8809
  `);
8774
8810
  for (const identity of canonical) {
@@ -143735,6 +143771,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
143735
143771
  CREATE TABLE IF NOT EXISTS message_history_index (
143736
143772
  session_id TEXT PRIMARY KEY,
143737
143773
  last_indexed_ordinal INTEGER NOT NULL DEFAULT 0,
143774
+ dirty_floor_ordinal INTEGER NOT NULL DEFAULT 0,
143738
143775
  updated_at INTEGER NOT NULL,
143739
143776
  harness TEXT NOT NULL DEFAULT 'opencode'
143740
143777
  );
@@ -143764,6 +143801,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
143764
143801
  last_nudge_level TEXT DEFAULT '',
143765
143802
  channel2_nudge_state TEXT DEFAULT '',
143766
143803
  channel2_nudge_claimed_at INTEGER DEFAULT 0,
143804
+ channel2_nudge_claim_token TEXT DEFAULT '',
143767
143805
  last_emergency_input_sample INTEGER DEFAULT 0,
143768
143806
  last_transform_error TEXT DEFAULT '',
143769
143807
  nudge_anchor_message_id TEXT DEFAULT '',
@@ -144010,6 +144048,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
144010
144048
  ensureColumn(db, "session_meta", "last_nudge_level", "TEXT DEFAULT ''");
144011
144049
  ensureColumn(db, "session_meta", "channel2_nudge_state", "TEXT DEFAULT ''");
144012
144050
  ensureColumn(db, "session_meta", "channel2_nudge_claimed_at", "INTEGER DEFAULT 0");
144051
+ ensureColumn(db, "session_meta", "channel2_nudge_claim_token", "TEXT DEFAULT ''");
144013
144052
  ensureColumn(db, "session_meta", "last_emergency_input_sample", "INTEGER DEFAULT 0");
144014
144053
  ensureColumn(db, "session_meta", "last_transform_error", "TEXT DEFAULT ''");
144015
144054
  ensureColumn(db, "session_meta", "nudge_anchor_message_id", "TEXT DEFAULT ''");
@@ -144222,6 +144261,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
144222
144261
  ON transform_decisions(session_id, harness);
144223
144262
  `);
144224
144263
  ensureColumn(db, "tags", "harness", "TEXT NOT NULL DEFAULT 'opencode'");
144264
+ ensureColumn(db, "message_history_index", "dirty_floor_ordinal", "INTEGER NOT NULL DEFAULT 0");
144225
144265
  ensureColumn(db, "pending_ops", "harness", "TEXT NOT NULL DEFAULT 'opencode'");
144226
144266
  ensureColumn(db, "source_contents", "harness", "TEXT NOT NULL DEFAULT 'opencode'");
144227
144267
  ensureColumn(db, "compartments", "harness", "TEXT NOT NULL DEFAULT 'opencode'");
@@ -144237,7 +144277,7 @@ var CHANNEL2_CLAIM_TTL_MS = 120000;
144237
144277
  function healWedgedChannel2Claims(db) {
144238
144278
  try {
144239
144279
  const staleBefore = Date.now() - CHANNEL2_CLAIM_TTL_MS;
144240
- db.prepare("UPDATE session_meta SET channel2_nudge_state = 'pending', channel2_nudge_claimed_at = 0 WHERE channel2_nudge_state = 'claimed' AND (channel2_nudge_claimed_at IS NULL OR channel2_nudge_claimed_at = 0 OR channel2_nudge_claimed_at <= ?)").run(staleBefore);
144280
+ db.prepare("UPDATE session_meta SET channel2_nudge_state = 'pending', channel2_nudge_claimed_at = 0, channel2_nudge_claim_token = '' WHERE channel2_nudge_state = 'claimed' AND (channel2_nudge_claimed_at IS NULL OR channel2_nudge_claimed_at = 0 OR channel2_nudge_claimed_at <= ?)").run(staleBefore);
144241
144281
  } catch {}
144242
144282
  }
144243
144283
  function openDatabase(dbPathOrOptions) {
@@ -145054,92 +145094,6 @@ function migrateLegacyExperimental(rawConfig, warnings) {
145054
145094
  return patched;
145055
145095
  }
145056
145096
 
145057
- // ../plugin/src/config/project-security.ts
145058
- var HIDDEN_AGENT_KEYS = ["historian", "dreamer", "sidekick"];
145059
- var AGENT_ESCALATION_FIELDS = ["prompt", "permission", "tools", "system_prompt"];
145060
- var EMBEDDING_DESTINATION_FIELDS = ["endpoint", "provider"];
145061
- function isPlainObject2(value) {
145062
- return typeof value === "object" && value !== null && !Array.isArray(value);
145063
- }
145064
- function stripUnsafeProjectConfigFields(projectRaw) {
145065
- const warnings = [];
145066
- if ("auto_update" in projectRaw) {
145067
- delete projectRaw.auto_update;
145068
- warnings.push("Ignoring auto_update from project config (security: this setting only honors user-level config).");
145069
- }
145070
- if ("language" in projectRaw) {
145071
- delete projectRaw.language;
145072
- warnings.push("Ignoring language from project config (security: output language is a user-level setting).");
145073
- }
145074
- if ("sqlite" in projectRaw) {
145075
- delete projectRaw.sqlite;
145076
- warnings.push("Ignoring sqlite.* from project config (security: SQLite cache/mmap PRAGMAs apply to the " + "process-global shared database handle; only user-level config may set them).");
145077
- }
145078
- const embedding = projectRaw.embedding;
145079
- if (isPlainObject2(embedding)) {
145080
- const removed = [];
145081
- for (const field of EMBEDDING_DESTINATION_FIELDS) {
145082
- if (field in embedding) {
145083
- delete embedding[field];
145084
- removed.push(field);
145085
- }
145086
- }
145087
- if (removed.length > 0) {
145088
- warnings.push(`Ignoring embedding.${removed.join("/")} from project config ` + "(security: a repository cannot choose where private text is embedded).");
145089
- }
145090
- }
145091
- for (const agentKey of HIDDEN_AGENT_KEYS) {
145092
- const block = projectRaw[agentKey];
145093
- if (!isPlainObject2(block))
145094
- continue;
145095
- const removed = [];
145096
- for (const field of AGENT_ESCALATION_FIELDS) {
145097
- if (field in block) {
145098
- delete block[field];
145099
- removed.push(field);
145100
- }
145101
- }
145102
- if (removed.length > 0) {
145103
- warnings.push(`Ignoring ${agentKey}.${removed.join("/")} from project config ` + "(security: a repository cannot reprogram or re-permission hidden agents).");
145104
- }
145105
- }
145106
- return warnings;
145107
- }
145108
- function normalizeEndpoint(value) {
145109
- if (typeof value !== "string")
145110
- return;
145111
- const trimmed = value.trim().replace(/\/+$/, "");
145112
- return trimmed.length > 0 ? trimmed.toLowerCase() : undefined;
145113
- }
145114
- function dropInheritedEmbeddingKeyOnRedirect(projectRaw, mergedRaw, userRaw) {
145115
- const projectEmbedding = projectRaw.embedding;
145116
- if (!isPlainObject2(projectEmbedding))
145117
- return [];
145118
- const redirectsEndpoint = "endpoint" in projectEmbedding;
145119
- if (!redirectsEndpoint)
145120
- return [];
145121
- const userEmbedding = userRaw?.embedding;
145122
- if (isPlainObject2(userEmbedding)) {
145123
- const projectEndpoint = normalizeEndpoint(projectEmbedding.endpoint);
145124
- const userEndpoint = normalizeEndpoint(userEmbedding.endpoint);
145125
- if (projectEndpoint !== undefined && projectEndpoint === userEndpoint) {
145126
- return [];
145127
- }
145128
- }
145129
- const providesOwnKey = typeof projectEmbedding.api_key === "string" && projectEmbedding.api_key.length > 0;
145130
- if (providesOwnKey)
145131
- return [];
145132
- const mergedEmbedding = mergedRaw.embedding;
145133
- if (!isPlainObject2(mergedEmbedding))
145134
- return [];
145135
- if (!("api_key" in mergedEmbedding))
145136
- return [];
145137
- delete mergedEmbedding.api_key;
145138
- return [
145139
- "Dropped inherited user embedding api_key because project config redirected " + "embedding.endpoint without supplying its own key (security: prevents key " + "exfiltration to a repository-chosen endpoint)."
145140
- ];
145141
- }
145142
-
145143
145097
  // ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
145144
145098
  var exports_external = {};
145145
145099
  __export(exports_external, {
@@ -145771,7 +145725,7 @@ __export(exports_util, {
145771
145725
  jsonStringifyReplacer: () => jsonStringifyReplacer,
145772
145726
  joinValues: () => joinValues,
145773
145727
  issue: () => issue,
145774
- isPlainObject: () => isPlainObject3,
145728
+ isPlainObject: () => isPlainObject2,
145775
145729
  isObject: () => isObject,
145776
145730
  hexToUint8Array: () => hexToUint8Array,
145777
145731
  getSizableOrigin: () => getSizableOrigin,
@@ -145953,7 +145907,7 @@ var allowsEval = /* @__PURE__ */ cached(() => {
145953
145907
  return false;
145954
145908
  }
145955
145909
  });
145956
- function isPlainObject3(o) {
145910
+ function isPlainObject2(o) {
145957
145911
  if (isObject(o) === false)
145958
145912
  return false;
145959
145913
  const ctor = o.constructor;
@@ -145970,7 +145924,7 @@ function isPlainObject3(o) {
145970
145924
  return true;
145971
145925
  }
145972
145926
  function shallowClone(o) {
145973
- if (isPlainObject3(o))
145927
+ if (isPlainObject2(o))
145974
145928
  return { ...o };
145975
145929
  if (Array.isArray(o))
145976
145930
  return [...o];
@@ -146174,7 +146128,7 @@ function omit(schema, mask) {
146174
146128
  return clone(schema, def);
146175
146129
  }
146176
146130
  function extend(schema, shape) {
146177
- if (!isPlainObject3(shape)) {
146131
+ if (!isPlainObject2(shape)) {
146178
146132
  throw new Error("Invalid input to extend: expected a plain object");
146179
146133
  }
146180
146134
  const checks = schema._zod.def.checks;
@@ -146197,7 +146151,7 @@ function extend(schema, shape) {
146197
146151
  return clone(schema, def);
146198
146152
  }
146199
146153
  function safeExtend(schema, shape) {
146200
- if (!isPlainObject3(shape)) {
146154
+ if (!isPlainObject2(shape)) {
146201
146155
  throw new Error("Invalid input to safeExtend: expected a plain object");
146202
146156
  }
146203
146157
  const def = mergeDefs(schema._zod.def, {
@@ -148540,7 +148494,7 @@ function mergeValues(a, b) {
148540
148494
  if (a instanceof Date && b instanceof Date && +a === +b) {
148541
148495
  return { valid: true, data: a };
148542
148496
  }
148543
- if (isPlainObject3(a) && isPlainObject3(b)) {
148497
+ if (isPlainObject2(a) && isPlainObject2(b)) {
148544
148498
  const bKeys = Object.keys(b);
148545
148499
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
148546
148500
  const newObj = { ...a, ...b };
@@ -148726,7 +148680,7 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
148726
148680
  $ZodType.init(inst, def);
148727
148681
  inst._zod.parse = (payload, ctx) => {
148728
148682
  const input = payload.value;
148729
- if (!isPlainObject3(input)) {
148683
+ if (!isPlainObject2(input)) {
148730
148684
  payload.issues.push({
148731
148685
  expected: "record",
148732
148686
  code: "invalid_type",
@@ -159897,6 +159851,10 @@ var MagicContextConfigSchema = exports_external.object({
159897
159851
  }).describe("Embedding provider configuration"),
159898
159852
  temporal_awareness: exports_external.boolean().default(true).describe('Inject wall-clock gap markers (<!-- +Xm -->) between user messages where > 5 min elapsed since the previous message, and add start/end date attributes on compartments. Gives the agent a sense of session pacing and "how long ago" across multi-day sessions. Graduated from experimental.temporal_awareness; default: true (set false to opt out).'),
159899
159853
  keep_subagents: exports_external.boolean().default(false).describe("Debug: keep the child sessions Magic Context spawns for its own subagents (historian, dreamer, sidekick, memory-migration) instead of deleting them on success. Useful for short-term inspection/data collection — their full transcript (prompt, tool calls, token usage, output) stays in the host session store. Kept sessions accumulate until manually cleared; leave false for normal use. Requires a restart to take effect."),
159854
+ todowrite: exports_external.object({
159855
+ enabled: exports_external.boolean().default(true).describe("Pi only: register Magic Context's todowrite task-list tool. Disable if you use your own todo extension. OpenCode ships its own built-in todowrite; this setting has no effect there."),
159856
+ overlay: exports_external.boolean().default(true).describe("Pi only: show the persistent todo overlay above the editor while tasks are active.")
159857
+ }).default({ enabled: true, overlay: true }).describe("Pi-only todowrite tool and overlay controls. Pi registers tools and widgets at extension boot, so changing this after /cd requires /reload or restart."),
159900
159858
  smart_drops: exports_external.boolean().default(false).describe("Content-aware reclaim of provably-superseded tool output, layered on the existing execute-pass auto-drop. When on: superseded todowrite (keep newest 1), spent ctx_reduce (keep newest 5), and zero-value meta (bash_status, bash_kill, ctx_note read/dismiss) outputs are dropped; older edits to a file are compressed to a filePath-preserving marker while the newest edit per file stays full. Only acts on passes already busting the cache, so it never originates a cache bust. Honors the protected-tag reserve. Experimental: opt-in, default off until cache stability is proven; when off the wire is byte-identical to the positional-only reclaim. Requires a restart."),
159901
159859
  caveman_text_compression: exports_external.object({
159902
159860
  enabled: exports_external.boolean().default(false).describe("Apply deterministic caveman-style text compression to old conversation text. Active for primary sessions when enabled; never for subagents. Compresses user/assistant text in oldest-first tiers: ultra (oldest 20%), full, lite, untouched (newest 40%)."),
@@ -159933,6 +159891,295 @@ var MagicContextConfigSchema = exports_external.object({
159933
159891
  };
159934
159892
  });
159935
159893
 
159894
+ // ../plugin/src/config/project-security.ts
159895
+ var HIDDEN_AGENT_KEYS = ["historian", "dreamer", "sidekick"];
159896
+ var HISTORIAN_USER_ONLY_FIELDS = ["model", "fallback_models"];
159897
+ var AGENT_ESCALATION_FIELDS = ["prompt", "permission", "tools", "system_prompt"];
159898
+ var EMBEDDING_DESTINATION_FIELDS = ["endpoint", "provider"];
159899
+ var PERCENTAGE_THRESHOLD_REASON = "security: a repository may only raise compaction thresholds above the user's effective value; it cannot force earlier historian work or cloned-repo cost escalation.";
159900
+ var TOKEN_THRESHOLD_REASON = "security: a repository may only raise execute_threshold_tokens above the user's trusted token threshold; it cannot force earlier historian work or cloned-repo cost escalation.";
159901
+ var TOKEN_THRESHOLD_INTRODUCTION_REASON = "security: a repository cannot introduce a new execute_threshold_tokens override when the user has no trusted token threshold for that key; that could force earlier historian work or cloned-repo cost escalation.";
159902
+ function isPlainObject3(value) {
159903
+ return typeof value === "object" && value !== null && !Array.isArray(value);
159904
+ }
159905
+ function isValidPercentageThreshold(value) {
159906
+ return typeof value === "number" && Number.isFinite(value) && value >= 20 && value <= 80;
159907
+ }
159908
+ function isValidTokenThreshold(value) {
159909
+ return typeof value === "number" && Number.isFinite(value) && value >= 5000 && value <= 2000000;
159910
+ }
159911
+ function normalizeTrustedPercentageThresholds(value) {
159912
+ if (typeof value === "number" && Number.isFinite(value)) {
159913
+ return { defaultValue: value, overrides: new Map };
159914
+ }
159915
+ if (isPlainObject3(value) && typeof value.default === "number" && Number.isFinite(value.default)) {
159916
+ const overrides = new Map;
159917
+ for (const [key, child] of Object.entries(value)) {
159918
+ if (key === "default")
159919
+ continue;
159920
+ if (typeof child === "number" && Number.isFinite(child)) {
159921
+ overrides.set(key, child);
159922
+ }
159923
+ }
159924
+ return { defaultValue: value.default, overrides };
159925
+ }
159926
+ return { defaultValue: DEFAULT_EXECUTE_THRESHOLD_PERCENTAGE, overrides: new Map };
159927
+ }
159928
+ function normalizeTrustedTokenThresholds(value) {
159929
+ if (!isPlainObject3(value)) {
159930
+ return { defaultValue: undefined, overrides: new Map };
159931
+ }
159932
+ const overrides = new Map;
159933
+ for (const [key, child] of Object.entries(value)) {
159934
+ if (key === "default")
159935
+ continue;
159936
+ if (typeof child === "number" && Number.isFinite(child)) {
159937
+ overrides.set(key, child);
159938
+ }
159939
+ }
159940
+ return {
159941
+ defaultValue: typeof value.default === "number" && Number.isFinite(value.default) ? value.default : undefined,
159942
+ overrides
159943
+ };
159944
+ }
159945
+ function clonePercentageThresholds(value) {
159946
+ return {
159947
+ defaultValue: value.defaultValue,
159948
+ overrides: new Map(value.overrides)
159949
+ };
159950
+ }
159951
+ function cloneTokenThresholds(value) {
159952
+ return {
159953
+ defaultValue: value.defaultValue,
159954
+ overrides: new Map(value.overrides)
159955
+ };
159956
+ }
159957
+ function percentageThresholdsEqual(left, right) {
159958
+ if (left.defaultValue !== right.defaultValue)
159959
+ return false;
159960
+ if (left.overrides.size !== right.overrides.size)
159961
+ return false;
159962
+ for (const [key, value] of left.overrides) {
159963
+ if (right.overrides.get(key) !== value)
159964
+ return false;
159965
+ }
159966
+ return true;
159967
+ }
159968
+ function setMergedPercentageThreshold(mergedRaw, value) {
159969
+ if (value.overrides.size === 0) {
159970
+ mergedRaw.execute_threshold_percentage = value.defaultValue;
159971
+ return;
159972
+ }
159973
+ const serialized = { default: value.defaultValue };
159974
+ for (const [key, threshold] of value.overrides) {
159975
+ serialized[key] = threshold;
159976
+ }
159977
+ mergedRaw.execute_threshold_percentage = serialized;
159978
+ }
159979
+ function setMergedTokenThreshold(mergedRaw, value) {
159980
+ if (value.defaultValue === undefined && value.overrides.size === 0) {
159981
+ delete mergedRaw.execute_threshold_tokens;
159982
+ return;
159983
+ }
159984
+ const serialized = {};
159985
+ if (value.defaultValue !== undefined) {
159986
+ serialized.default = value.defaultValue;
159987
+ }
159988
+ for (const [key, threshold] of value.overrides) {
159989
+ serialized[key] = threshold;
159990
+ }
159991
+ mergedRaw.execute_threshold_tokens = serialized;
159992
+ }
159993
+ function makeProjectThresholdWarning(field, reason) {
159994
+ return `Ignoring ${field} from project config (${reason})`;
159995
+ }
159996
+ function stripUnsafeProjectConfigFields(projectRaw) {
159997
+ const warnings = [];
159998
+ if ("auto_update" in projectRaw) {
159999
+ delete projectRaw.auto_update;
160000
+ warnings.push("Ignoring auto_update from project config (security: this setting only honors user-level config).");
160001
+ }
160002
+ if ("language" in projectRaw) {
160003
+ delete projectRaw.language;
160004
+ warnings.push("Ignoring language from project config (security: output language is a user-level setting).");
160005
+ }
160006
+ if ("sqlite" in projectRaw) {
160007
+ delete projectRaw.sqlite;
160008
+ warnings.push("Ignoring sqlite.* from project config (security: SQLite cache/mmap PRAGMAs apply to the " + "process-global shared database handle; only user-level config may set them).");
160009
+ }
160010
+ const embedding = projectRaw.embedding;
160011
+ if (isPlainObject3(embedding)) {
160012
+ const removed = [];
160013
+ for (const field of EMBEDDING_DESTINATION_FIELDS) {
160014
+ if (field in embedding) {
160015
+ delete embedding[field];
160016
+ removed.push(field);
160017
+ }
160018
+ }
160019
+ if (removed.length > 0) {
160020
+ warnings.push(`Ignoring embedding.${removed.join("/")} from project config ` + "(security: a repository cannot choose where private text is embedded).");
160021
+ }
160022
+ }
160023
+ const historian = projectRaw.historian;
160024
+ if (isPlainObject3(historian)) {
160025
+ const removed = [];
160026
+ for (const field of HISTORIAN_USER_ONLY_FIELDS) {
160027
+ if (field in historian) {
160028
+ delete historian[field];
160029
+ removed.push(field);
160030
+ }
160031
+ }
160032
+ if (removed.length > 0) {
160033
+ warnings.push(`Ignoring historian.${removed.join("/")} from project config ` + "(security: historian model selection is user-level only; a repository cannot force extra compaction cost).");
160034
+ }
160035
+ }
160036
+ for (const agentKey of HIDDEN_AGENT_KEYS) {
160037
+ const block = projectRaw[agentKey];
160038
+ if (!isPlainObject3(block))
160039
+ continue;
160040
+ const removed = [];
160041
+ for (const field of AGENT_ESCALATION_FIELDS) {
160042
+ if (field in block) {
160043
+ delete block[field];
160044
+ removed.push(field);
160045
+ }
160046
+ }
160047
+ if (removed.length > 0) {
160048
+ warnings.push(`Ignoring ${agentKey}.${removed.join("/")} from project config ` + "(security: a repository cannot reprogram or re-permission hidden agents).");
160049
+ }
160050
+ }
160051
+ return warnings;
160052
+ }
160053
+ function constrainProjectThresholdOverrides(args) {
160054
+ const warnings = [];
160055
+ const basePercentage = normalizeTrustedPercentageThresholds(args.trustedBaseConfig.execute_threshold_percentage);
160056
+ const baseTokens = normalizeTrustedTokenThresholds(args.trustedBaseConfig.execute_threshold_tokens);
160057
+ if ("execute_threshold_percentage" in args.projectRaw) {
160058
+ const projectValue = args.projectRaw.execute_threshold_percentage;
160059
+ if (isValidPercentageThreshold(projectValue)) {
160060
+ const constrained = clonePercentageThresholds(basePercentage);
160061
+ constrained.defaultValue = Math.max(basePercentage.defaultValue, projectValue);
160062
+ for (const [modelKey, threshold] of basePercentage.overrides) {
160063
+ const raisedThreshold = Math.max(threshold, projectValue);
160064
+ if (raisedThreshold === constrained.defaultValue) {
160065
+ constrained.overrides.delete(modelKey);
160066
+ } else {
160067
+ constrained.overrides.set(modelKey, raisedThreshold);
160068
+ }
160069
+ }
160070
+ setMergedPercentageThreshold(args.mergedRaw, constrained);
160071
+ if (percentageThresholdsEqual(constrained, basePercentage)) {
160072
+ warnings.push(makeProjectThresholdWarning("execute_threshold_percentage", PERCENTAGE_THRESHOLD_REASON));
160073
+ }
160074
+ } else if (isPlainObject3(projectValue)) {
160075
+ const constrained = clonePercentageThresholds(basePercentage);
160076
+ let touchedValidEntry = false;
160077
+ if (isValidPercentageThreshold(projectValue.default)) {
160078
+ touchedValidEntry = true;
160079
+ if (projectValue.default > basePercentage.defaultValue) {
160080
+ constrained.defaultValue = projectValue.default;
160081
+ } else {
160082
+ warnings.push(makeProjectThresholdWarning("execute_threshold_percentage.default", PERCENTAGE_THRESHOLD_REASON));
160083
+ }
160084
+ }
160085
+ for (const [modelKey, rawValue] of Object.entries(projectValue)) {
160086
+ if (modelKey === "default")
160087
+ continue;
160088
+ if (!isValidPercentageThreshold(rawValue))
160089
+ continue;
160090
+ touchedValidEntry = true;
160091
+ const baseValue = basePercentage.overrides.get(modelKey) ?? basePercentage.defaultValue;
160092
+ if (rawValue > baseValue) {
160093
+ if (rawValue === constrained.defaultValue) {
160094
+ constrained.overrides.delete(modelKey);
160095
+ } else {
160096
+ constrained.overrides.set(modelKey, rawValue);
160097
+ }
160098
+ } else {
160099
+ warnings.push(makeProjectThresholdWarning(`execute_threshold_percentage.${modelKey}`, PERCENTAGE_THRESHOLD_REASON));
160100
+ }
160101
+ }
160102
+ if (touchedValidEntry) {
160103
+ setMergedPercentageThreshold(args.mergedRaw, constrained);
160104
+ }
160105
+ }
160106
+ }
160107
+ if ("execute_threshold_tokens" in args.projectRaw && isPlainObject3(args.projectRaw.execute_threshold_tokens)) {
160108
+ const projectValue = args.projectRaw.execute_threshold_tokens;
160109
+ const constrained = cloneTokenThresholds(baseTokens);
160110
+ let touchedValidEntry = false;
160111
+ if (isValidTokenThreshold(projectValue.default)) {
160112
+ touchedValidEntry = true;
160113
+ if (baseTokens.defaultValue === undefined) {
160114
+ warnings.push(makeProjectThresholdWarning("execute_threshold_tokens.default", TOKEN_THRESHOLD_INTRODUCTION_REASON));
160115
+ } else if (projectValue.default > baseTokens.defaultValue) {
160116
+ constrained.defaultValue = projectValue.default;
160117
+ } else {
160118
+ warnings.push(makeProjectThresholdWarning("execute_threshold_tokens.default", TOKEN_THRESHOLD_REASON));
160119
+ }
160120
+ }
160121
+ for (const [modelKey, rawValue] of Object.entries(projectValue)) {
160122
+ if (modelKey === "default")
160123
+ continue;
160124
+ if (!isValidTokenThreshold(rawValue))
160125
+ continue;
160126
+ touchedValidEntry = true;
160127
+ const baseValue = baseTokens.overrides.get(modelKey) ?? baseTokens.defaultValue;
160128
+ if (baseValue === undefined) {
160129
+ warnings.push(makeProjectThresholdWarning(`execute_threshold_tokens.${modelKey}`, TOKEN_THRESHOLD_INTRODUCTION_REASON));
160130
+ continue;
160131
+ }
160132
+ if (rawValue > baseValue) {
160133
+ if (rawValue === constrained.defaultValue) {
160134
+ constrained.overrides.delete(modelKey);
160135
+ } else {
160136
+ constrained.overrides.set(modelKey, rawValue);
160137
+ }
160138
+ } else {
160139
+ warnings.push(makeProjectThresholdWarning(`execute_threshold_tokens.${modelKey}`, TOKEN_THRESHOLD_REASON));
160140
+ }
160141
+ }
160142
+ if (touchedValidEntry) {
160143
+ setMergedTokenThreshold(args.mergedRaw, constrained);
160144
+ }
160145
+ }
160146
+ return warnings;
160147
+ }
160148
+ function normalizeEndpoint(value) {
160149
+ if (typeof value !== "string")
160150
+ return;
160151
+ const trimmed = value.trim().replace(/\/+$/, "");
160152
+ return trimmed.length > 0 ? trimmed.toLowerCase() : undefined;
160153
+ }
160154
+ function dropInheritedEmbeddingKeyOnRedirect(projectRaw, mergedRaw, userRaw) {
160155
+ const projectEmbedding = projectRaw.embedding;
160156
+ if (!isPlainObject3(projectEmbedding))
160157
+ return [];
160158
+ const redirectsEndpoint = "endpoint" in projectEmbedding;
160159
+ if (!redirectsEndpoint)
160160
+ return [];
160161
+ const userEmbedding = userRaw?.embedding;
160162
+ if (isPlainObject3(userEmbedding)) {
160163
+ const projectEndpoint = normalizeEndpoint(projectEmbedding.endpoint);
160164
+ const userEndpoint = normalizeEndpoint(userEmbedding.endpoint);
160165
+ if (projectEndpoint !== undefined && projectEndpoint === userEndpoint) {
160166
+ return [];
160167
+ }
160168
+ }
160169
+ const providesOwnKey = typeof projectEmbedding.api_key === "string" && projectEmbedding.api_key.length > 0;
160170
+ if (providesOwnKey)
160171
+ return [];
160172
+ const mergedEmbedding = mergedRaw.embedding;
160173
+ if (!isPlainObject3(mergedEmbedding))
160174
+ return [];
160175
+ if (!("api_key" in mergedEmbedding))
160176
+ return [];
160177
+ delete mergedEmbedding.api_key;
160178
+ return [
160179
+ "Dropped inherited user embedding api_key because project config redirected " + "embedding.endpoint without supplying its own key (security: prevents key " + "exfiltration to a repository-chosen endpoint)."
160180
+ ];
160181
+ }
160182
+
159936
160183
  // ../plugin/src/config/variable.ts
159937
160184
  import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
159938
160185
  import { homedir as homedir3 } from "node:os";
@@ -160265,6 +160512,7 @@ function loadPiConfig(opts = {}) {
160265
160512
  return a.scope === "user" ? -1 : 1;
160266
160513
  });
160267
160514
  const userRaw = mergeFiles.find((f) => f.scope === "user")?.config;
160515
+ const trustedBaseConfig = parsePiConfig(userRaw ?? {}).config;
160268
160516
  for (const loaded of mergeFiles) {
160269
160517
  const prefix = loaded.scope === "user" ? "[user config]" : "[project config]";
160270
160518
  warnings.push(...loaded.warnings.map((warning) => `${prefix} ${warning}`));
@@ -160277,6 +160525,13 @@ function loadPiConfig(opts = {}) {
160277
160525
  for (const warning of dropInheritedEmbeddingKeyOnRedirect(projectRaw, rawConfig, userRaw)) {
160278
160526
  warnings.push(`${prefix} ${warning}`);
160279
160527
  }
160528
+ for (const warning of constrainProjectThresholdOverrides({
160529
+ mergedRaw: rawConfig,
160530
+ projectRaw,
160531
+ trustedBaseConfig
160532
+ })) {
160533
+ warnings.push(`${prefix} ${warning}`);
160534
+ }
160280
160535
  } else {
160281
160536
  rawConfig = mergeRawConfigs(rawConfig, loaded.config);
160282
160537
  }
@@ -160375,6 +160630,7 @@ function loadPiConfigDetailed(opts = {}) {
160375
160630
  return a.scope === "user" ? -1 : 1;
160376
160631
  });
160377
160632
  const userRaw = mergeFiles.find((f) => f.scope === "user")?.config;
160633
+ const trustedBaseConfig = parsePiConfig(userRaw ?? {}).config;
160378
160634
  for (const loaded of mergeFiles) {
160379
160635
  const prefix = loaded.scope === "user" ? "[user config]" : "[project config]";
160380
160636
  warnings.push(...loaded.warnings.map((warning) => `${prefix} ${warning}`));
@@ -160387,6 +160643,13 @@ function loadPiConfigDetailed(opts = {}) {
160387
160643
  for (const warning of dropInheritedEmbeddingKeyOnRedirect(projectRaw, rawConfig, userRaw)) {
160388
160644
  warnings.push(`${prefix} ${warning}`);
160389
160645
  }
160646
+ for (const warning of constrainProjectThresholdOverrides({
160647
+ mergedRaw: rawConfig,
160648
+ projectRaw,
160649
+ trustedBaseConfig
160650
+ })) {
160651
+ warnings.push(`${prefix} ${warning}`);
160652
+ }
160390
160653
  } else {
160391
160654
  rawConfig = mergeRawConfigs(rawConfig, loaded.config);
160392
160655
  }
@@ -161771,6 +162034,7 @@ class OpenAICompatibleEmbeddingProvider {
161771
162034
 
161772
162035
  // ../plugin/src/features/magic-context/memory/storage-memory-embeddings.ts
161773
162036
  var saveEmbeddingStatements = new WeakMap;
162037
+ var saveEmbeddingIfHashMatchesStatements = new WeakMap;
161774
162038
  var loadAllEmbeddingsStatements = new WeakMap;
161775
162039
  var deleteEmbeddingStatements = new WeakMap;
161776
162040
  var getStoredModelIdStatements = new WeakMap;
@@ -161801,6 +162065,14 @@ function getSaveEmbeddingStatement(db) {
161801
162065
  }
161802
162066
  return stmt;
161803
162067
  }
162068
+ function getSaveEmbeddingIfHashMatchesStatement(db) {
162069
+ let stmt = saveEmbeddingIfHashMatchesStatements.get(db);
162070
+ if (!stmt) {
162071
+ stmt = db.prepare("INSERT INTO memory_embeddings (memory_id, embedding, model_id) SELECT ?, ?, ? FROM memories WHERE id = ? AND normalized_hash = ? ON CONFLICT(memory_id, model_id) DO UPDATE SET embedding = excluded.embedding");
162072
+ saveEmbeddingIfHashMatchesStatements.set(db, stmt);
162073
+ }
162074
+ return stmt;
162075
+ }
161804
162076
  function getLoadAllEmbeddingsStatement(db) {
161805
162077
  let stmt = loadAllEmbeddingsStatements.get(db);
161806
162078
  if (!stmt) {
@@ -161829,6 +162101,10 @@ function saveEmbedding(db, memoryId, embedding, modelId) {
161829
162101
  const blob = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
161830
162102
  getSaveEmbeddingStatement(db).run(memoryId, blob, modelId);
161831
162103
  }
162104
+ function saveEmbeddingIfHashMatches(db, memoryId, embedding, modelId, normalizedHash) {
162105
+ const blob = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
162106
+ return getSaveEmbeddingIfHashMatchesStatement(db).run(memoryId, blob, modelId, memoryId, normalizedHash).changes > 0;
162107
+ }
161832
162108
  function loadAllEmbeddings(db, projectPath, modelId) {
161833
162109
  const rows = getLoadAllEmbeddingsStatement(db).all(projectPath, modelId).filter(isEmbeddingRow);
161834
162110
  const embeddings = new Map;
@@ -162760,12 +163036,12 @@ function isUnembeddedMemoryRow(row) {
162760
163036
  if (row === null || typeof row !== "object")
162761
163037
  return false;
162762
163038
  const candidate = row;
162763
- return typeof candidate.id === "number" && typeof candidate.content === "string";
163039
+ return typeof candidate.id === "number" && typeof candidate.content === "string" && typeof candidate.normalized_hash === "string";
162764
163040
  }
162765
163041
  function getLoadUnembeddedMemoriesStatement(db) {
162766
163042
  let stmt = loadUnembeddedMemoriesStatements.get(db);
162767
163043
  if (!stmt) {
162768
- stmt = db.prepare("SELECT m.id AS id, m.content AS content FROM memories m LEFT JOIN memory_embeddings me ON m.id = me.memory_id AND me.model_id = ? WHERE m.project_path = ? AND m.status = 'active' AND me.memory_id IS NULL LIMIT ?");
163044
+ stmt = db.prepare("SELECT m.id AS id, m.content AS content, m.normalized_hash AS normalized_hash FROM memories m LEFT JOIN memory_embeddings me ON m.id = me.memory_id AND me.model_id = ? WHERE m.project_path = ? AND m.status = 'active' AND me.memory_id IS NULL LIMIT ?");
162769
163045
  loadUnembeddedMemoriesStatements.set(db, stmt);
162770
163046
  }
162771
163047
  return stmt;
@@ -162788,8 +163064,9 @@ async function embedUnembeddedMemoriesForProject(db, projectIdentity, batchSize
162788
163064
  const embedding = result.vectors[index];
162789
163065
  if (!embedding)
162790
163066
  continue;
162791
- saveEmbedding(db, memory.id, embedding, result.modelId);
162792
- embeddedCount += 1;
163067
+ if (saveEmbeddingIfHashMatches(db, memory.id, embedding, result.modelId, memory.normalized_hash)) {
163068
+ embeddedCount += 1;
163069
+ }
162793
163070
  }
162794
163071
  })();
162795
163072
  return embeddedCount;
@@ -164295,12 +164572,19 @@ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal
164295
164572
  let totalTokens = 0;
164296
164573
  let messagesProcessed = 0;
164297
164574
  let lastOrdinal = startOrdinal - 1;
164575
+ let highestScannedOrdinal = startOrdinal - 1;
164298
164576
  let lastMessageId = "";
164299
164577
  let firstMessageId = "";
164300
164578
  let currentBlock = null;
164301
164579
  let pendingNoiseMeta = [];
164302
164580
  let commitClusters = 0;
164303
164581
  let lastFlushedRole = "";
164582
+ function recordFilteredNoise(meta3) {
164583
+ pendingNoiseMeta.push(meta3);
164584
+ if (!currentBlock) {
164585
+ highestScannedOrdinal = Math.max(highestScannedOrdinal, meta3.ordinal);
164586
+ }
164587
+ }
164304
164588
  function flushCurrentBlock() {
164305
164589
  if (!currentBlock)
164306
164590
  return true;
@@ -164316,6 +164600,7 @@ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal
164316
164600
  if (!firstMessageId)
164317
164601
  firstMessageId = currentBlock.meta[0]?.messageId ?? "";
164318
164602
  lastOrdinal = currentBlock.meta[currentBlock.meta.length - 1]?.ordinal ?? currentBlock.endOrdinal;
164603
+ highestScannedOrdinal = Math.max(highestScannedOrdinal, lastOrdinal);
164319
164604
  lastMessageId = currentBlock.meta[currentBlock.meta.length - 1]?.messageId ?? "";
164320
164605
  messagesProcessed += currentBlock.meta.length;
164321
164606
  lines.push(blockText);
@@ -164339,7 +164624,7 @@ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal
164339
164624
  if (msg.role === "user" && !hasMeaningfulUserText(msg.parts)) {
164340
164625
  const tcSummaries = extractToolCallSummaries(msg.parts);
164341
164626
  if (tcSummaries.length === 0) {
164342
- pendingNoiseMeta.push(meta3);
164627
+ recordFilteredNoise(meta3);
164343
164628
  continue;
164344
164629
  }
164345
164630
  const tcText = tcSummaries.join(" / ");
@@ -164371,7 +164656,7 @@ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal
164371
164656
  const compacted = compactTextForSummary(allParts.join(" / "), msg.role);
164372
164657
  const text = compacted.text;
164373
164658
  if (!text) {
164374
- pendingNoiseMeta.push(meta3);
164659
+ recordFilteredNoise(meta3);
164375
164660
  continue;
164376
164661
  }
164377
164662
  const msgHasNarrative = textParts.length > 0;
@@ -164398,7 +164683,9 @@ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal
164398
164683
  };
164399
164684
  pendingNoiseMeta = [];
164400
164685
  }
164401
- flushCurrentBlock();
164686
+ if (flushCurrentBlock() && pendingNoiseMeta.length > 0) {
164687
+ highestScannedOrdinal = Math.max(highestScannedOrdinal, pendingNoiseMeta[pendingNoiseMeta.length - 1]?.ordinal ?? highestScannedOrdinal);
164688
+ }
164402
164689
  const toolOnlyRanges = [];
164403
164690
  for (const range of flushedToolOnlyBlocks) {
164404
164691
  const last = toolOnlyRanges[toolOnlyRanges.length - 1];
@@ -164415,7 +164702,7 @@ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal
164415
164702
  endMessageId: lastMessageId,
164416
164703
  messageCount: messagesProcessed,
164417
164704
  tokenEstimate: totalTokens,
164418
- hasMore: lastOrdinal < (eligibleEndOrdinal !== undefined ? Math.min(eligibleEndOrdinal - 1, totalMessageCount) : totalMessageCount),
164705
+ hasMore: Math.max(lastOrdinal, highestScannedOrdinal) < (eligibleEndOrdinal !== undefined ? Math.min(eligibleEndOrdinal - 1, totalMessageCount) : totalMessageCount),
164419
164706
  text: lines.join(`
164420
164707
  `),
164421
164708
  lines: lineMeta,
@@ -168860,6 +169147,8 @@ function isMidTurnPi(event, _sessionId) {
168860
169147
  }
168861
169148
  if (latestAssistant === null)
168862
169149
  return false;
169150
+ if (hasRealUserAfter(messages, latestAssistantIndex))
169151
+ return false;
168863
169152
  if (latestAssistant.stopReason === "toolUse")
168864
169153
  return true;
168865
169154
  const toolCallIds = getToolCallIds(latestAssistant.content);
@@ -168882,6 +169171,15 @@ function isMidTurnPi(event, _sessionId) {
168882
169171
  }
168883
169172
  return false;
168884
169173
  }
169174
+ function hasRealUserAfter(messages, latestAssistantIndex) {
169175
+ for (const msg of messages.slice(latestAssistantIndex + 1)) {
169176
+ if (msg === null || typeof msg !== "object")
169177
+ continue;
169178
+ if (msg.role === "user")
169179
+ return true;
169180
+ }
169181
+ return false;
169182
+ }
168885
169183
  function getToolCallIds(content) {
168886
169184
  const ids = new Set;
168887
169185
  if (!Array.isArray(content))
@@ -169120,16 +169418,16 @@ function synthesizeToolResultParts(msg) {
169120
169418
  // src/tools/ctx-expand.ts
169121
169419
  var ParamsSchema = exports_typebox.Object({
169122
169420
  start: exports_typebox.Optional(exports_typebox.Number({
169123
- description: "Start message ordinal (from compartment start attribute)"
169421
+ description: `First message ordinal to expand — a compartment's start="N" attribute, or an ordinal from a ctx_search message hit`
169124
169422
  })),
169125
169423
  end: exports_typebox.Optional(exports_typebox.Number({
169126
- description: "End message ordinal (from compartment end attribute)"
169424
+ description: `Last message ordinal to expand (inclusive) — a compartment's end="M" attribute`
169127
169425
  })),
169128
169426
  verbose: exports_typebox.Optional(exports_typebox.Boolean({
169129
169427
  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."
169130
169428
  })),
169131
169429
  message: exports_typebox.Optional(exports_typebox.Number({
169132
- 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."
169430
+ description: "Full untruncated recovery of ONE message by its ordinal (every text part + every tool call's complete input/output). Use an ordinal from a compartment, ctx_search hit, or verbose range. Recovers a tool output you dropped with ctx_reduce."
169133
169431
  }))
169134
169432
  });
169135
169433
  function ok(text) {
@@ -169606,25 +169904,33 @@ function getMemoryCountsByStatusStatement(db) {
169606
169904
  }
169607
169905
  return stmt;
169608
169906
  }
169609
- function insertMemory(db, input) {
169610
- const now = Date.now();
169611
- const normalizedHash = computeNormalizedHash(input.content);
169907
+ function buildInsertMemoryValues(input, normalizedHash, now, includeImportance) {
169612
169908
  const insertValues = [
169613
169909
  input.projectPath,
169614
169910
  input.category,
169615
169911
  input.content,
169616
169912
  normalizedHash
169617
169913
  ];
169618
- if (hasMemoryImportanceColumn(db)) {
169914
+ if (includeImportance) {
169619
169915
  insertValues.push(input.importance ?? 50);
169620
169916
  }
169621
169917
  insertValues.push(input.sourceSessionId ?? null, input.sourceType ?? "historian", 1, 0, now, now, now, now, null, "active", input.expiresAt ?? null, "unverified", null, null, null, input.metadataJson ?? null);
169622
- const result = getInsertMemoryStatement(db).run(...insertValues);
169623
- const insertedResult = result;
169624
- const inserted = getMemoryById(db, Number(insertedResult.lastInsertRowid));
169918
+ return insertValues;
169919
+ }
169920
+ function loadInsertedMemory(db, rowid) {
169921
+ const inserted = getMemoryById(db, Number(rowid));
169625
169922
  if (!inserted) {
169626
169923
  throw new Error("Failed to load inserted memory row");
169627
169924
  }
169925
+ return inserted;
169926
+ }
169927
+ function insertMemory(db, input) {
169928
+ const now = Date.now();
169929
+ const normalizedHash = computeNormalizedHash(input.content);
169930
+ const insertValues = buildInsertMemoryValues(input, normalizedHash, now, hasMemoryImportanceColumn(db));
169931
+ const result = getInsertMemoryStatement(db).run(...insertValues);
169932
+ const insertedResult = result;
169933
+ const inserted = loadInsertedMemory(db, insertedResult.lastInsertRowid);
169628
169934
  invalidateProject(input.projectPath);
169629
169935
  return inserted;
169630
169936
  }
@@ -170105,9 +170411,11 @@ import path4 from "node:path";
170105
170411
  import { promisify } from "node:util";
170106
170412
  var execFileAsync = promisify(execFile);
170107
170413
  var GIT_TIMEOUT_MS2 = 1e4;
170414
+ var defaultExecFileForVerificationPaths = async (file2, args, options) => await execFileAsync(file2, [...args], options);
170415
+ var execFileForVerificationPaths = defaultExecFileForVerificationPaths;
170108
170416
  async function runGit(cwd, args) {
170109
170417
  try {
170110
- const result = await execFileAsync("git", [...args], {
170418
+ const result = await execFileForVerificationPaths("git", args, {
170111
170419
  cwd,
170112
170420
  timeout: GIT_TIMEOUT_MS2,
170113
170421
  maxBuffer: 16 * 1024 * 1024,
@@ -170291,7 +170599,10 @@ async function normalizeVerificationFiles(args) {
170291
170599
  var lastIndexedStatements = new WeakMap;
170292
170600
  var insertMessageStatements = new WeakMap;
170293
170601
  var upsertIndexStatements = new WeakMap;
170602
+ var upsertCleanIndexStatements = new WeakMap;
170603
+ var upsertDirtyFloorStatements = new WeakMap;
170294
170604
  var deleteFtsStatements = new WeakMap;
170605
+ var deleteFtsFromOrdinalStatements = new WeakMap;
170295
170606
  var deleteIndexStatements = new WeakMap;
170296
170607
  var countIndexedMessageStatements = new WeakMap;
170297
170608
  function normalizeIndexText(text) {
@@ -170300,7 +170611,7 @@ function normalizeIndexText(text) {
170300
170611
  function getLastIndexedStatement(db) {
170301
170612
  let stmt = lastIndexedStatements.get(db);
170302
170613
  if (!stmt) {
170303
- stmt = db.prepare("SELECT last_indexed_ordinal FROM message_history_index WHERE session_id = ?");
170614
+ stmt = db.prepare("SELECT last_indexed_ordinal, dirty_floor_ordinal FROM message_history_index WHERE session_id = ?");
170304
170615
  lastIndexedStatements.set(db, stmt);
170305
170616
  }
170306
170617
  return stmt;
@@ -170321,6 +170632,22 @@ function getUpsertIndexStatement(db) {
170321
170632
  }
170322
170633
  return stmt;
170323
170634
  }
170635
+ function getUpsertCleanIndexStatement(db) {
170636
+ let stmt = upsertCleanIndexStatements.get(db);
170637
+ if (!stmt) {
170638
+ stmt = db.prepare("INSERT INTO message_history_index (session_id, last_indexed_ordinal, dirty_floor_ordinal, updated_at, harness) VALUES (?, ?, 0, ?, ?) ON CONFLICT(session_id) DO UPDATE SET last_indexed_ordinal = excluded.last_indexed_ordinal, dirty_floor_ordinal = 0, updated_at = excluded.updated_at");
170639
+ upsertCleanIndexStatements.set(db, stmt);
170640
+ }
170641
+ return stmt;
170642
+ }
170643
+ function getUpsertDirtyFloorStatement(db) {
170644
+ let stmt = upsertDirtyFloorStatements.get(db);
170645
+ if (!stmt) {
170646
+ stmt = db.prepare("INSERT INTO message_history_index (session_id, last_indexed_ordinal, dirty_floor_ordinal, updated_at, harness) VALUES (?, ?, ?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET last_indexed_ordinal = MAX(message_history_index.last_indexed_ordinal, excluded.last_indexed_ordinal), dirty_floor_ordinal = CASE WHEN message_history_index.dirty_floor_ordinal <= 0 THEN excluded.dirty_floor_ordinal WHEN excluded.dirty_floor_ordinal <= 0 THEN message_history_index.dirty_floor_ordinal ELSE MIN(message_history_index.dirty_floor_ordinal, excluded.dirty_floor_ordinal) END, updated_at = excluded.updated_at");
170647
+ upsertDirtyFloorStatements.set(db, stmt);
170648
+ }
170649
+ return stmt;
170650
+ }
170324
170651
  function getDeleteFtsStatement(db) {
170325
170652
  let stmt = deleteFtsStatements.get(db);
170326
170653
  if (!stmt) {
@@ -170329,6 +170656,14 @@ function getDeleteFtsStatement(db) {
170329
170656
  }
170330
170657
  return stmt;
170331
170658
  }
170659
+ function getDeleteFtsFromOrdinalStatement(db) {
170660
+ let stmt = deleteFtsFromOrdinalStatements.get(db);
170661
+ if (!stmt) {
170662
+ stmt = db.prepare("DELETE FROM message_history_fts WHERE session_id = ? AND CAST(message_ordinal AS INTEGER) >= ?");
170663
+ deleteFtsFromOrdinalStatements.set(db, stmt);
170664
+ }
170665
+ return stmt;
170666
+ }
170332
170667
  function getDeleteIndexStatement(db) {
170333
170668
  let stmt = deleteIndexStatements.get(db);
170334
170669
  if (!stmt) {
@@ -170349,6 +170684,14 @@ function getLastIndexedOrdinal(db, sessionId) {
170349
170684
  const row = getLastIndexedStatement(db).get(sessionId);
170350
170685
  return typeof row?.last_indexed_ordinal === "number" ? row.last_indexed_ordinal : 0;
170351
170686
  }
170687
+ function getDirtyIndexFloor(db, sessionId) {
170688
+ const row = getLastIndexedStatement(db).get(sessionId);
170689
+ return typeof row?.dirty_floor_ordinal === "number" && row.dirty_floor_ordinal > 0 ? row.dirty_floor_ordinal : null;
170690
+ }
170691
+ function markMessageIndexDirty(db, sessionId, floorOrdinal) {
170692
+ const dirtyFloor = Math.max(1, Math.floor(floorOrdinal));
170693
+ getUpsertDirtyFloorStatement(db).run(sessionId, getLastIndexedOrdinal(db, sessionId), dirtyFloor, Date.now(), getHarness());
170694
+ }
170352
170695
  function isMessageAlreadyIndexed(db, sessionId, messageId) {
170353
170696
  const row = getCountIndexedMessageStatement(db).get(sessionId, messageId);
170354
170697
  return (typeof row?.count === "number" ? row.count : 0) > 0;
@@ -170416,7 +170759,15 @@ function indexMessagesAfterOrdinal(db, sessionId, messages, lastIndexedOrdinal,
170416
170759
  db.exec("BEGIN IMMEDIATE");
170417
170760
  let committed = false;
170418
170761
  try {
170419
- const effectiveWatermark = Math.max(lastIndexedOrdinal, getLastIndexedOrdinal(db, sessionId));
170762
+ let effectiveWatermark = Math.max(lastIndexedOrdinal, getLastIndexedOrdinal(db, sessionId));
170763
+ const dirtyFloor = getDirtyIndexFloor(db, sessionId);
170764
+ if (dirtyFloor !== null) {
170765
+ const rewindOrdinal = Math.max(1, Math.min(dirtyFloor, finalWatermark + 1));
170766
+ if (rewindOrdinal <= finalWatermark) {
170767
+ getDeleteFtsFromOrdinalStatement(db).run(sessionId, rewindOrdinal);
170768
+ }
170769
+ effectiveWatermark = Math.min(effectiveWatermark, rewindOrdinal - 1);
170770
+ }
170420
170771
  const insertMessage = getInsertMessageStatement(db);
170421
170772
  for (const message of messages) {
170422
170773
  if (message.ordinal <= effectiveWatermark) {
@@ -170433,7 +170784,7 @@ function indexMessagesAfterOrdinal(db, sessionId, messages, lastIndexedOrdinal,
170433
170784
  inserted++;
170434
170785
  }
170435
170786
  const newWatermark = Math.max(effectiveWatermark, finalWatermark);
170436
- getUpsertIndexStatement(db).run(sessionId, newWatermark, now, getHarness());
170787
+ getUpsertCleanIndexStatement(db).run(sessionId, newWatermark, now, getHarness());
170437
170788
  db.exec("COMMIT");
170438
170789
  committed = true;
170439
170790
  } finally {
@@ -171204,7 +171555,7 @@ function setChannel2NudgeState(db, sessionId, state) {
171204
171555
  db.transaction(() => {
171205
171556
  ensureSessionMetaRow(db, sessionId);
171206
171557
  const claimedAt = state === "claimed" ? Date.now() : 0;
171207
- db.prepare("UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ? WHERE session_id = ?").run(state, claimedAt, sessionId);
171558
+ db.prepare("UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ?, channel2_nudge_claim_token = '' WHERE session_id = ?").run(state, claimedAt, sessionId);
171208
171559
  })();
171209
171560
  }
171210
171561
  function casChannel2NudgeState(db, sessionId, from, to) {
@@ -171212,7 +171563,7 @@ function casChannel2NudgeState(db, sessionId, from, to) {
171212
171563
  db.transaction(() => {
171213
171564
  ensureSessionMetaRow(db, sessionId);
171214
171565
  const claimedAt = to === "claimed" ? Date.now() : 0;
171215
- const result = db.prepare("UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ? WHERE session_id = ? AND channel2_nudge_state = ?").run(to, claimedAt, sessionId, from);
171566
+ const result = db.prepare("UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ?, channel2_nudge_claim_token = '' WHERE session_id = ? AND channel2_nudge_state = ?").run(to, claimedAt, sessionId, from);
171216
171567
  changed = (result.changes ?? 0) > 0;
171217
171568
  })();
171218
171569
  return changed;
@@ -171620,6 +171971,13 @@ function setSessionWorkMetrics(db, sessionId, newWorkTokens, totalInputTokens) {
171620
171971
  SET new_work_tokens = ?, total_input_tokens = ?
171621
171972
  WHERE session_id = ?`).run(Math.max(0, Math.floor(newWorkTokens)), Math.max(0, Math.floor(totalInputTokens)), sessionId);
171622
171973
  }
171974
+ function getSessionWorkMetrics(db, sessionId) {
171975
+ const row = db.prepare("SELECT new_work_tokens, total_input_tokens FROM session_meta WHERE session_id = ?").get(sessionId);
171976
+ return {
171977
+ newWorkTokens: typeof row?.new_work_tokens === "number" ? row.new_work_tokens : 0,
171978
+ totalInputTokens: typeof row?.total_input_tokens === "number" ? row.total_input_tokens : 0
171979
+ };
171980
+ }
171623
171981
  // ../plugin/src/features/magic-context/storage-meta-session.ts
171624
171982
  import { Buffer as Buffer3 } from "node:buffer";
171625
171983
 
@@ -172941,25 +173299,45 @@ function formatMemoryList(memories) {
172941
173299
  id: String(m.id),
172942
173300
  category: m.category,
172943
173301
  status: m.status,
173302
+ verification: m.verificationStatus,
172944
173303
  updated: new Date(m.updatedAt).toISOString(),
172945
173304
  content: m.content.replace(/\s+/g, " ").trim()
172946
173305
  }));
173306
+ const headers = {
173307
+ id: "ID",
173308
+ category: "CATEGORY",
173309
+ status: "STATUS",
173310
+ verification: "VERIFY",
173311
+ updated: "UPDATED",
173312
+ content: "CONTENT"
173313
+ };
172947
173314
  const widths = {
172948
- id: Math.max(2, ...rows.map((r) => r.id.length)),
172949
- category: Math.max(8, ...rows.map((r) => r.category.length)),
172950
- status: Math.max(6, ...rows.map((r) => r.status.length)),
172951
- updated: Math.max(7, ...rows.map((r) => r.updated.length))
173315
+ id: Math.max(headers.id.length, ...rows.map((r) => r.id.length)),
173316
+ category: Math.max(headers.category.length, ...rows.map((r) => r.category.length)),
173317
+ status: Math.max(headers.status.length, ...rows.map((r) => r.status.length)),
173318
+ verification: Math.max(headers.verification.length, ...rows.map((r) => r.verification.length)),
173319
+ updated: Math.max(headers.updated.length, ...rows.map((r) => r.updated.length))
172952
173320
  };
172953
173321
  const fmt = (r) => [
172954
173322
  r.id.padEnd(widths.id),
172955
173323
  r.category.padEnd(widths.category),
172956
173324
  r.status.padEnd(widths.status),
173325
+ r.verification.padEnd(widths.verification),
172957
173326
  r.updated.padEnd(widths.updated),
172958
173327
  r.content
172959
173328
  ].join(" | ");
172960
173329
  return [
172961
173330
  `Found ${rows.length} active ${rows.length === 1 ? "memory" : "memories"}:`,
172962
173331
  "",
173332
+ fmt(headers),
173333
+ [
173334
+ "-".repeat(widths.id),
173335
+ "-".repeat(widths.category),
173336
+ "-".repeat(widths.status),
173337
+ "-".repeat(widths.verification),
173338
+ "-".repeat(widths.updated),
173339
+ "-------"
173340
+ ].join("-+-"),
172963
173341
  ...rows.map(fmt)
172964
173342
  ].join(`
172965
173343
  `);
@@ -173029,8 +173407,9 @@ function createCtxMemoryTool(deps) {
173029
173407
  const isOwn = targetIdentityForStoredPath(memory2.projectPath) === projectIdentity;
173030
173408
  if (isOwn)
173031
173409
  return true;
173032
- return toolShareCategories === null || toolShareCategories.includes(memory2.category);
173410
+ return toolShareCategories?.includes(memory2.category) ?? false;
173033
173411
  };
173412
+ const memoryOwnedByTool = (memory2) => workspaceIdentitySet.identities.length > 1 ? targetIdentityForStoredPath(memory2.projectPath) === projectIdentity : storedPathBelongsToIdentity(memory2.projectPath, projectIdentity);
173034
173413
  const snapshot = getProjectEmbeddingSnapshot(projectIdentity);
173035
173414
  if (snapshot ? !snapshot.features.memoryEnabled : deps.memoryEnabled === false) {
173036
173415
  return err2("Cross-session memory is disabled for this project.");
@@ -173077,7 +173456,8 @@ function createCtxMemoryTool(deps) {
173077
173456
  return err2("Error: 'content' is required when action is 'update'.");
173078
173457
  }
173079
173458
  const memory2 = getMemoryById(deps.db, updateId);
173080
- if (!memory2 || !memoryVisibleToTool(memory2)) {
173459
+ const updateAllowed = memory2 ? dreamerAllowed ? memoryVisibleToTool(memory2) : memoryOwnedByTool(memory2) : false;
173460
+ if (!memory2 || !updateAllowed) {
173081
173461
  return err2(`Error: Memory with ID ${updateId} was not found.`);
173082
173462
  }
173083
173463
  if (!dreamerAllowed && !isPrimaryMutableMemory(memory2)) {
@@ -173124,7 +173504,7 @@ function createCtxMemoryTool(deps) {
173124
173504
  return err2("Error: One or more source memories were not found.");
173125
173505
  }
173126
173506
  if (!dreamerAllowed) {
173127
- const foreign = sourceMemories.find((memory2) => !memoryVisibleToTool(memory2));
173507
+ const foreign = sourceMemories.find((memory2) => !memoryOwnedByTool(memory2));
173128
173508
  if (foreign) {
173129
173509
  return err2(`Error: Memory with ID ${foreign.id} was not found.`);
173130
173510
  }
@@ -173226,7 +173606,8 @@ function createCtxMemoryTool(deps) {
173226
173606
  const archiveIds = [...new Set(rawArchiveIds)];
173227
173607
  for (const memoryId of archiveIds) {
173228
173608
  const memory2 = getMemoryById(deps.db, memoryId);
173229
- if (!memory2 || !memoryVisibleToTool(memory2)) {
173609
+ const archiveAllowed = memory2 ? dreamerAllowed ? memoryVisibleToTool(memory2) : memoryOwnedByTool(memory2) : false;
173610
+ if (!memory2 || !archiveAllowed) {
173230
173611
  return err2(`Error: Memory with ID ${memoryId} was not found.`);
173231
173612
  }
173232
173613
  if (!dreamerAllowed && !isPrimaryMutableMemory(memory2)) {
@@ -173365,6 +173746,7 @@ function createCtxNoteTool(deps) {
173365
173746
  parameters: ParamsSchema3,
173366
173747
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
173367
173748
  const sessionId = ctx.sessionManager.getSessionId();
173749
+ const dreamerEnabled = deps.resolveDreamerEnabled?.(ctx) ?? deps.dreamerEnabled;
173368
173750
  const action2 = params.action ?? (params.content?.trim() ? "write" : "read");
173369
173751
  if (action2 === "write") {
173370
173752
  const content = params.content?.trim();
@@ -173373,7 +173755,7 @@ function createCtxNoteTool(deps) {
173373
173755
  const anchorOrdinal = captureAnchorOrdinal(deps.db, sessionId);
173374
173756
  const surfaceCondition = params.surface_condition?.trim();
173375
173757
  if (surfaceCondition) {
173376
- if (deps.dreamerEnabled !== true) {
173758
+ if (dreamerEnabled !== true) {
173377
173759
  return err3("Error: Smart notes require dreamer to be enabled. Enable dreamer in magic-context.jsonc to use surface_condition.");
173378
173760
  }
173379
173761
  const projectIdentity = resolveProjectIdentity(ctx.cwd);
@@ -173382,6 +173764,7 @@ function createCtxNoteTool(deps) {
173382
173764
  }
173383
173765
  const note2 = addNote(deps.db, "smart", {
173384
173766
  content,
173767
+ sessionId,
173385
173768
  projectPath: projectIdentity,
173386
173769
  surfaceCondition,
173387
173770
  anchorOrdinal
@@ -173633,6 +174016,7 @@ function createCtxReduceTool(deps) {
173633
174016
  parameters: ParamsSchema4,
173634
174017
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
173635
174018
  const sessionId = ctx.sessionManager.getSessionId();
174019
+ const protectedTags = Math.max(0, Math.floor(deps.resolveProtectedTags?.(ctx) ?? deps.protectedTags));
173636
174020
  if (!params.drop) {
173637
174021
  return err4("Error: 'drop' must be provided.");
173638
174022
  }
@@ -173650,7 +174034,7 @@ function createCtxReduceTool(deps) {
173650
174034
  return err4(`Error: Unknown tag(s) ${formatIds(unknownIds)}. Check available tags in conversation.`);
173651
174035
  }
173652
174036
  const activeTags = allTags.filter((tag) => tag.status === "active");
173653
- const protectedTagIds = activeTags.map((tag) => tag.tagNumber).sort((left, right) => right - left).slice(0, deps.protectedTags);
174037
+ const protectedTagIds = activeTags.map((tag) => tag.tagNumber).sort((left, right) => right - left).slice(0, protectedTags);
173654
174038
  const protectedSet = new Set(protectedTagIds);
173655
174039
  const tagStatusMap = new Map(allTags.map((tag) => [tag.tagNumber, tag.status]));
173656
174040
  const pendingOps = getPendingOps(deps.db, sessionId);
@@ -174176,24 +174560,46 @@ function getMessageSearchStatementWithCutoff(db) {
174176
174560
  return stmt;
174177
174561
  }
174178
174562
  var ftsRowCountStatements = new WeakMap;
174563
+ var ftsRowCountStatementsWithCutoff = new WeakMap;
174179
174564
  var ftsMatchCountStatements = new WeakMap;
174180
- function getSessionFtsRowCount(db, sessionId) {
174181
- let stmt = ftsRowCountStatements.get(db);
174182
- if (!stmt) {
174183
- stmt = db.prepare("SELECT COUNT(*) AS n FROM message_history_fts WHERE session_id = ?");
174184
- ftsRowCountStatements.set(db, stmt);
174185
- }
174186
- const row = stmt.get(sessionId);
174565
+ var ftsMatchCountStatementsWithCutoff = new WeakMap;
174566
+ function getSessionFtsRowCount(db, sessionId, cutoff) {
174567
+ const stmt = cutoff === null ? (() => {
174568
+ let cached2 = ftsRowCountStatements.get(db);
174569
+ if (!cached2) {
174570
+ cached2 = db.prepare("SELECT COUNT(*) AS n FROM message_history_fts WHERE session_id = ?");
174571
+ ftsRowCountStatements.set(db, cached2);
174572
+ }
174573
+ return cached2;
174574
+ })() : (() => {
174575
+ let cached2 = ftsRowCountStatementsWithCutoff.get(db);
174576
+ if (!cached2) {
174577
+ cached2 = db.prepare("SELECT COUNT(*) AS n FROM message_history_fts WHERE session_id = ? AND CAST(message_ordinal AS INTEGER) <= ?");
174578
+ ftsRowCountStatementsWithCutoff.set(db, cached2);
174579
+ }
174580
+ return cached2;
174581
+ })();
174582
+ const row = cutoff === null ? stmt.get(sessionId) : stmt.get(sessionId, cutoff);
174187
174583
  return typeof row?.n === "number" ? row.n : 0;
174188
174584
  }
174189
- function countSessionFtsMatches(db, sessionId, ftsQuery) {
174190
- let stmt = ftsMatchCountStatements.get(db);
174191
- if (!stmt) {
174192
- stmt = db.prepare("SELECT COUNT(*) AS n FROM message_history_fts WHERE session_id = ? AND message_history_fts MATCH ?");
174193
- ftsMatchCountStatements.set(db, stmt);
174194
- }
174585
+ function countSessionFtsMatches(db, sessionId, ftsQuery, cutoff) {
174586
+ const stmt = cutoff === null ? (() => {
174587
+ let cached2 = ftsMatchCountStatements.get(db);
174588
+ if (!cached2) {
174589
+ cached2 = db.prepare("SELECT COUNT(*) AS n FROM message_history_fts WHERE session_id = ? AND message_history_fts MATCH ?");
174590
+ ftsMatchCountStatements.set(db, cached2);
174591
+ }
174592
+ return cached2;
174593
+ })() : (() => {
174594
+ let cached2 = ftsMatchCountStatementsWithCutoff.get(db);
174595
+ if (!cached2) {
174596
+ cached2 = db.prepare("SELECT COUNT(*) AS n FROM message_history_fts WHERE session_id = ? AND message_history_fts MATCH ? AND CAST(message_ordinal AS INTEGER) <= ?");
174597
+ ftsMatchCountStatementsWithCutoff.set(db, cached2);
174598
+ }
174599
+ return cached2;
174600
+ })();
174195
174601
  try {
174196
- const row = stmt.get(sessionId, ftsQuery);
174602
+ const row = cutoff === null ? stmt.get(sessionId, ftsQuery) : stmt.get(sessionId, ftsQuery, cutoff);
174197
174603
  return typeof row?.n === "number" ? row.n : 0;
174198
174604
  } catch {
174199
174605
  return 0;
@@ -174443,7 +174849,7 @@ function searchMessages(args) {
174443
174849
  role: row.role
174444
174850
  }));
174445
174851
  }
174446
- const corpusSize = getSessionFtsRowCount(args.db, args.sessionId);
174852
+ const corpusSize = getSessionFtsRowCount(args.db, args.sessionId, cutoff);
174447
174853
  const queryLists = [];
174448
174854
  if (baseQuery.length > 0) {
174449
174855
  queryLists.push({
@@ -174456,7 +174862,7 @@ function searchMessages(args) {
174456
174862
  const probeQuery = sanitizeFtsQuery(probe);
174457
174863
  if (probeQuery.length === 0)
174458
174864
  continue;
174459
- const df = countSessionFtsMatches(args.db, args.sessionId, probeQuery);
174865
+ const df = countSessionFtsMatches(args.db, args.sessionId, probeQuery, cutoff);
174460
174866
  const weight = probeDiscriminationWeight(df, corpusSize);
174461
174867
  probeWeights.set(probe, weight);
174462
174868
  queryLists.push({
@@ -175627,18 +176033,539 @@ function createCtxSearchTool(deps) {
175627
176033
  };
175628
176034
  }
175629
176035
 
175630
- // src/tools/todowrite.ts
175631
- var STATUS_VALUES = [
175632
- "pending",
175633
- "in_progress",
175634
- "completed",
175635
- "cancelled"
176036
+ // src/tools/todo-view-pi.ts
176037
+ import {
176038
+ truncateToWidth
176039
+ } from "@earendil-works/pi-tui";
176040
+
176041
+ // ../plugin/src/hooks/magic-context/todo-view.ts
176042
+ import { createHash as createHash8 } from "node:crypto";
176043
+ var TODO_STATUS_PENDING = "pending";
176044
+ var TODO_STATUS_IN_PROGRESS = "in_progress";
176045
+ var TODO_STATUS_COMPLETED = "completed";
176046
+ var TODO_STATUS_CANCELLED = "cancelled";
176047
+ var TODO_PRIORITY_HIGH = "high";
176048
+ var TODO_PRIORITY_MEDIUM = "medium";
176049
+ var TODO_PRIORITY_LOW = "low";
176050
+ var TODO_STATUSES = [
176051
+ TODO_STATUS_PENDING,
176052
+ TODO_STATUS_IN_PROGRESS,
176053
+ TODO_STATUS_COMPLETED,
176054
+ TODO_STATUS_CANCELLED
176055
+ ];
176056
+ var TODO_PRIORITIES = [
176057
+ TODO_PRIORITY_HIGH,
176058
+ TODO_PRIORITY_MEDIUM,
176059
+ TODO_PRIORITY_LOW
175636
176060
  ];
175637
- var PRIORITY_VALUES = ["high", "medium", "low"];
176061
+ var TODO_STATUS_SET = new Set(TODO_STATUSES);
176062
+ var TODO_PRIORITY_SET = new Set(TODO_PRIORITIES);
176063
+ var TERMINAL_STATUSES = new Set([
176064
+ TODO_STATUS_COMPLETED,
176065
+ TODO_STATUS_CANCELLED
176066
+ ]);
176067
+ var TITLE_DONE_STATUSES = new Set([TODO_STATUS_COMPLETED]);
176068
+ var SYNTHETIC_CALL_ID_PREFIX = "mc_synthetic_todo_";
176069
+ function normalizeTodoStateJson(todos) {
176070
+ if (!Array.isArray(todos))
176071
+ return null;
176072
+ const normalized = [];
176073
+ for (const todo of todos) {
176074
+ if (!isTodoItem(todo))
176075
+ return null;
176076
+ normalized.push({
176077
+ content: todo.content,
176078
+ status: todo.status,
176079
+ priority: todo.priority ?? TODO_PRIORITY_MEDIUM
176080
+ });
176081
+ }
176082
+ return JSON.stringify(normalized);
176083
+ }
176084
+ function buildSyntheticTodoPart(stateJson) {
176085
+ const todos = parseTodoState(stateJson);
176086
+ if (todos === null || todos.length === 0)
176087
+ return null;
176088
+ if (todos.every((t) => TERMINAL_STATUSES.has(t.status)))
176089
+ return null;
176090
+ const callID = computeSyntheticCallId(stateJson);
176091
+ const activeCount = todos.filter((t) => !TITLE_DONE_STATUSES.has(t.status)).length;
176092
+ const output = JSON.stringify(todos, null, 2);
176093
+ const ts = 0;
176094
+ return {
176095
+ type: "tool",
176096
+ callID,
176097
+ tool: "todowrite",
176098
+ state: {
176099
+ status: "completed",
176100
+ input: { todos },
176101
+ output,
176102
+ title: `${activeCount} todos`,
176103
+ metadata: { todos, truncated: false },
176104
+ time: { start: ts, end: ts }
176105
+ },
176106
+ syntheticTodoMarker: true
176107
+ };
176108
+ }
176109
+ function computeSyntheticCallId(stateJson) {
176110
+ const hash2 = createHash8("sha256").update(stateJson).digest("hex").slice(0, 16);
176111
+ return `${SYNTHETIC_CALL_ID_PREFIX}${hash2}`;
176112
+ }
176113
+ function parseTodoState(stateJson) {
176114
+ if (stateJson.length === 0)
176115
+ return null;
176116
+ try {
176117
+ const parsed = JSON.parse(stateJson);
176118
+ if (!Array.isArray(parsed))
176119
+ return null;
176120
+ const result = [];
176121
+ for (const item of parsed) {
176122
+ if (!isTodoItem(item))
176123
+ return null;
176124
+ result.push({
176125
+ content: item.content,
176126
+ status: item.status,
176127
+ priority: item.priority ?? TODO_PRIORITY_MEDIUM
176128
+ });
176129
+ }
176130
+ return result;
176131
+ } catch {
176132
+ return null;
176133
+ }
176134
+ }
176135
+ function isTodoStatus(value) {
176136
+ return typeof value === "string" && TODO_STATUS_SET.has(value);
176137
+ }
176138
+ function isTodoPriority(value) {
176139
+ return typeof value === "string" && TODO_PRIORITY_SET.has(value);
176140
+ }
176141
+ function isTodoItem(value) {
176142
+ if (value === null || typeof value !== "object")
176143
+ return false;
176144
+ const todo = value;
176145
+ return typeof todo.content === "string" && isTodoStatus(todo.status) && (todo.priority === undefined || isTodoPriority(todo.priority));
176146
+ }
176147
+
176148
+ // src/tools/todo-view-pi.ts
176149
+ var TODO_TOOL_NAME = "todowrite";
176150
+ var TODOS_COMMAND_NAME = "todos";
176151
+ var WIDGET_KEY = "magic-context-todos";
176152
+ var MAX_TODO_CONTENT_ROWS = 12;
176153
+ var STATUS_GLYPH = {
176154
+ pending: "○",
176155
+ in_progress: "◐",
176156
+ completed: "✓",
176157
+ cancelled: "✗"
176158
+ };
176159
+ var STATUS_COLOR = {
176160
+ pending: "dim",
176161
+ in_progress: "warning",
176162
+ completed: "success",
176163
+ cancelled: "error"
176164
+ };
176165
+ var snapshotsBySession = new Map;
176166
+ function isTodoStatus2(value) {
176167
+ return TODO_STATUSES.includes(value);
176168
+ }
176169
+ function isTodoPriority2(value) {
176170
+ return TODO_PRIORITIES.includes(value);
176171
+ }
176172
+ function cloneTodos(todos) {
176173
+ return todos.map((todo) => ({ ...todo }));
176174
+ }
176175
+ function parseTodos(input) {
176176
+ if (!Array.isArray(input))
176177
+ return null;
176178
+ const todos = [];
176179
+ for (const item of input) {
176180
+ if (item === null || typeof item !== "object")
176181
+ return null;
176182
+ const raw = item;
176183
+ if (typeof raw.content !== "string" || !isTodoStatus2(raw.status)) {
176184
+ return null;
176185
+ }
176186
+ const todo = {
176187
+ content: raw.content,
176188
+ status: raw.status
176189
+ };
176190
+ if (isTodoPriority2(raw.priority))
176191
+ todo.priority = raw.priority;
176192
+ if (typeof raw.id === "string" && raw.id.length > 0)
176193
+ todo.id = raw.id;
176194
+ todos.push(todo);
176195
+ }
176196
+ return todos;
176197
+ }
176198
+ function parseTodoStateJson(stateJson) {
176199
+ if (!stateJson)
176200
+ return null;
176201
+ try {
176202
+ return parseTodos(JSON.parse(stateJson));
176203
+ } catch {
176204
+ return null;
176205
+ }
176206
+ }
176207
+ function setTodoSnapshot(sessionId, todos) {
176208
+ const parsed = parseTodos(todos);
176209
+ if (parsed === null)
176210
+ return false;
176211
+ snapshotsBySession.set(sessionId, { todos: parsed });
176212
+ return true;
176213
+ }
176214
+ function seedTodoSnapshotFromStateJson(sessionId, stateJson) {
176215
+ const parsed = parseTodoStateJson(stateJson);
176216
+ if (parsed === null) {
176217
+ snapshotsBySession.delete(sessionId);
176218
+ return false;
176219
+ }
176220
+ snapshotsBySession.set(sessionId, { todos: parsed });
176221
+ return true;
176222
+ }
176223
+ function getTodoSnapshot(sessionId) {
176224
+ if (!sessionId)
176225
+ return { todos: [] };
176226
+ const snapshot = snapshotsBySession.get(sessionId);
176227
+ return { todos: snapshot ? cloneTodos(snapshot.todos) : [] };
176228
+ }
176229
+ function clearTodoSnapshot(sessionId) {
176230
+ snapshotsBySession.delete(sessionId);
176231
+ }
176232
+ function getSessionId(ctx) {
176233
+ try {
176234
+ const id = ctx.sessionManager?.getSessionId?.();
176235
+ return typeof id === "string" && id.length > 0 ? id : undefined;
176236
+ } catch {
176237
+ return;
176238
+ }
176239
+ }
176240
+ function countTodos(todos) {
176241
+ const counts = {
176242
+ total: todos.length,
176243
+ pending: 0,
176244
+ inProgress: 0,
176245
+ completed: 0,
176246
+ cancelled: 0
176247
+ };
176248
+ for (const todo of todos) {
176249
+ switch (todo.status) {
176250
+ case "pending":
176251
+ counts.pending++;
176252
+ break;
176253
+ case "in_progress":
176254
+ counts.inProgress++;
176255
+ break;
176256
+ case "completed":
176257
+ counts.completed++;
176258
+ break;
176259
+ case "cancelled":
176260
+ counts.cancelled++;
176261
+ break;
176262
+ }
176263
+ }
176264
+ return counts;
176265
+ }
176266
+ function activeTitleCount(todos) {
176267
+ return todos.filter((todo) => !TITLE_DONE_STATUSES.has(todo.status)).length;
176268
+ }
176269
+ function formatCounts(counts) {
176270
+ if (counts.total === 0)
176271
+ return "No todos";
176272
+ const parts = [`${counts.completed}/${counts.total} completed`];
176273
+ if (counts.inProgress > 0)
176274
+ parts.push(`${counts.inProgress} in progress`);
176275
+ if (counts.pending > 0)
176276
+ parts.push(`${counts.pending} pending`);
176277
+ if (counts.cancelled > 0)
176278
+ parts.push(`${counts.cancelled} cancelled`);
176279
+ return parts.join(" · ");
176280
+ }
176281
+ function formatTodoLine(todo, theme, options = {}) {
176282
+ const glyph = theme.fg(STATUS_COLOR[todo.status], STATUS_GLYPH[todo.status]);
176283
+ const id = options.showId && todo.id ? `${theme.fg("accent", `#${todo.id}`)} ` : "";
176284
+ const color = todo.status === "pending" || todo.status === "in_progress" ? "text" : "dim";
176285
+ let content = theme.fg(color, todo.content);
176286
+ if (todo.status === "completed" || todo.status === "cancelled") {
176287
+ content = theme.strikethrough(content);
176288
+ }
176289
+ return `${glyph} ${id}${content}`;
176290
+ }
176291
+ function formatCommandLine(todo) {
176292
+ const id = todo.id ? `#${todo.id} ` : "";
176293
+ return ` ${STATUS_GLYPH[todo.status]} ${id}${todo.content}`;
176294
+ }
176295
+ function lineComponent(renderLines) {
176296
+ return {
176297
+ render: renderLines,
176298
+ invalidate() {}
176299
+ };
176300
+ }
176301
+ function capTodoRows(rows) {
176302
+ if (rows.length <= MAX_TODO_CONTENT_ROWS) {
176303
+ return { visible: [...rows], hiddenCount: 0 };
176304
+ }
176305
+ const visibleCount = Math.max(0, MAX_TODO_CONTENT_ROWS - 1);
176306
+ return {
176307
+ visible: rows.slice(0, visibleCount),
176308
+ hiddenCount: rows.length - visibleCount
176309
+ };
176310
+ }
176311
+ function renderTodowriteCall(args, theme) {
176312
+ const todos = parseTodos(args.todos) ?? [];
176313
+ const active = activeTitleCount(todos);
176314
+ const activeColor = active > 0 ? "warning" : "success";
176315
+ const line = `${theme.fg("toolTitle", theme.bold("Todos"))} ${theme.fg("muted", "—")} ${theme.fg(activeColor, `${active} active`)}`;
176316
+ return lineComponent((width) => [truncateToWidth(line, width, "…")]);
176317
+ }
176318
+ function renderTodowriteResult(result, theme) {
176319
+ const detailsTodos = result.details?.todos;
176320
+ let todos = parseTodos(detailsTodos);
176321
+ if (todos === null && Array.isArray(result.content)) {
176322
+ const text = result.content.find((part) => part !== null && typeof part === "object" && part.type === "text" && typeof part.text === "string")?.text;
176323
+ if (text) {
176324
+ try {
176325
+ todos = parseTodos(JSON.parse(text));
176326
+ } catch {}
176327
+ }
176328
+ }
176329
+ const renderedTodos = todos ?? [];
176330
+ return lineComponent((width) => {
176331
+ if (renderedTodos.length === 0)
176332
+ return [theme.fg("dim", "No todos")];
176333
+ const { visible, hiddenCount } = capTodoRows(renderedTodos);
176334
+ const lines = visible.map((todo) => truncateToWidth(formatTodoLine(todo, theme, { showId: true }), width, "…"));
176335
+ if (hiddenCount > 0) {
176336
+ lines.push(truncateToWidth(theme.fg("dim", `+${hiddenCount} more`), width, "…"));
176337
+ }
176338
+ return lines;
176339
+ });
176340
+ }
176341
+ function registerTodosCommand(pi) {
176342
+ pi.registerCommand(TODOS_COMMAND_NAME, {
176343
+ description: "Show the current Magic Context todo list",
176344
+ handler: async (_args, ctx) => {
176345
+ if (!ctx.hasUI) {
176346
+ ctx.ui.notify("The /todos command requires interactive UI.", "error");
176347
+ return;
176348
+ }
176349
+ const sessionId = getSessionId(ctx);
176350
+ const todos = getTodoSnapshot(sessionId).todos;
176351
+ if (todos.length === 0) {
176352
+ ctx.ui.notify("No todos yet.", "info");
176353
+ return;
176354
+ }
176355
+ const counts = countTodos(todos);
176356
+ const lines = [formatCounts(counts)];
176357
+ const groups = [
176358
+ ["pending", "── Pending ──"],
176359
+ ["in_progress", "── In Progress ──"],
176360
+ ["completed", "── Completed ──"],
176361
+ ["cancelled", "── Cancelled ──"]
176362
+ ];
176363
+ for (const [status, heading] of groups) {
176364
+ const group = todos.filter((todo) => todo.status === status);
176365
+ if (group.length === 0)
176366
+ continue;
176367
+ lines.push(heading);
176368
+ const { visible, hiddenCount } = capTodoRows(group);
176369
+ for (const todo of visible)
176370
+ lines.push(formatCommandLine(todo));
176371
+ if (hiddenCount > 0)
176372
+ lines.push(` +${hiddenCount} more`);
176373
+ }
176374
+ ctx.ui.notify(lines.join(`
176375
+ `), "info");
176376
+ }
176377
+ });
176378
+ }
176379
+ function todoKey(todo, index) {
176380
+ return todo.id ? `id:${todo.id}` : `pos:${index}:${todo.content}`;
176381
+ }
176382
+ function isOverlayLive(todo) {
176383
+ return todo.status === "pending" || todo.status === "in_progress";
176384
+ }
176385
+
176386
+ class TodoOverlay {
176387
+ uiCtx;
176388
+ sessionId;
176389
+ widgetRegistered = false;
176390
+ tui;
176391
+ completedTaskIdsPendingHide = new Set;
176392
+ hiddenCompletedTaskIds = new Set;
176393
+ lastTodoKeys = new Set;
176394
+ setUICtx(sessionId, ctx) {
176395
+ if (ctx !== this.uiCtx || sessionId !== this.sessionId) {
176396
+ this.uiCtx = ctx;
176397
+ this.sessionId = sessionId;
176398
+ this.widgetRegistered = false;
176399
+ this.tui = undefined;
176400
+ this.resetCompletedDisplayState();
176401
+ }
176402
+ }
176403
+ update(sessionId = this.sessionId) {
176404
+ if (!this.uiCtx || !this.sessionId || sessionId !== this.sessionId)
176405
+ return;
176406
+ const todos = getTodoSnapshot(this.sessionId).todos;
176407
+ this.pruneCompletedDisplayState(todos);
176408
+ const visible = this.selectOverlayTodos(todos);
176409
+ if (visible.length === 0) {
176410
+ if (this.widgetRegistered) {
176411
+ this.uiCtx.setWidget(WIDGET_KEY, undefined);
176412
+ this.widgetRegistered = false;
176413
+ this.tui = undefined;
176414
+ }
176415
+ return;
176416
+ }
176417
+ if (!this.widgetRegistered) {
176418
+ this.uiCtx.setWidget(WIDGET_KEY, (tui, theme) => {
176419
+ this.tui = tui;
176420
+ return {
176421
+ render: (width) => this.renderWidget(theme, width),
176422
+ invalidate: () => {
176423
+ this.widgetRegistered = false;
176424
+ this.tui = undefined;
176425
+ }
176426
+ };
176427
+ }, { placement: "aboveEditor" });
176428
+ this.widgetRegistered = true;
176429
+ } else {
176430
+ this.tui?.requestRender();
176431
+ }
176432
+ }
176433
+ resetCompletedDisplayState() {
176434
+ this.completedTaskIdsPendingHide.clear();
176435
+ this.hiddenCompletedTaskIds.clear();
176436
+ this.lastTodoKeys.clear();
176437
+ }
176438
+ hideCompletedTasksFromPreviousTurn() {
176439
+ if (this.completedTaskIdsPendingHide.size === 0)
176440
+ return;
176441
+ for (const taskId of this.completedTaskIdsPendingHide) {
176442
+ this.hiddenCompletedTaskIds.add(taskId);
176443
+ }
176444
+ this.completedTaskIdsPendingHide.clear();
176445
+ this.update();
176446
+ }
176447
+ disposeSession(sessionId) {
176448
+ if (this.sessionId !== sessionId)
176449
+ return;
176450
+ this.dispose();
176451
+ }
176452
+ dispose() {
176453
+ if (this.uiCtx)
176454
+ this.uiCtx.setWidget(WIDGET_KEY, undefined);
176455
+ this.widgetRegistered = false;
176456
+ this.tui = undefined;
176457
+ this.uiCtx = undefined;
176458
+ this.sessionId = undefined;
176459
+ this.resetCompletedDisplayState();
176460
+ }
176461
+ pruneCompletedDisplayState(todos) {
176462
+ const currentKeys = new Set(todos.map((todo, index) => todoKey(todo, index)));
176463
+ const hasSharedKeys = [...currentKeys].some((key) => this.lastTodoKeys.has(key));
176464
+ if (this.lastTodoKeys.size > 0 && currentKeys.size > 0 && !hasSharedKeys) {
176465
+ this.resetCompletedDisplayState();
176466
+ }
176467
+ this.lastTodoKeys = currentKeys;
176468
+ const completedKeys = new Set(todos.map((todo, index) => ({ todo, key: todoKey(todo, index) })).filter(({ todo }) => todo.status === "completed").map(({ key }) => key));
176469
+ for (const taskId of this.completedTaskIdsPendingHide) {
176470
+ if (!completedKeys.has(taskId))
176471
+ this.completedTaskIdsPendingHide.delete(taskId);
176472
+ }
176473
+ for (const taskId of this.hiddenCompletedTaskIds) {
176474
+ if (!completedKeys.has(taskId))
176475
+ this.hiddenCompletedTaskIds.delete(taskId);
176476
+ }
176477
+ }
176478
+ selectOverlayTodos(todos) {
176479
+ return todos.map((todo, index) => ({ todo, key: todoKey(todo, index) })).filter(({ todo, key }) => {
176480
+ if (isOverlayLive(todo))
176481
+ return true;
176482
+ return todo.status === "completed" && !this.hiddenCompletedTaskIds.has(key);
176483
+ });
176484
+ }
176485
+ renderWidget(theme, width) {
176486
+ if (!this.sessionId)
176487
+ return [];
176488
+ const todos = getTodoSnapshot(this.sessionId).todos;
176489
+ this.pruneCompletedDisplayState(todos);
176490
+ const overlayTodos = this.selectOverlayTodos(todos);
176491
+ if (overlayTodos.length === 0)
176492
+ return [];
176493
+ const counts = countTodos(todos);
176494
+ const hasInProgress = counts.inProgress > 0;
176495
+ const headingColor = hasInProgress ? "accent" : "dim";
176496
+ const headingIcon = hasInProgress ? "●" : "○";
176497
+ const heading = `${theme.fg(headingColor, headingIcon)} ${theme.fg(headingColor, "Todos")} ${theme.fg("muted", "—")} ${theme.fg("dim", formatCounts(counts))}`;
176498
+ const truncate2 = (line) => truncateToWidth(line, width, "…");
176499
+ const lines = [truncate2(heading)];
176500
+ const { visible, hiddenCount } = capTodoRows(overlayTodos);
176501
+ for (const [index, { todo, key }] of visible.entries()) {
176502
+ const isLast = index === visible.length - 1 && hiddenCount === 0;
176503
+ const branch = theme.fg("dim", isLast ? "└─" : "├─");
176504
+ lines.push(truncate2(`${branch} ${formatTodoLine(todo, theme, { showId: true })}`));
176505
+ if (todo.status === "completed")
176506
+ this.completedTaskIdsPendingHide.add(key);
176507
+ }
176508
+ if (hiddenCount > 0) {
176509
+ lines.push(truncate2(`${theme.fg("dim", "└─")} ${theme.fg("dim", `+${hiddenCount} more`)}`));
176510
+ }
176511
+ return lines;
176512
+ }
176513
+ }
176514
+ function registerTodoOverlay(pi, deps) {
176515
+ const overlay = new TodoOverlay;
176516
+ pi.on("session_start", async (_event, ctx) => {
176517
+ const sessionId = getSessionId(ctx);
176518
+ if (!sessionId)
176519
+ return;
176520
+ seedTodoSnapshotFromStateJson(sessionId, deps.readLastTodoState(sessionId));
176521
+ if (!ctx.hasUI)
176522
+ return;
176523
+ overlay.setUICtx(sessionId, ctx.ui);
176524
+ overlay.update(sessionId);
176525
+ });
176526
+ pi.on("agent_start", async () => {
176527
+ overlay.hideCompletedTasksFromPreviousTurn();
176528
+ });
176529
+ pi.on("session_shutdown", async (_event, ctx) => {
176530
+ const sessionId = getSessionId(ctx);
176531
+ if (!sessionId)
176532
+ return;
176533
+ clearTodoSnapshot(sessionId);
176534
+ overlay.disposeSession(sessionId);
176535
+ });
176536
+ pi.on("session_before_switch", async (_event, ctx) => {
176537
+ const sessionId = getSessionId(ctx);
176538
+ if (!sessionId)
176539
+ return;
176540
+ clearTodoSnapshot(sessionId);
176541
+ overlay.disposeSession(sessionId);
176542
+ });
176543
+ return overlay;
176544
+ }
176545
+ function registerTodoStateLifecycle(pi, deps) {
176546
+ pi.on("session_start", async (_event, ctx) => {
176547
+ const sessionId = getSessionId(ctx);
176548
+ if (!sessionId)
176549
+ return;
176550
+ seedTodoSnapshotFromStateJson(sessionId, deps.readLastTodoState(sessionId));
176551
+ });
176552
+ pi.on("session_shutdown", async (_event, ctx) => {
176553
+ const sessionId = getSessionId(ctx);
176554
+ if (sessionId)
176555
+ clearTodoSnapshot(sessionId);
176556
+ });
176557
+ pi.on("session_before_switch", async (_event, ctx) => {
176558
+ const sessionId = getSessionId(ctx);
176559
+ if (sessionId)
176560
+ clearTodoSnapshot(sessionId);
176561
+ });
176562
+ }
176563
+
176564
+ // src/tools/todowrite.ts
175638
176565
  var TodoItem = exports_typebox.Object({
175639
176566
  content: exports_typebox.String({ description: "Brief description of the task" }),
175640
- status: exports_typebox.Union(STATUS_VALUES.map((v) => exports_typebox.Literal(v))),
175641
- priority: exports_typebox.Optional(exports_typebox.Union(PRIORITY_VALUES.map((v) => exports_typebox.Literal(v)))),
176567
+ status: exports_typebox.Union(TODO_STATUSES.map((v) => exports_typebox.Literal(v))),
176568
+ priority: exports_typebox.Optional(exports_typebox.Union(TODO_PRIORITIES.map((v) => exports_typebox.Literal(v)))),
175642
176569
  id: exports_typebox.Optional(exports_typebox.String({ description: "Optional stable id for the todo" }))
175643
176570
  });
175644
176571
  var TodowriteParams = exports_typebox.Object({
@@ -175646,34 +176573,24 @@ var TodowriteParams = exports_typebox.Object({
175646
176573
  description: "Replace the current task list with this complete set of todos. Include every task you intend to track this turn — pending, in_progress, completed, or cancelled — because the list overwrites previous state."
175647
176574
  })
175648
176575
  });
175649
- var TOOL_DESCRIPTION = [
175650
- "Manage your task list for this session.",
175651
- "",
175652
- "Use this tool to plan multi-step work, track in-flight tasks, and",
175653
- "mark progress as you complete steps. Pass the COMPLETE updated list",
175654
- "of todos every time — this tool replaces the prior list rather than",
175655
- "appending to it.",
175656
- "",
175657
- "Task states:",
175658
- " - pending: not started yet",
175659
- " - in_progress: currently working on (limit to ONE task at a time)",
175660
- " - completed: finished successfully",
175661
- " - cancelled: no longer needed",
175662
- "",
175663
- "Use this tool proactively for non-trivial work spanning 3+ steps.",
175664
- "Skip it for single-shot answers or trivial 1-2 step tasks."
175665
- ].join(`
175666
- `);
176576
+ var PROMPT_SNIPPET = "Manage a task list to track multi-step progress";
176577
+ var PROMPT_GUIDELINES = [
176578
+ "Use `todowrite` for non-trivial work spanning 3+ steps, when the user gives you multiple tasks, or when you need to track progress across a verify/fix loop. Skip it for single-shot answers or trivial one-step work.",
176579
+ "Pass the COMPLETE updated todo list every time. This tool replaces the prior list rather than appending to it, so include pending, in_progress, completed, and cancelled tasks that should remain visible.",
176580
+ "When starting a task, mark exactly one todo `in_progress` before doing the work. Mark items `completed` immediately when done; use `cancelled` only for work that is no longer needed.",
176581
+ "Never mark a todo completed if verification is failing, implementation is partial, or an unresolved blocker remains. Keep it `in_progress` and add or update a todo for the blocker instead."
176582
+ ];
175667
176583
  function createTodowriteTool() {
175668
176584
  return {
175669
- name: "todowrite",
176585
+ name: TODO_TOOL_NAME,
175670
176586
  label: "Todos",
175671
- description: TOOL_DESCRIPTION,
176587
+ description: "Manage the session task list.",
176588
+ promptSnippet: PROMPT_SNIPPET,
176589
+ promptGuidelines: PROMPT_GUIDELINES,
175672
176590
  parameters: TodowriteParams,
175673
176591
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
175674
176592
  const todos = params.todos ?? [];
175675
- const completed = todos.filter((t) => t.status === "completed").length;
175676
- const active = todos.length - completed;
176593
+ const active = todos.filter((todo) => !TITLE_DONE_STATUSES.has(todo.status)).length;
175677
176594
  return {
175678
176595
  content: [
175679
176596
  {
@@ -175687,6 +176604,12 @@ function createTodowriteTool() {
175687
176604
  truncated: false
175688
176605
  }
175689
176606
  };
176607
+ },
176608
+ renderCall(args, theme) {
176609
+ return renderTodowriteCall(args, theme);
176610
+ },
176611
+ renderResult(result, _opts, theme) {
176612
+ return renderTodowriteResult(result, theme);
175690
176613
  }
175691
176614
  };
175692
176615
  }
@@ -175712,15 +176635,22 @@ function registerMagicContextTools(pi, opts) {
175712
176635
  if (!opts.sessionScopedToolsDisabled) {
175713
176636
  pi.registerTool(createCtxNoteTool({
175714
176637
  db: opts.db,
175715
- dreamerEnabled: opts.dreamerEnabled ?? false
176638
+ dreamerEnabled: opts.dreamerEnabled ?? false,
176639
+ resolveDreamerEnabled: opts.resolveDreamerEnabled
175716
176640
  }));
175717
176641
  pi.registerTool(createCtxExpandTool({ db: opts.db }));
175718
176642
  }
175719
- pi.registerTool(createTodowriteTool());
176643
+ if (opts.todowriteEnabled !== false) {
176644
+ pi.registerTool(createTodowriteTool());
176645
+ if (opts.todowriteCommandEnabled !== false) {
176646
+ registerTodosCommand(pi);
176647
+ }
176648
+ }
175720
176649
  if (!opts.sessionScopedToolsDisabled) {
175721
176650
  pi.registerTool(createCtxReduceTool({
175722
176651
  db: opts.db,
175723
- protectedTags: opts.protectedTags ?? 20
176652
+ protectedTags: opts.protectedTags ?? 20,
176653
+ resolveProtectedTags: opts.resolveProtectedTags
175724
176654
  }));
175725
176655
  }
175726
176656
  }
@@ -175751,9 +176681,11 @@ function magicContextSubagentExtension(pi) {
175751
176681
  ensureProjectRegistered: ensureProjectRegisteredFromPiDirectory,
175752
176682
  memoryToolEnabled: dreamerActionsEnabled,
175753
176683
  allowDreamerActions: dreamerActionsEnabled,
175754
- sessionScopedToolsDisabled: true
176684
+ sessionScopedToolsDisabled: true,
176685
+ todowriteEnabled: cfg.todowrite.enabled !== false,
176686
+ todowriteCommandEnabled: false
175755
176687
  });
175756
- log(`[pi-subagent] registered tools: ctx_search${dreamerActionsEnabled ? ", ctx_memory" : ""}` + ` (ctx_note/ctx_expand omitted: --no-session child;` + ` memory=${cfg.memory.enabled}, embedding=${cfg.embedding.provider !== "off"},` + ` git_commits=${cfg.memory.git_commit_indexing.enabled}, dreamer_actions=${dreamerActionsEnabled})`);
176688
+ log(`[pi-subagent] registered tools: ctx_search${dreamerActionsEnabled ? ", ctx_memory" : ""}${cfg.todowrite.enabled !== false ? ", todowrite" : ""}` + ` (ctx_note/ctx_expand omitted: --no-session child;` + ` memory=${cfg.memory.enabled}, embedding=${cfg.embedding.provider !== "off"},` + ` git_commits=${cfg.memory.git_commit_indexing.enabled}, dreamer_actions=${dreamerActionsEnabled})`);
175757
176689
  } catch (err5) {
175758
176690
  const message = err5 instanceof Error ? err5.message : String(err5);
175759
176691
  log(`[pi-subagent] startup failed: ${message}`);