@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.
package/dist/index.js CHANGED
@@ -14990,12 +14990,41 @@ function pushNotification(type, payload, sessionId) {
14990
14990
  queue = kept.sort((a, b) => a.id - b.id);
14991
14991
  }
14992
14992
  }
14993
- function drainNotifications(lastReceivedId = 0, sessionId) {
14993
+ function cursor(value) {
14994
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
14995
+ }
14996
+ function drainNotifications(lastReceivedId = 0, sessionId, options = {}) {
14997
+ const sessionCursor = cursor(lastReceivedId);
14998
+ if (options.globalOnly) {
14999
+ queue = queue.filter((notification) => notification.sessionId !== undefined || notification.id > sessionCursor);
15000
+ return queue.filter((notification) => notification.sessionId === undefined && notification.id > sessionCursor);
15001
+ }
15002
+ if (options.sessionOnly) {
15003
+ if (sessionId === undefined)
15004
+ return [];
15005
+ queue = queue.filter((notification) => notification.sessionId !== sessionId || notification.id > sessionCursor);
15006
+ return queue.filter((notification) => notification.sessionId === sessionId && notification.id > sessionCursor);
15007
+ }
15008
+ if (sessionId !== undefined && options.globalLastReceivedId !== undefined) {
15009
+ const globalCursor = cursor(options.globalLastReceivedId);
15010
+ queue = queue.filter((notification) => {
15011
+ if (notification.sessionId === undefined)
15012
+ return notification.id > globalCursor;
15013
+ if (notification.sessionId === sessionId)
15014
+ return notification.id > sessionCursor;
15015
+ return true;
15016
+ });
15017
+ return queue.filter((notification) => {
15018
+ if (notification.sessionId === undefined)
15019
+ return notification.id > globalCursor;
15020
+ return notification.sessionId === sessionId && notification.id > sessionCursor;
15021
+ });
15022
+ }
14994
15023
  const matchesClient = (notification) => sessionId === undefined || notification.sessionId === undefined || notification.sessionId === sessionId;
14995
- if (lastReceivedId > 0) {
14996
- queue = queue.filter((notification) => !(notification.id <= lastReceivedId && matchesClient(notification)));
15024
+ if (sessionCursor > 0) {
15025
+ queue = queue.filter((notification) => !(notification.id <= sessionCursor && matchesClient(notification)));
14997
15026
  }
14998
- return queue.filter((notification) => notification.id > lastReceivedId && matchesClient(notification));
15027
+ return queue.filter((notification) => notification.id > sessionCursor && matchesClient(notification));
14999
15028
  }
15000
15029
  function isTuiConnected(sessionId) {
15001
15030
  if (sinks.size === 0)
@@ -15138,7 +15167,7 @@ var PROMPT_CONTEXT_MESSAGE_LIMIT = 50;
15138
15167
 
15139
15168
  // src/index.ts
15140
15169
  import { createRequire as createRequire3 } from "node:module";
15141
- import { join as join13 } from "node:path";
15170
+ import { join as join13, resolve as resolve3 } from "node:path";
15142
15171
 
15143
15172
  // ../plugin/src/config/agent-disable.ts
15144
15173
  function isDreamerRunnable(config) {
@@ -149215,12 +149244,19 @@ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal
149215
149244
  let totalTokens = 0;
149216
149245
  let messagesProcessed = 0;
149217
149246
  let lastOrdinal = startOrdinal - 1;
149247
+ let highestScannedOrdinal = startOrdinal - 1;
149218
149248
  let lastMessageId = "";
149219
149249
  let firstMessageId = "";
149220
149250
  let currentBlock = null;
149221
149251
  let pendingNoiseMeta = [];
149222
149252
  let commitClusters = 0;
149223
149253
  let lastFlushedRole = "";
149254
+ function recordFilteredNoise(meta) {
149255
+ pendingNoiseMeta.push(meta);
149256
+ if (!currentBlock) {
149257
+ highestScannedOrdinal = Math.max(highestScannedOrdinal, meta.ordinal);
149258
+ }
149259
+ }
149224
149260
  function flushCurrentBlock() {
149225
149261
  if (!currentBlock)
149226
149262
  return true;
@@ -149236,6 +149272,7 @@ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal
149236
149272
  if (!firstMessageId)
149237
149273
  firstMessageId = currentBlock.meta[0]?.messageId ?? "";
149238
149274
  lastOrdinal = currentBlock.meta[currentBlock.meta.length - 1]?.ordinal ?? currentBlock.endOrdinal;
149275
+ highestScannedOrdinal = Math.max(highestScannedOrdinal, lastOrdinal);
149239
149276
  lastMessageId = currentBlock.meta[currentBlock.meta.length - 1]?.messageId ?? "";
149240
149277
  messagesProcessed += currentBlock.meta.length;
149241
149278
  lines.push(blockText);
@@ -149259,7 +149296,7 @@ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal
149259
149296
  if (msg.role === "user" && !hasMeaningfulUserText(msg.parts)) {
149260
149297
  const tcSummaries = extractToolCallSummaries(msg.parts);
149261
149298
  if (tcSummaries.length === 0) {
149262
- pendingNoiseMeta.push(meta);
149299
+ recordFilteredNoise(meta);
149263
149300
  continue;
149264
149301
  }
149265
149302
  const tcText = tcSummaries.join(" / ");
@@ -149291,7 +149328,7 @@ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal
149291
149328
  const compacted = compactTextForSummary(allParts.join(" / "), msg.role);
149292
149329
  const text = compacted.text;
149293
149330
  if (!text) {
149294
- pendingNoiseMeta.push(meta);
149331
+ recordFilteredNoise(meta);
149295
149332
  continue;
149296
149333
  }
149297
149334
  const msgHasNarrative = textParts.length > 0;
@@ -149318,7 +149355,9 @@ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal
149318
149355
  };
149319
149356
  pendingNoiseMeta = [];
149320
149357
  }
149321
- flushCurrentBlock();
149358
+ if (flushCurrentBlock() && pendingNoiseMeta.length > 0) {
149359
+ highestScannedOrdinal = Math.max(highestScannedOrdinal, pendingNoiseMeta[pendingNoiseMeta.length - 1]?.ordinal ?? highestScannedOrdinal);
149360
+ }
149322
149361
  const toolOnlyRanges = [];
149323
149362
  for (const range of flushedToolOnlyBlocks) {
149324
149363
  const last = toolOnlyRanges[toolOnlyRanges.length - 1];
@@ -149335,7 +149374,7 @@ function readSessionChunk(sessionId, tokenBudget, offset = 1, eligibleEndOrdinal
149335
149374
  endMessageId: lastMessageId,
149336
149375
  messageCount: messagesProcessed,
149337
149376
  tokenEstimate: totalTokens,
149338
- hasMore: lastOrdinal < (eligibleEndOrdinal !== undefined ? Math.min(eligibleEndOrdinal - 1, totalMessageCount) : totalMessageCount),
149377
+ hasMore: Math.max(lastOrdinal, highestScannedOrdinal) < (eligibleEndOrdinal !== undefined ? Math.min(eligibleEndOrdinal - 1, totalMessageCount) : totalMessageCount),
149339
149378
  text: lines.join(`
149340
149379
  `),
149341
149380
  lines: lineMeta,
@@ -149370,7 +149409,10 @@ function clearCompressionDepthRange(db, sessionId, startOrdinal, endOrdinal) {
149370
149409
  var lastIndexedStatements = new WeakMap;
149371
149410
  var insertMessageStatements = new WeakMap;
149372
149411
  var upsertIndexStatements = new WeakMap;
149412
+ var upsertCleanIndexStatements = new WeakMap;
149413
+ var upsertDirtyFloorStatements = new WeakMap;
149373
149414
  var deleteFtsStatements = new WeakMap;
149415
+ var deleteFtsFromOrdinalStatements = new WeakMap;
149374
149416
  var deleteIndexStatements = new WeakMap;
149375
149417
  var countIndexedMessageStatements = new WeakMap;
149376
149418
  function normalizeIndexText(text) {
@@ -149379,7 +149421,7 @@ function normalizeIndexText(text) {
149379
149421
  function getLastIndexedStatement(db) {
149380
149422
  let stmt = lastIndexedStatements.get(db);
149381
149423
  if (!stmt) {
149382
- stmt = db.prepare("SELECT last_indexed_ordinal FROM message_history_index WHERE session_id = ?");
149424
+ stmt = db.prepare("SELECT last_indexed_ordinal, dirty_floor_ordinal FROM message_history_index WHERE session_id = ?");
149383
149425
  lastIndexedStatements.set(db, stmt);
149384
149426
  }
149385
149427
  return stmt;
@@ -149400,6 +149442,22 @@ function getUpsertIndexStatement(db) {
149400
149442
  }
149401
149443
  return stmt;
149402
149444
  }
149445
+ function getUpsertCleanIndexStatement(db) {
149446
+ let stmt = upsertCleanIndexStatements.get(db);
149447
+ if (!stmt) {
149448
+ 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");
149449
+ upsertCleanIndexStatements.set(db, stmt);
149450
+ }
149451
+ return stmt;
149452
+ }
149453
+ function getUpsertDirtyFloorStatement(db) {
149454
+ let stmt = upsertDirtyFloorStatements.get(db);
149455
+ if (!stmt) {
149456
+ 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");
149457
+ upsertDirtyFloorStatements.set(db, stmt);
149458
+ }
149459
+ return stmt;
149460
+ }
149403
149461
  function getDeleteFtsStatement(db) {
149404
149462
  let stmt = deleteFtsStatements.get(db);
149405
149463
  if (!stmt) {
@@ -149408,6 +149466,14 @@ function getDeleteFtsStatement(db) {
149408
149466
  }
149409
149467
  return stmt;
149410
149468
  }
149469
+ function getDeleteFtsFromOrdinalStatement(db) {
149470
+ let stmt = deleteFtsFromOrdinalStatements.get(db);
149471
+ if (!stmt) {
149472
+ stmt = db.prepare("DELETE FROM message_history_fts WHERE session_id = ? AND CAST(message_ordinal AS INTEGER) >= ?");
149473
+ deleteFtsFromOrdinalStatements.set(db, stmt);
149474
+ }
149475
+ return stmt;
149476
+ }
149411
149477
  function getDeleteIndexStatement(db) {
149412
149478
  let stmt = deleteIndexStatements.get(db);
149413
149479
  if (!stmt) {
@@ -149428,6 +149494,14 @@ function getLastIndexedOrdinal(db, sessionId) {
149428
149494
  const row = getLastIndexedStatement(db).get(sessionId);
149429
149495
  return typeof row?.last_indexed_ordinal === "number" ? row.last_indexed_ordinal : 0;
149430
149496
  }
149497
+ function getDirtyIndexFloor(db, sessionId) {
149498
+ const row = getLastIndexedStatement(db).get(sessionId);
149499
+ return typeof row?.dirty_floor_ordinal === "number" && row.dirty_floor_ordinal > 0 ? row.dirty_floor_ordinal : null;
149500
+ }
149501
+ function markMessageIndexDirty(db, sessionId, floorOrdinal) {
149502
+ const dirtyFloor = Math.max(1, Math.floor(floorOrdinal));
149503
+ getUpsertDirtyFloorStatement(db).run(sessionId, getLastIndexedOrdinal(db, sessionId), dirtyFloor, Date.now(), getHarness());
149504
+ }
149431
149505
  function isMessageAlreadyIndexed(db, sessionId, messageId) {
149432
149506
  const row = getCountIndexedMessageStatement(db).get(sessionId, messageId);
149433
149507
  return (typeof row?.count === "number" ? row.count : 0) > 0;
@@ -149495,7 +149569,15 @@ function indexMessagesAfterOrdinal(db, sessionId, messages, lastIndexedOrdinal,
149495
149569
  db.exec("BEGIN IMMEDIATE");
149496
149570
  let committed = false;
149497
149571
  try {
149498
- const effectiveWatermark = Math.max(lastIndexedOrdinal, getLastIndexedOrdinal(db, sessionId));
149572
+ let effectiveWatermark = Math.max(lastIndexedOrdinal, getLastIndexedOrdinal(db, sessionId));
149573
+ const dirtyFloor = getDirtyIndexFloor(db, sessionId);
149574
+ if (dirtyFloor !== null) {
149575
+ const rewindOrdinal = Math.max(1, Math.min(dirtyFloor, finalWatermark + 1));
149576
+ if (rewindOrdinal <= finalWatermark) {
149577
+ getDeleteFtsFromOrdinalStatement(db).run(sessionId, rewindOrdinal);
149578
+ }
149579
+ effectiveWatermark = Math.min(effectiveWatermark, rewindOrdinal - 1);
149580
+ }
149499
149581
  const insertMessage = getInsertMessageStatement(db);
149500
149582
  for (const message of messages) {
149501
149583
  if (message.ordinal <= effectiveWatermark) {
@@ -149512,7 +149594,7 @@ function indexMessagesAfterOrdinal(db, sessionId, messages, lastIndexedOrdinal,
149512
149594
  inserted++;
149513
149595
  }
149514
149596
  const newWatermark = Math.max(effectiveWatermark, finalWatermark);
149515
- getUpsertIndexStatement(db).run(sessionId, newWatermark, now, getHarness());
149597
+ getUpsertCleanIndexStatement(db).run(sessionId, newWatermark, now, getHarness());
149516
149598
  db.exec("COMMIT");
149517
149599
  committed = true;
149518
149600
  } finally {
@@ -149609,8 +149691,9 @@ function scheduleReconciliation(db, sessionId, readMessages) {
149609
149691
  reconciliationScheduledSessions.add(sessionId);
149610
149692
  defer(() => {
149611
149693
  reconcileSessionIndex(db, sessionId, readMessages).catch((error) => {
149612
- reconciliationScheduledSessions.delete(sessionId);
149613
149694
  logIndexingError(sessionId, "reconciliation", error);
149695
+ }).finally(() => {
149696
+ reconciliationScheduledSessions.delete(sessionId);
149614
149697
  });
149615
149698
  });
149616
149699
  }
@@ -149622,13 +149705,17 @@ function scheduleIncrementalIndex(db, sessionId, messageId, readSingleMessage) {
149622
149705
  const timer = setTimeout(() => {
149623
149706
  incrementalTimers.delete(key);
149624
149707
  pendingIncrementalKeys.add(key);
149708
+ let attemptedOrdinal = null;
149625
149709
  runWithSessionLock(sessionId, () => {
149626
149710
  const message = readSingleMessage(sessionId, messageId);
149627
149711
  if (!message) {
149628
149712
  return;
149629
149713
  }
149714
+ attemptedOrdinal = message.ordinal;
149630
149715
  indexSingleMessage(db, sessionId, message);
149631
149716
  }).catch((error) => {
149717
+ markMessageIndexDirty(db, sessionId, attemptedOrdinal ?? getLastIndexedOrdinal(db, sessionId) + 1);
149718
+ reconciledSessions.delete(sessionId);
149632
149719
  logIndexingError(sessionId, `incremental index for ${messageId}`, error);
149633
149720
  }).finally(() => {
149634
149721
  pendingIncrementalKeys.delete(key);
@@ -150580,6 +150667,7 @@ function healNullTextColumns(db) {
150580
150667
  ["cache_ttl", ""],
150581
150668
  ["last_nudge_band", ""],
150582
150669
  ["last_nudge_level", ""],
150670
+ ["channel2_nudge_claim_token", ""],
150583
150671
  ["last_transform_error", ""],
150584
150672
  ["nudge_anchor_message_id", ""],
150585
150673
  ["nudge_anchor_text", ""],
@@ -150643,6 +150731,7 @@ function healNullIntegerColumns(db) {
150643
150731
  }
150644
150732
 
150645
150733
  // ../plugin/src/features/magic-context/workspaces.ts
150734
+ init_logger();
150646
150735
  import { createHash as createHash2 } from "node:crypto";
150647
150736
 
150648
150737
  // ../plugin/src/features/magic-context/memory/constants.ts
@@ -150693,6 +150782,7 @@ var CATEGORY_DEFAULT_TTL = {
150693
150782
  };
150694
150783
  // ../plugin/src/features/magic-context/workspaces.ts
150695
150784
  var VALID_SHARE_CATEGORIES = new Set(V2_MEMORY_CATEGORIES);
150785
+ var DEFAULT_WORKSPACE_SHARE_CATEGORIES = ["CONSTRAINTS"];
150696
150786
  function tableExists(db, tableName) {
150697
150787
  const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ? LIMIT 1").get(tableName);
150698
150788
  return Boolean(row);
@@ -150707,23 +150797,39 @@ function uniqueSorted(values) {
150707
150797
  function placeholders(values) {
150708
150798
  return values.map(() => "?").join(", ");
150709
150799
  }
150800
+ function defaultWorkspaceShareCategories() {
150801
+ return [...DEFAULT_WORKSPACE_SHARE_CATEGORIES];
150802
+ }
150803
+ function warnInvalidShareCategories(reason, raw) {
150804
+ log("[magic-context] WARN: invalid workspace share_categories; sharing no foreign memory categories", {
150805
+ reason,
150806
+ raw
150807
+ });
150808
+ }
150710
150809
  function normalizeShareCategories(raw) {
150711
- if (raw === null || raw === undefined)
150712
- return null;
150713
- if (typeof raw !== "string")
150714
- return null;
150810
+ if (raw === null || raw === undefined) {
150811
+ return defaultWorkspaceShareCategories();
150812
+ }
150813
+ if (typeof raw !== "string") {
150814
+ warnInvalidShareCategories("not a string", raw);
150815
+ return [];
150816
+ }
150715
150817
  let parsed;
150716
150818
  try {
150717
150819
  parsed = JSON.parse(raw);
150718
150820
  } catch {
150719
- return null;
150821
+ warnInvalidShareCategories("malformed JSON", raw);
150822
+ return [];
150823
+ }
150824
+ if (!Array.isArray(parsed)) {
150825
+ warnInvalidShareCategories("not a JSON array", raw);
150826
+ return [];
150720
150827
  }
150721
- if (!Array.isArray(parsed))
150722
- return null;
150723
150828
  const categories = [];
150724
150829
  for (const value of parsed) {
150725
150830
  if (typeof value !== "string" || !VALID_SHARE_CATEGORIES.has(value)) {
150726
- return null;
150831
+ warnInvalidShareCategories("unknown category", raw);
150832
+ return [];
150727
150833
  }
150728
150834
  if (!categories.includes(value))
150729
150835
  categories.push(value);
@@ -150732,8 +150838,21 @@ function normalizeShareCategories(raw) {
150732
150838
  }
150733
150839
  function selectWorkspaceShareCategories(db, identities) {
150734
150840
  const candidates = uniqueSorted(identities.filter((identity) => identity.length > 0));
150735
- if (candidates.length === 0 || !tableExists(db, "workspace_members") || !tableExists(db, "workspaces") || !columnExists(db, "workspaces", "share_categories")) {
150841
+ if (candidates.length === 0 || !tableExists(db, "workspace_members")) {
150842
+ return null;
150843
+ }
150844
+ const hasMembership = Boolean(db.prepare(`SELECT 1
150845
+ FROM workspace_members
150846
+ WHERE project_path IN (${placeholders(candidates)})
150847
+ LIMIT 1`).get(...candidates));
150848
+ if (!hasMembership)
150736
150849
  return null;
150850
+ if (!tableExists(db, "workspaces")) {
150851
+ log("[magic-context] WARN: workspace member has no workspaces table; sharing no foreign memory categories");
150852
+ return [];
150853
+ }
150854
+ if (!columnExists(db, "workspaces", "share_categories")) {
150855
+ return defaultWorkspaceShareCategories();
150737
150856
  }
150738
150857
  const row = db.prepare(`SELECT workspace.share_categories AS shareCategories
150739
150858
  FROM workspace_members AS member
@@ -150741,7 +150860,11 @@ function selectWorkspaceShareCategories(db, identities) {
150741
150860
  WHERE member.project_path IN (${placeholders(candidates)})
150742
150861
  ORDER BY workspace.id ASC
150743
150862
  LIMIT 1`).get(...candidates);
150744
- return normalizeShareCategories(row?.shareCategories ?? null);
150863
+ if (!row) {
150864
+ log("[magic-context] WARN: workspace member has no workspace share_categories row; sharing no foreign memory categories");
150865
+ return [];
150866
+ }
150867
+ return normalizeShareCategories(row.shareCategories);
150745
150868
  }
150746
150869
  function resolveWorkspaceShareCategories(db, projectIdentity) {
150747
150870
  return selectWorkspaceShareCategories(db, [projectIdentity]);
@@ -150846,7 +150969,7 @@ function computeWorkspaceEpochFingerprint(db, identities) {
150846
150969
  const hash = createHash2("sha256");
150847
150970
  hash.update("share_categories", "utf8");
150848
150971
  hash.update("\x00");
150849
- hash.update(shareCategories === null ? "ALL" : JSON.stringify(shareCategories), "utf8");
150972
+ hash.update(shareCategories === null ? "NO_WORKSPACE" : JSON.stringify(shareCategories), "utf8");
150850
150973
  hash.update(`
150851
150974
  `);
150852
150975
  for (const identity of canonical) {
@@ -153418,6 +153541,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
153418
153541
  CREATE TABLE IF NOT EXISTS message_history_index (
153419
153542
  session_id TEXT PRIMARY KEY,
153420
153543
  last_indexed_ordinal INTEGER NOT NULL DEFAULT 0,
153544
+ dirty_floor_ordinal INTEGER NOT NULL DEFAULT 0,
153421
153545
  updated_at INTEGER NOT NULL,
153422
153546
  harness TEXT NOT NULL DEFAULT 'opencode'
153423
153547
  );
@@ -153447,6 +153571,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
153447
153571
  last_nudge_level TEXT DEFAULT '',
153448
153572
  channel2_nudge_state TEXT DEFAULT '',
153449
153573
  channel2_nudge_claimed_at INTEGER DEFAULT 0,
153574
+ channel2_nudge_claim_token TEXT DEFAULT '',
153450
153575
  last_emergency_input_sample INTEGER DEFAULT 0,
153451
153576
  last_transform_error TEXT DEFAULT '',
153452
153577
  nudge_anchor_message_id TEXT DEFAULT '',
@@ -153693,6 +153818,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
153693
153818
  ensureColumn(db, "session_meta", "last_nudge_level", "TEXT DEFAULT ''");
153694
153819
  ensureColumn(db, "session_meta", "channel2_nudge_state", "TEXT DEFAULT ''");
153695
153820
  ensureColumn(db, "session_meta", "channel2_nudge_claimed_at", "INTEGER DEFAULT 0");
153821
+ ensureColumn(db, "session_meta", "channel2_nudge_claim_token", "TEXT DEFAULT ''");
153696
153822
  ensureColumn(db, "session_meta", "last_emergency_input_sample", "INTEGER DEFAULT 0");
153697
153823
  ensureColumn(db, "session_meta", "last_transform_error", "TEXT DEFAULT ''");
153698
153824
  ensureColumn(db, "session_meta", "nudge_anchor_message_id", "TEXT DEFAULT ''");
@@ -153905,6 +154031,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
153905
154031
  ON transform_decisions(session_id, harness);
153906
154032
  `);
153907
154033
  ensureColumn(db, "tags", "harness", "TEXT NOT NULL DEFAULT 'opencode'");
154034
+ ensureColumn(db, "message_history_index", "dirty_floor_ordinal", "INTEGER NOT NULL DEFAULT 0");
153908
154035
  ensureColumn(db, "pending_ops", "harness", "TEXT NOT NULL DEFAULT 'opencode'");
153909
154036
  ensureColumn(db, "source_contents", "harness", "TEXT NOT NULL DEFAULT 'opencode'");
153910
154037
  ensureColumn(db, "compartments", "harness", "TEXT NOT NULL DEFAULT 'opencode'");
@@ -153920,7 +154047,7 @@ var CHANNEL2_CLAIM_TTL_MS = 120000;
153920
154047
  function healWedgedChannel2Claims(db) {
153921
154048
  try {
153922
154049
  const staleBefore = Date.now() - CHANNEL2_CLAIM_TTL_MS;
153923
- 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);
154050
+ 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);
153924
154051
  } catch {}
153925
154052
  }
153926
154053
  function openDatabase(dbPathOrOptions) {
@@ -154613,7 +154740,7 @@ function setChannel2NudgeState(db, sessionId, state) {
154613
154740
  db.transaction(() => {
154614
154741
  ensureSessionMetaRow(db, sessionId);
154615
154742
  const claimedAt = state === "claimed" ? Date.now() : 0;
154616
- db.prepare("UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ? WHERE session_id = ?").run(state, claimedAt, sessionId);
154743
+ db.prepare("UPDATE session_meta SET channel2_nudge_state = ?, channel2_nudge_claimed_at = ?, channel2_nudge_claim_token = '' WHERE session_id = ?").run(state, claimedAt, sessionId);
154617
154744
  })();
154618
154745
  }
154619
154746
  function casChannel2NudgeState(db, sessionId, from, to) {
@@ -154621,7 +154748,7 @@ function casChannel2NudgeState(db, sessionId, from, to) {
154621
154748
  db.transaction(() => {
154622
154749
  ensureSessionMetaRow(db, sessionId);
154623
154750
  const claimedAt = to === "claimed" ? Date.now() : 0;
154624
- 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);
154751
+ 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);
154625
154752
  changed = (result.changes ?? 0) > 0;
154626
154753
  })();
154627
154754
  return changed;
@@ -155029,6 +155156,13 @@ function setSessionWorkMetrics(db, sessionId, newWorkTokens, totalInputTokens) {
155029
155156
  SET new_work_tokens = ?, total_input_tokens = ?
155030
155157
  WHERE session_id = ?`).run(Math.max(0, Math.floor(newWorkTokens)), Math.max(0, Math.floor(totalInputTokens)), sessionId);
155031
155158
  }
155159
+ function getSessionWorkMetrics(db, sessionId) {
155160
+ const row = db.prepare("SELECT new_work_tokens, total_input_tokens FROM session_meta WHERE session_id = ?").get(sessionId);
155161
+ return {
155162
+ newWorkTokens: typeof row?.new_work_tokens === "number" ? row.new_work_tokens : 0,
155163
+ totalInputTokens: typeof row?.total_input_tokens === "number" ? row.total_input_tokens : 0
155164
+ };
155165
+ }
155032
155166
  // ../plugin/src/features/magic-context/storage-meta-session.ts
155033
155167
  import { Buffer as Buffer3 } from "node:buffer";
155034
155168
 
@@ -156826,8 +156960,31 @@ function clearNoteNudgeTriggerOnly(db, sessionId) {
156826
156960
 
156827
156961
  // ../plugin/src/hooks/magic-context/todo-view.ts
156828
156962
  import { createHash as createHash5 } from "node:crypto";
156829
- var TERMINAL_STATUSES = new Set(["completed", "cancelled"]);
156830
- var TITLE_DONE_STATUSES = new Set(["completed"]);
156963
+ var TODO_STATUS_PENDING = "pending";
156964
+ var TODO_STATUS_IN_PROGRESS = "in_progress";
156965
+ var TODO_STATUS_COMPLETED = "completed";
156966
+ var TODO_STATUS_CANCELLED = "cancelled";
156967
+ var TODO_PRIORITY_HIGH = "high";
156968
+ var TODO_PRIORITY_MEDIUM = "medium";
156969
+ var TODO_PRIORITY_LOW = "low";
156970
+ var TODO_STATUSES = [
156971
+ TODO_STATUS_PENDING,
156972
+ TODO_STATUS_IN_PROGRESS,
156973
+ TODO_STATUS_COMPLETED,
156974
+ TODO_STATUS_CANCELLED
156975
+ ];
156976
+ var TODO_PRIORITIES = [
156977
+ TODO_PRIORITY_HIGH,
156978
+ TODO_PRIORITY_MEDIUM,
156979
+ TODO_PRIORITY_LOW
156980
+ ];
156981
+ var TODO_STATUS_SET = new Set(TODO_STATUSES);
156982
+ var TODO_PRIORITY_SET = new Set(TODO_PRIORITIES);
156983
+ var TERMINAL_STATUSES = new Set([
156984
+ TODO_STATUS_COMPLETED,
156985
+ TODO_STATUS_CANCELLED
156986
+ ]);
156987
+ var TITLE_DONE_STATUSES = new Set([TODO_STATUS_COMPLETED]);
156831
156988
  var SYNTHETIC_CALL_ID_PREFIX = "mc_synthetic_todo_";
156832
156989
  function normalizeTodoStateJson(todos) {
156833
156990
  if (!Array.isArray(todos))
@@ -156839,7 +156996,7 @@ function normalizeTodoStateJson(todos) {
156839
156996
  normalized.push({
156840
156997
  content: todo.content,
156841
156998
  status: todo.status,
156842
- priority: todo.priority ?? "medium"
156999
+ priority: todo.priority ?? TODO_PRIORITY_MEDIUM
156843
157000
  });
156844
157001
  }
156845
157002
  return JSON.stringify(normalized);
@@ -156883,11 +157040,11 @@ function parseTodoState(stateJson) {
156883
157040
  const result = [];
156884
157041
  for (const item of parsed) {
156885
157042
  if (!isTodoItem(item))
156886
- continue;
157043
+ return null;
156887
157044
  result.push({
156888
157045
  content: item.content,
156889
157046
  status: item.status,
156890
- priority: item.priority ?? "medium"
157047
+ priority: item.priority ?? TODO_PRIORITY_MEDIUM
156891
157048
  });
156892
157049
  }
156893
157050
  return result;
@@ -156895,11 +157052,17 @@ function parseTodoState(stateJson) {
156895
157052
  return null;
156896
157053
  }
156897
157054
  }
157055
+ function isTodoStatus(value) {
157056
+ return typeof value === "string" && TODO_STATUS_SET.has(value);
157057
+ }
157058
+ function isTodoPriority(value) {
157059
+ return typeof value === "string" && TODO_PRIORITY_SET.has(value);
157060
+ }
156898
157061
  function isTodoItem(value) {
156899
157062
  if (value === null || typeof value !== "object")
156900
157063
  return false;
156901
157064
  const todo = value;
156902
- return typeof todo.content === "string" && typeof todo.status === "string" && (todo.priority === undefined || typeof todo.priority === "string");
157065
+ return typeof todo.content === "string" && isTodoStatus(todo.status) && (todo.priority === undefined || isTodoPriority(todo.priority));
156903
157066
  }
156904
157067
 
156905
157068
  // ../plugin/src/hooks/magic-context/upgrade-reminder.ts
@@ -157979,7 +158142,8 @@ function registerCtxAugCommand(pi, config) {
157979
158142
  const branch = ctx.sessionManager.getBranch();
157980
158143
  const lastEntryId = branch.length > 0 ? branch[branch.length - 1]?.id : "unknown";
157981
158144
  const sessionLabel = `pi-session-${lastEntryId}`;
157982
- if (!config) {
158145
+ const currentConfig = typeof config === "function" ? config(ctx) : config;
158146
+ if (!currentConfig) {
157983
158147
  ctx.ui.notify("/ctx-aug: Sidekick is not configured. Add `sidekick.model` to your magic-context.jsonc to enable this command.", "warning");
157984
158148
  return;
157985
158149
  }
@@ -157991,20 +158155,20 @@ function registerCtxAugCommand(pi, config) {
157991
158155
  ctx.ui.notify("\uD83D\uDD0D Preparing augmentation… 2-10s depending on your sidekick provider.", "info");
157992
158156
  }
157993
158157
  sessionLog(sessionLabel, "/ctx-aug: spawning sidekick", {
157994
- model: config.model
158158
+ model: currentConfig.model
157995
158159
  });
157996
158160
  const projectIdentity = resolveProjectIdentity(ctx.cwd);
157997
158161
  sessionLog(sessionLabel, "/ctx-aug: project identity", projectIdentity);
157998
158162
  const result = await runner.run({
157999
158163
  agent: "sidekick",
158000
- systemPrompt: withContentLanguageDirective(config.systemPrompt ?? SIDEKICK_SYSTEM_PROMPT, config.language),
158164
+ systemPrompt: withContentLanguageDirective(currentConfig.systemPrompt ?? SIDEKICK_SYSTEM_PROMPT, currentConfig.language),
158001
158165
  userMessage: prompt,
158002
- model: config.model,
158003
- fallbackModels: config.fallbackModels,
158004
- timeoutMs: config.timeoutMs ?? 30000,
158166
+ model: currentConfig.model,
158167
+ fallbackModels: currentConfig.fallbackModels,
158168
+ timeoutMs: currentConfig.timeoutMs ?? 30000,
158005
158169
  cwd: ctx.cwd,
158006
158170
  signal: ctx.signal,
158007
- thinkingLevel: config.thinking_level,
158171
+ thinkingLevel: currentConfig.thinking_level,
158008
158172
  accountingSessionId: sessionLabel,
158009
158173
  accountingSubagent: "sidekick"
158010
158174
  });
@@ -158577,6 +158741,22 @@ function extractLatestAssistantText(messages) {
158577
158741
  return getTextParts(latest).map((part) => part.text).join(`
158578
158742
  `) || null;
158579
158743
  }
158744
+ function hasLengthCappedOutput(value) {
158745
+ if (Array.isArray(value))
158746
+ return value.some((item) => hasLengthCappedOutput(item));
158747
+ if (!isRecord(value))
158748
+ return false;
158749
+ if (value.length_capped === true || value.lengthCapped === true)
158750
+ return true;
158751
+ const finishReason = value.finish_reason ?? value.finishReason;
158752
+ if (typeof finishReason === "string") {
158753
+ const normalized = finishReason.toLowerCase();
158754
+ if (normalized === "length" || normalized === "max_tokens" || normalized === "max_output_tokens") {
158755
+ return true;
158756
+ }
158757
+ }
158758
+ return Object.values(value).some((item) => hasLengthCappedOutput(item));
158759
+ }
158580
158760
 
158581
158761
  // ../plugin/src/features/magic-context/dreamer/task-executor.ts
158582
158762
  init_logger();
@@ -173234,6 +173414,10 @@ var MagicContextConfigSchema = exports_external.object({
173234
173414
  }).describe("Embedding provider configuration"),
173235
173415
  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).'),
173236
173416
  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."),
173417
+ todowrite: exports_external.object({
173418
+ 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."),
173419
+ overlay: exports_external.boolean().default(true).describe("Pi only: show the persistent todo overlay above the editor while tasks are active.")
173420
+ }).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."),
173237
173421
  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."),
173238
173422
  caveman_text_compression: exports_external.object({
173239
173423
  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%)."),
@@ -174625,6 +174809,7 @@ class OpenAICompatibleEmbeddingProvider {
174625
174809
 
174626
174810
  // ../plugin/src/features/magic-context/memory/storage-memory-embeddings.ts
174627
174811
  var saveEmbeddingStatements = new WeakMap;
174812
+ var saveEmbeddingIfHashMatchesStatements = new WeakMap;
174628
174813
  var loadAllEmbeddingsStatements = new WeakMap;
174629
174814
  var deleteEmbeddingStatements = new WeakMap;
174630
174815
  var getStoredModelIdStatements = new WeakMap;
@@ -174655,6 +174840,14 @@ function getSaveEmbeddingStatement(db) {
174655
174840
  }
174656
174841
  return stmt;
174657
174842
  }
174843
+ function getSaveEmbeddingIfHashMatchesStatement(db) {
174844
+ let stmt = saveEmbeddingIfHashMatchesStatements.get(db);
174845
+ if (!stmt) {
174846
+ 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");
174847
+ saveEmbeddingIfHashMatchesStatements.set(db, stmt);
174848
+ }
174849
+ return stmt;
174850
+ }
174658
174851
  function getLoadAllEmbeddingsStatement(db) {
174659
174852
  let stmt = loadAllEmbeddingsStatements.get(db);
174660
174853
  if (!stmt) {
@@ -174683,6 +174876,10 @@ function saveEmbedding(db, memoryId, embedding, modelId) {
174683
174876
  const blob = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
174684
174877
  getSaveEmbeddingStatement(db).run(memoryId, blob, modelId);
174685
174878
  }
174879
+ function saveEmbeddingIfHashMatches(db, memoryId, embedding, modelId, normalizedHash) {
174880
+ const blob = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
174881
+ return getSaveEmbeddingIfHashMatchesStatement(db).run(memoryId, blob, modelId, memoryId, normalizedHash).changes > 0;
174882
+ }
174686
174883
  function loadAllEmbeddings(db, projectPath, modelId) {
174687
174884
  const rows = getLoadAllEmbeddingsStatement(db).all(projectPath, modelId).filter(isEmbeddingRow);
174688
174885
  const embeddings = new Map;
@@ -175543,12 +175740,12 @@ function isUnembeddedMemoryRow(row) {
175543
175740
  if (row === null || typeof row !== "object")
175544
175741
  return false;
175545
175742
  const candidate = row;
175546
- return typeof candidate.id === "number" && typeof candidate.content === "string";
175743
+ return typeof candidate.id === "number" && typeof candidate.content === "string" && typeof candidate.normalized_hash === "string";
175547
175744
  }
175548
175745
  function getLoadUnembeddedMemoriesStatement(db) {
175549
175746
  let stmt = loadUnembeddedMemoriesStatements.get(db);
175550
175747
  if (!stmt) {
175551
- 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 ?");
175748
+ 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 ?");
175552
175749
  loadUnembeddedMemoriesStatements.set(db, stmt);
175553
175750
  }
175554
175751
  return stmt;
@@ -175571,8 +175768,9 @@ async function embedUnembeddedMemoriesForProject(db, projectIdentity, batchSize
175571
175768
  const embedding = result.vectors[index];
175572
175769
  if (!embedding)
175573
175770
  continue;
175574
- saveEmbedding(db, memory.id, embedding, result.modelId);
175575
- embeddedCount += 1;
175771
+ if (saveEmbeddingIfHashMatches(db, memory.id, embedding, result.modelId, memory.normalized_hash)) {
175772
+ embeddedCount += 1;
175773
+ }
175576
175774
  }
175577
175775
  })();
175578
175776
  return embeddedCount;
@@ -176274,25 +176472,33 @@ function getMemoryCountsByStatusStatement(db) {
176274
176472
  }
176275
176473
  return stmt;
176276
176474
  }
176277
- function insertMemory(db, input) {
176278
- const now = Date.now();
176279
- const normalizedHash = computeNormalizedHash(input.content);
176475
+ function buildInsertMemoryValues(input, normalizedHash, now, includeImportance) {
176280
176476
  const insertValues = [
176281
176477
  input.projectPath,
176282
176478
  input.category,
176283
176479
  input.content,
176284
176480
  normalizedHash
176285
176481
  ];
176286
- if (hasMemoryImportanceColumn(db)) {
176482
+ if (includeImportance) {
176287
176483
  insertValues.push(input.importance ?? 50);
176288
176484
  }
176289
176485
  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);
176290
- const result = getInsertMemoryStatement(db).run(...insertValues);
176291
- const insertedResult = result;
176292
- const inserted = getMemoryById(db, Number(insertedResult.lastInsertRowid));
176486
+ return insertValues;
176487
+ }
176488
+ function loadInsertedMemory(db, rowid) {
176489
+ const inserted = getMemoryById(db, Number(rowid));
176293
176490
  if (!inserted) {
176294
176491
  throw new Error("Failed to load inserted memory row");
176295
176492
  }
176493
+ return inserted;
176494
+ }
176495
+ function insertMemory(db, input) {
176496
+ const now = Date.now();
176497
+ const normalizedHash = computeNormalizedHash(input.content);
176498
+ const insertValues = buildInsertMemoryValues(input, normalizedHash, now, hasMemoryImportanceColumn(db));
176499
+ const result = getInsertMemoryStatement(db).run(...insertValues);
176500
+ const insertedResult = result;
176501
+ const inserted = loadInsertedMemory(db, insertedResult.lastInsertRowid);
176296
176502
  invalidateProject(input.projectPath);
176297
176503
  return inserted;
176298
176504
  }
@@ -176773,9 +176979,11 @@ import path7 from "node:path";
176773
176979
  import { promisify } from "node:util";
176774
176980
  var execFileAsync = promisify(execFile);
176775
176981
  var GIT_TIMEOUT_MS2 = 1e4;
176982
+ var defaultExecFileForVerificationPaths = async (file2, args, options) => await execFileAsync(file2, [...args], options);
176983
+ var execFileForVerificationPaths = defaultExecFileForVerificationPaths;
176776
176984
  async function runGit(cwd, args) {
176777
176985
  try {
176778
- const result = await execFileAsync("git", [...args], {
176986
+ const result = await execFileForVerificationPaths("git", args, {
176779
176987
  cwd,
176780
176988
  timeout: GIT_TIMEOUT_MS2,
176781
176989
  maxBuffer: 16 * 1024 * 1024,
@@ -177084,6 +177292,14 @@ function renewLease(db, holderId, leaseKey = DREAMING_LEASE_KEY) {
177084
177292
  return true;
177085
177293
  });
177086
177294
  }
177295
+ function runLeaseGuardedWrite(db, holderId, leaseKey, fn) {
177296
+ return runImmediate2(db, () => {
177297
+ if (!peekLeaseHolderAndExpiry(db, holderId, leaseKey)) {
177298
+ throw new Error("Dream lease lost before guarded write");
177299
+ }
177300
+ return fn();
177301
+ });
177302
+ }
177087
177303
  var LEASE_HEARTBEAT_INTERVAL_MS = 60 * 1000;
177088
177304
  function startLeaseHeartbeat(db, holderId, leaseKey, onLost, intervalMs = LEASE_HEARTBEAT_INTERVAL_MS) {
177089
177305
  let lost = false;
@@ -177878,6 +178094,39 @@ function hasShareabilitySensitiveText(text) {
177878
178094
  }
177879
178095
  }
177880
178096
 
178097
+ // ../plugin/src/features/magic-context/dreamer/manifest-parser.ts
178098
+ function extractCompleteManifestBody(text, rootName) {
178099
+ const escapedRoot = rootName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
178100
+ const rootMatch = new RegExp(`<${escapedRoot}\\b[^>]*>([\\s\\S]*?)<\\/${escapedRoot}>`, "i").exec(text);
178101
+ if (rootMatch)
178102
+ return rootMatch[1];
178103
+ const hasOpenRoot = new RegExp(`<${escapedRoot}\\b`, "i").test(text);
178104
+ const hasCloseRoot = new RegExp(`<\\/${escapedRoot}>`, "i").test(text);
178105
+ if (hasOpenRoot && !hasCloseRoot) {
178106
+ throw new Error(`${rootName} manifest missing closing root tag`);
178107
+ }
178108
+ throw new Error(`${rootName} manifest missing complete root element`);
178109
+ }
178110
+ function assertNoDuplicateManifestIds(ids, rootName) {
178111
+ const seen = new Set;
178112
+ for (const id of ids) {
178113
+ if (seen.has(id))
178114
+ throw new Error(`${rootName} manifest contains duplicate id ${id}`);
178115
+ seen.add(id);
178116
+ }
178117
+ }
178118
+ function assertManifestCoversExactly(ids, expectedIds, rootName) {
178119
+ assertNoDuplicateManifestIds(ids, rootName);
178120
+ for (const id of ids) {
178121
+ if (!expectedIds.has(id))
178122
+ throw new Error(`${rootName} manifest contains unknown id ${id}`);
178123
+ }
178124
+ for (const id of expectedIds) {
178125
+ if (!ids.includes(id))
178126
+ throw new Error(`${rootName} manifest missing id ${id}`);
178127
+ }
178128
+ }
178129
+
177881
178130
  // ../plugin/src/features/magic-context/dreamer/classify-prompt.ts
177882
178131
  var SCORING_GUIDANCE = `### How to score importance (1-100)
177883
178132
  Importance decides which memories survive when the injected memory block is over budget: high scores stay in context, low scores drop first. So the score is only useful if it **discriminates** — if most memories land in the same band, you have not classified them, you have just labelled them.
@@ -177944,14 +178193,15 @@ ${renderPool(args.memories)}`;
177944
178193
  var SCOPES = new Set(["project", "ecosystem", "universe"]);
177945
178194
  function parseClassifyManifest(text) {
177946
178195
  const out = [];
177947
- for (const m of text.matchAll(/<memory\b([^>]*)\/?>/g)) {
178196
+ const body = extractCompleteManifestBody(text, "classify");
178197
+ for (const m of body.matchAll(/<memory\b([^>]*)\/?>/g)) {
177948
178198
  const attrs = m[1];
177949
178199
  const idMatch = attrs.match(/\bid\s*=\s*"(\d+)"/);
177950
178200
  if (!idMatch)
177951
- continue;
178201
+ throw new Error("classify manifest entry missing numeric id");
177952
178202
  const id = Number.parseInt(idMatch[1], 10);
177953
178203
  if (!Number.isInteger(id))
177954
- continue;
178204
+ throw new Error("classify manifest entry missing numeric id");
177955
178205
  const entry = { id };
177956
178206
  const impMatch = attrs.match(/\bimportance\s*=\s*"(\d+)"/);
177957
178207
  if (impMatch) {
@@ -177960,18 +178210,23 @@ function parseClassifyManifest(text) {
177960
178210
  entry.importance = Math.max(1, Math.min(100, imp));
177961
178211
  }
177962
178212
  const scopeMatch = attrs.match(/\bscope\s*=\s*"([a-z]+)"/i);
177963
- if (scopeMatch && SCOPES.has(scopeMatch[1].toLowerCase())) {
177964
- entry.scope = scopeMatch[1].toLowerCase();
178213
+ if (scopeMatch) {
178214
+ const scope = scopeMatch[1].toLowerCase();
178215
+ if (!SCOPES.has(scope))
178216
+ throw new Error(`classify manifest invalid scope ${scope}`);
178217
+ entry.scope = scope;
177965
178218
  }
177966
178219
  const shareMatch = attrs.match(/\bshareable\s*=\s*"(true|false|1|0)"/i);
177967
178220
  if (shareMatch) {
177968
178221
  const v = shareMatch[1].toLowerCase();
177969
178222
  entry.shareable = v === "true" || v === "1";
177970
178223
  }
177971
- if (entry.importance !== undefined || entry.scope || entry.shareable !== undefined) {
177972
- out.push(entry);
178224
+ if (entry.importance === undefined && !entry.scope && entry.shareable === undefined) {
178225
+ throw new Error(`classify manifest entry ${id} missing classification fields`);
177973
178226
  }
178227
+ out.push(entry);
177974
178228
  }
178229
+ assertNoDuplicateManifestIds(out.map((entry) => entry.id), "classify");
177975
178230
  return out;
177976
178231
  }
177977
178232
 
@@ -178106,9 +178361,13 @@ async function classifyOneChunk(args, chunk, anchors, sliceMs, signal) {
178106
178361
  });
178107
178362
  },
178108
178363
  validateOutput: (messages) => {
178364
+ if (hasLengthCappedOutput(messages)) {
178365
+ throw new Error("classify returned length-capped output");
178366
+ }
178109
178367
  const text = extractLatestAssistantText(messages);
178110
178368
  if (!text)
178111
178369
  throw new Error("classify returned no output");
178370
+ parseClassifyManifest(text);
178112
178371
  return text;
178113
178372
  }
178114
178373
  });
@@ -178134,17 +178393,13 @@ async function classifyOneChunk(args, chunk, anchors, sliceMs, signal) {
178134
178393
  }
178135
178394
  function applyClassifications(args, chunk, manifestText) {
178136
178395
  const byId = new Map(chunk.map((m) => [m.id, m]));
178137
- const parsed = parseClassifyManifest(manifestText).filter((p) => byId.has(p.id));
178396
+ const parsed = parseClassifyManifest(manifestText);
178397
+ assertManifestCoversExactly(parsed.map((entry) => entry.id), new Set(byId.keys()), "classify");
178138
178398
  if (parsed.length === 0)
178139
178399
  return { classified: 0, changed: 0 };
178140
178400
  let classified = 0;
178141
178401
  let changed = 0;
178142
- let leaseLost = false;
178143
- args.db.transaction(() => {
178144
- if (!peekLeaseHolderAndExpiry(args.db, args.holderId, args.leaseKey)) {
178145
- leaseLost = true;
178146
- return;
178147
- }
178402
+ runLeaseGuardedWrite(args.db, args.holderId, args.leaseKey, () => {
178148
178403
  for (const p of parsed) {
178149
178404
  const memory = byId.get(p.id);
178150
178405
  if (!memory)
@@ -178159,9 +178414,7 @@ function applyClassifications(args, chunk, manifestText) {
178159
178414
  if (didChange)
178160
178415
  changed += 1;
178161
178416
  }
178162
- })();
178163
- if (leaseLost)
178164
- throw new Error("Dream lease lost during classify commit");
178417
+ });
178165
178418
  return { classified, changed };
178166
178419
  }
178167
178420
  function recordInvocation(args, startedAt, params) {
@@ -178209,9 +178462,11 @@ var SMART_NOTE_CHECK_LIVENESS_RECHECK_MS = 24 * 60 * 60 * 1000;
178209
178462
 
178210
178463
  class SmartNoteNetworkError extends Error {
178211
178464
  isSmartNoteNetworkError = true;
178212
- constructor(message) {
178465
+ terminal;
178466
+ constructor(message, options = {}) {
178213
178467
  super(message);
178214
178468
  this.name = "SmartNoteNetworkError";
178469
+ this.terminal = options.terminal ?? false;
178215
178470
  }
178216
178471
  }
178217
178472
 
@@ -178225,6 +178480,9 @@ class SmartNoteSecurityError extends Error {
178225
178480
  function isSmartNoteNetworkError(error51) {
178226
178481
  return error51 instanceof SmartNoteNetworkError || error51 instanceof Error && (error51.name === "SmartNoteNetworkError" || error51.message.includes("SmartNoteNetworkError") || error51.message.includes("SMART_NOTE_NETWORK"));
178227
178482
  }
178483
+ function isTerminalSmartNoteNetworkError(error51) {
178484
+ return error51 instanceof SmartNoteNetworkError && error51.terminal;
178485
+ }
178228
178486
  function parseSmartNoteManifest(json2) {
178229
178487
  if (!json2)
178230
178488
  return { capabilities: [] };
@@ -178254,6 +178512,7 @@ function stringArray(value) {
178254
178512
  var DNS_TIMEOUT_MS = 3000;
178255
178513
  var DEFAULT_HTTP_TIMEOUT_MS = 5000;
178256
178514
  var DEFAULT_HTTP_BODY_LIMIT_BYTES = 64 * 1024;
178515
+ var MAX_HTTP_ADDRESS_CANDIDATES = 4;
178257
178516
  var defaultResolver = {
178258
178517
  async lookup(hostname3, signal) {
178259
178518
  return await withAbortAndTimeout(dnsLookup(hostname3, { all: true, verbatim: true }), signal, DNS_TIMEOUT_MS, "DNS lookup timed out").then((rows) => rows.filter((row) => row.family === 4 || row.family === 6).map((row) => ({ address: row.address, family: row.family })));
@@ -178290,18 +178549,21 @@ async function guardedSmartNoteHttpGet(input, options) {
178290
178549
  });
178291
178550
  const timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
178292
178551
  const bodyLimitBytes = options.bodyLimitBytes ?? DEFAULT_HTTP_BODY_LIMIT_BYTES;
178552
+ const requestAddress = options.requestAddress ?? requestValidatedAddress;
178553
+ const candidates = validation.addresses.slice(0, MAX_HTTP_ADDRESS_CANDIDATES);
178293
178554
  let lastError;
178294
- for (const candidate of validation.addresses) {
178555
+ for (const candidate of candidates) {
178295
178556
  try {
178296
- return await requestValidatedAddress(validation, candidate, {
178557
+ return await requestAddress(validation, candidate, {
178297
178558
  signal: options.signal,
178298
178559
  timeoutMs,
178299
178560
  bodyLimitBytes
178300
178561
  });
178301
178562
  } catch (error51) {
178302
178563
  lastError = error51;
178303
- if (error51 instanceof SmartNoteSecurityError || options.signal.aborted)
178564
+ if (error51 instanceof SmartNoteSecurityError || options.signal.aborted || isTerminalSmartNoteNetworkError(error51)) {
178304
178565
  throw error51;
178566
+ }
178305
178567
  }
178306
178568
  }
178307
178569
  throw toNetworkError(lastError, "all validated addresses failed");
@@ -178371,7 +178633,9 @@ function requestValidatedAddress(validation, candidate, options) {
178371
178633
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
178372
178634
  bytes += buf.byteLength;
178373
178635
  if (bytes > options.bodyLimitBytes) {
178374
- request2.destroy(new SmartNoteNetworkError("SMART_NOTE_NETWORK: response body too large"));
178636
+ reject(new SmartNoteNetworkError("SMART_NOTE_NETWORK: response body too large", { terminal: true }));
178637
+ response.destroy();
178638
+ request2.destroy();
178375
178639
  return;
178376
178640
  }
178377
178641
  chunks.push(buf);
@@ -178388,10 +178652,16 @@ function requestValidatedAddress(validation, candidate, options) {
178388
178652
  resolve({ status, body: Buffer.concat(chunks).toString("utf8") });
178389
178653
  });
178390
178654
  });
178391
- const onAbort = () => request2.destroy(new SmartNoteNetworkError("SMART_NOTE_NETWORK: aborted"));
178655
+ const onAbort = () => {
178656
+ reject(new SmartNoteNetworkError("SMART_NOTE_NETWORK: aborted"));
178657
+ request2.destroy();
178658
+ };
178392
178659
  options.signal.addEventListener("abort", onAbort, { once: true });
178393
178660
  request2.on("timeout", () => {
178394
- request2.destroy(new SmartNoteNetworkError("SMART_NOTE_NETWORK: request timed out"));
178661
+ reject(new SmartNoteNetworkError("SMART_NOTE_NETWORK: request timed out", {
178662
+ terminal: true
178663
+ }));
178664
+ request2.destroy();
178395
178665
  });
178396
178666
  request2.on("error", (error51) => {
178397
178667
  options.signal.removeEventListener("abort", onAbort);
@@ -178739,6 +179009,20 @@ function withSandboxLock(fn) {
178739
179009
  var DEFAULT_TIMEOUT_MS = 2000;
178740
179010
  var DEFAULT_HEAP_LIMIT_BYTES = 8 * 1024 * 1024;
178741
179011
  var DEFAULT_STACK_LIMIT_BYTES = 512 * 1024;
179012
+ function resolveCapabilitiesForRun(options, signal) {
179013
+ if (options.capabilityFactory) {
179014
+ return options.capabilityFactory(signal);
179015
+ }
179016
+ if (options.capabilities) {
179017
+ return options.capabilities;
179018
+ }
179019
+ throw new Error("smart-note check requires capabilities");
179020
+ }
179021
+ function throwIfRunAborted(signal) {
179022
+ if (signal.aborted) {
179023
+ throw signal.reason ?? new Error("smart-note check aborted");
179024
+ }
179025
+ }
178742
179026
  async function runCompiledSmartNoteCheck(options) {
178743
179027
  return withSandboxLock(() => runCompiledSmartNoteCheckLocked(options));
178744
179028
  }
@@ -178746,17 +179030,24 @@ async function runCompiledSmartNoteCheckLocked(options) {
178746
179030
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
178747
179031
  const controller = new AbortController;
178748
179032
  const externalAbort = () => controller.abort(options.signal?.reason);
178749
- options.signal?.addEventListener("abort", externalAbort, { once: true });
179033
+ if (options.signal?.aborted) {
179034
+ externalAbort();
179035
+ } else {
179036
+ options.signal?.addEventListener("abort", externalAbort, { once: true });
179037
+ }
178750
179038
  const timer = setTimeout(() => controller.abort(new Error("smart-note check timed out")), timeoutMs);
178751
179039
  try {
179040
+ throwIfRunAborted(controller.signal);
179041
+ const capabilities = resolveCapabilitiesForRun(options, controller.signal);
178752
179042
  const deadline = Date.now() + timeoutMs;
178753
179043
  const quickjs = await getAsyncModule();
179044
+ throwIfRunAborted(controller.signal);
178754
179045
  const context = quickjs.newContext();
178755
179046
  try {
178756
179047
  context.runtime.setMemoryLimit(options.heapLimitBytes ?? DEFAULT_HEAP_LIMIT_BYTES);
178757
179048
  context.runtime.setMaxStackSize(options.stackLimitBytes ?? DEFAULT_STACK_LIMIT_BYTES);
178758
179049
  context.runtime.setInterruptHandler(() => controller.signal.aborted || Date.now() > deadline);
178759
- installCapabilityObject(context, options.capabilities);
179050
+ installCapabilityObject(context, capabilities);
178760
179051
  disableAmbientDynamicCode(context);
178761
179052
  const result = await evalCheck(context, options.compiledCheck);
178762
179053
  const checkResult = result;
@@ -178909,6 +179200,7 @@ Remember: output only the JSON object described by the system prompt.`;
178909
179200
  }
178910
179201
  }, {
178911
179202
  timeoutMs: remainingMs,
179203
+ signal: args.signal,
178912
179204
  fallbackModels: args.fallbackModels,
178913
179205
  callContext: "dreamer:smart-note-compiler",
178914
179206
  fetchOutput: async () => {
@@ -178933,7 +179225,8 @@ Remember: output only the JSON object described by the system prompt.`;
178933
179225
  }
178934
179226
  const dryRun = await runCompiledSmartNoteCheck({
178935
179227
  compiledCheck,
178936
- capabilities: args.capabilities,
179228
+ capabilityFactory: args.capabilityFactory,
179229
+ signal: args.signal,
178937
179230
  timeoutMs: 2000
178938
179231
  });
178939
179232
  if (!dryRun.ok) {
@@ -179081,7 +179374,9 @@ function nextSmartNoteCheckDueAt(cron, options = {}) {
179081
179374
  const rawNext = cron?.trim() ? nextDueAtMs(cron, now) : null;
179082
179375
  const rawDelta = rawNext ? rawNext - now : SMART_NOTE_CHECK_DEFAULT_INTERVAL_MS;
179083
179376
  const clamped = Math.min(ceilingMs, Math.max(floorMs, rawDelta));
179084
- return now + clamped + deterministicJitterMs(clamped, options.noteId, options.hash);
179377
+ const jittered = clamped + deterministicJitterMs(clamped, options.noteId, options.hash);
179378
+ const bounded = Math.min(ceilingMs, Math.max(floorMs, jittered));
179379
+ return now + bounded;
179085
179380
  }
179086
179381
  function deterministicJitterMs(intervalMs, noteId, hash2) {
179087
179382
  const max = Math.min(60000, Math.floor(intervalMs * 0.1));
@@ -179250,9 +179545,9 @@ async function runDueCompiledSmartNoteChecks(args) {
179250
179545
  try {
179251
179546
  const result = await runCompiledSmartNoteCheck({
179252
179547
  compiledCheck: note.compiledCheck,
179253
- capabilities: createSmartNoteCapabilities({
179548
+ capabilityFactory: (signal) => createSmartNoteCapabilities({
179254
179549
  projectRoot: args.projectRoot,
179255
- signal: controller.signal
179550
+ signal
179256
179551
  }),
179257
179552
  signal: controller.signal,
179258
179553
  timeoutMs: Math.min(2000, remaining)
@@ -179315,6 +179610,24 @@ var MAX_COMPILE_PER_RUN = 5;
179315
179610
  var MAX_FALLBACK_PER_RUN = 3;
179316
179611
  var MAX_COMPILATION_FAILURES = 3;
179317
179612
  var SMART_NOTE_CONFIRMATION_SYSTEM_PROMPT = 'You are a no-tool smart-note confirmation evaluator. Output only JSON shaped as {"met": boolean}. Return true only when the supplied text alone proves the condition is satisfied.';
179613
+ function createPromptAbortSignal(parent, timeoutMs, timeoutMessage) {
179614
+ const controller = new AbortController;
179615
+ const abortFromParent = () => {
179616
+ controller.abort(parent.reason ?? new Error("smart-note prompt aborted by lease loss"));
179617
+ };
179618
+ if (parent.aborted)
179619
+ abortFromParent();
179620
+ else
179621
+ parent.addEventListener("abort", abortFromParent, { once: true });
179622
+ const timer = setTimeout(() => controller.abort(new Error(timeoutMessage)), timeoutMs);
179623
+ return {
179624
+ signal: controller.signal,
179625
+ cleanup: () => {
179626
+ clearTimeout(timer);
179627
+ parent.removeEventListener("abort", abortFromParent);
179628
+ }
179629
+ };
179630
+ }
179318
179631
  async function evaluateSmartNotes(args) {
179319
179632
  const projectRoot = args.sessionDirectory ?? args.projectIdentity;
179320
179633
  const pendingAtStart = getPendingSmartNotes(args.db, args.projectIdentity).length;
@@ -179323,6 +179636,7 @@ async function evaluateSmartNotes(args) {
179323
179636
  return { surfaced: 0, pending: 0, ran: false };
179324
179637
  }
179325
179638
  let leaseLost = false;
179639
+ const leaseAbortController = new AbortController;
179326
179640
  const leaseHeld = () => !leaseLost && peekLeaseHolderAndExpiry(args.db, args.holderId, args.leaseKey);
179327
179641
  const assertLeaseHeld = (phase) => {
179328
179642
  if (!leaseHeld()) {
@@ -179332,6 +179646,7 @@ async function evaluateSmartNotes(args) {
179332
179646
  };
179333
179647
  const heartbeat = startLeaseHeartbeat(args.db, args.holderId, args.leaseKey, () => {
179334
179648
  leaseLost = true;
179649
+ leaseAbortController.abort(new Error("Dream lease lost during smart notes"));
179335
179650
  log("[dreamer] smart notes: lease lost — aborting");
179336
179651
  args.onLeaseLost?.("smart notes");
179337
179652
  });
@@ -179354,7 +179669,7 @@ async function evaluateSmartNotes(args) {
179354
179669
  break;
179355
179670
  assertLeaseHeld("compile start");
179356
179671
  didWork = true;
179357
- const compiled = await compileNote(args, note, projectRoot, assertLeaseHeld, leaseHeld);
179672
+ const compiled = await compileNote(args, note, projectRoot, assertLeaseHeld, leaseHeld, leaseAbortController.signal);
179358
179673
  if (compiled)
179359
179674
  surfaced += 1;
179360
179675
  }
@@ -179374,7 +179689,7 @@ async function evaluateSmartNotes(args) {
179374
179689
  break;
179375
179690
  assertLeaseHeld("fallback start");
179376
179691
  didWork = true;
179377
- const met = await confirmReadOnly(args, note.id, note.content, note.surfaceCondition);
179692
+ const met = await confirmReadOnly(args, note.id, note.content, note.surfaceCondition, leaseAbortController.signal);
179378
179693
  const now = Date.now();
179379
179694
  assertLeaseHeld("fallback commit");
179380
179695
  commitSmartNoteState(args.db, {
@@ -179400,9 +179715,8 @@ async function evaluateSmartNotes(args) {
179400
179715
  heartbeat.stop();
179401
179716
  }
179402
179717
  }
179403
- async function compileNote(args, note, projectRoot, assertLeaseHeld, leaseHeld) {
179404
- const controller = new AbortController;
179405
- const timer = setTimeout(() => controller.abort(new Error("smart-note compile deadline")), Math.max(1000, args.deadline - Date.now()));
179718
+ async function compileNote(args, note, projectRoot, assertLeaseHeld, leaseHeld, leaseSignal) {
179719
+ const promptSignal = createPromptAbortSignal(leaseSignal, Math.max(1000, args.deadline - Date.now()), "smart-note compile deadline");
179406
179720
  try {
179407
179721
  const result = await compileSmartNoteCheck({
179408
179722
  client: args.client,
@@ -179411,7 +179725,8 @@ async function compileNote(args, note, projectRoot, assertLeaseHeld, leaseHeld)
179411
179725
  sessionDirectory: args.sessionDirectory,
179412
179726
  projectIdentity: args.projectIdentity,
179413
179727
  note,
179414
- capabilities: createSmartNoteCapabilities({ projectRoot, signal: controller.signal }),
179728
+ capabilityFactory: (signal) => createSmartNoteCapabilities({ projectRoot, signal }),
179729
+ signal: promptSignal.signal,
179415
179730
  deadline: args.deadline,
179416
179731
  model: args.model,
179417
179732
  fallbackModels: args.fallbackModels
@@ -179456,7 +179771,7 @@ async function compileNote(args, note, projectRoot, assertLeaseHeld, leaseHeld)
179456
179771
  });
179457
179772
  return result.dryRun.met;
179458
179773
  } finally {
179459
- clearTimeout(timer);
179774
+ promptSignal.cleanup();
179460
179775
  }
179461
179776
  }
179462
179777
  async function runLivenessCheck(args, note, projectRoot, assertLeaseHeld, leaseHeld) {
@@ -179467,7 +179782,7 @@ async function runLivenessCheck(args, note, projectRoot, assertLeaseHeld, leaseH
179467
179782
  try {
179468
179783
  const result = await runCompiledSmartNoteCheck({
179469
179784
  compiledCheck: note.compiledCheck,
179470
- capabilities: createSmartNoteCapabilities({ projectRoot, signal: controller.signal }),
179785
+ capabilityFactory: (signal) => createSmartNoteCapabilities({ projectRoot, signal }),
179471
179786
  signal: controller.signal,
179472
179787
  timeoutMs: 2000
179473
179788
  });
@@ -179496,7 +179811,7 @@ async function runLivenessCheck(args, note, projectRoot, assertLeaseHeld, leaseH
179496
179811
  clearTimeout(timer);
179497
179812
  }
179498
179813
  }
179499
- async function confirmReadOnly(args, noteId, content, surfaceCondition) {
179814
+ async function confirmReadOnly(args, noteId, content, surfaceCondition, leaseSignal) {
179500
179815
  let childSessionId = null;
179501
179816
  const startedAt = Date.now();
179502
179817
  let invocationRecorded = false;
@@ -179539,42 +179854,49 @@ Note content: ${JSON.stringify(content)}
179539
179854
  Surface condition: ${JSON.stringify(surfaceCondition ?? "")}
179540
179855
 
179541
179856
  Output exactly JSON: {"met": false}`;
179542
- const run = await promptSyncWithValidatedOutputRetry(args.client, {
179543
- path: { id: childSessionId },
179544
- query: { directory: args.sessionDirectory ?? args.projectIdentity },
179545
- body: {
179546
- agent: SMART_NOTE_COMPILER_AGENT,
179547
- system: SMART_NOTE_CONFIRMATION_SYSTEM_PROMPT,
179548
- ...modelBodyField(args.model),
179549
- parts: [{ type: "text", text: prompt, synthetic: true }]
179550
- }
179551
- }, {
179552
- timeoutMs: Math.max(1000, args.deadline - Date.now()),
179553
- fallbackModels: args.fallbackModels,
179554
- callContext: "dreamer:smart-note-read-only-confirm",
179555
- fetchOutput: async () => {
179556
- const messagesResponse = await args.client.session.messages({
179557
- path: { id: childSessionId },
179558
- query: {
179559
- directory: args.sessionDirectory ?? args.projectIdentity,
179560
- limit: 20
179561
- }
179562
- });
179563
- return normalizeSDKResponse(messagesResponse, [], {
179564
- preferResponseOnMissingData: true
179565
- });
179566
- },
179567
- validateOutput: (messages) => {
179568
- const text = extractLatestAssistantText(messages) ?? "";
179569
- const match = text.match(/\{[\s\S]*\}/);
179570
- if (!match)
179571
- throw new Error("confirmation evaluator returned no JSON");
179572
- const parsed = JSON.parse(match[0]);
179573
- if (typeof parsed.met !== "boolean")
179574
- throw new Error("confirmation met missing");
179575
- return parsed.met;
179576
- }
179577
- });
179857
+ const promptSignal = createPromptAbortSignal(leaseSignal, Math.max(1000, args.deadline - Date.now()), "smart-note confirmation deadline");
179858
+ let run;
179859
+ try {
179860
+ run = await promptSyncWithValidatedOutputRetry(args.client, {
179861
+ path: { id: childSessionId },
179862
+ query: { directory: args.sessionDirectory ?? args.projectIdentity },
179863
+ body: {
179864
+ agent: SMART_NOTE_COMPILER_AGENT,
179865
+ system: SMART_NOTE_CONFIRMATION_SYSTEM_PROMPT,
179866
+ ...modelBodyField(args.model),
179867
+ parts: [{ type: "text", text: prompt, synthetic: true }]
179868
+ }
179869
+ }, {
179870
+ timeoutMs: Math.max(1000, args.deadline - Date.now()),
179871
+ signal: promptSignal.signal,
179872
+ fallbackModels: args.fallbackModels,
179873
+ callContext: "dreamer:smart-note-read-only-confirm",
179874
+ fetchOutput: async () => {
179875
+ const messagesResponse = await args.client.session.messages({
179876
+ path: { id: childSessionId },
179877
+ query: {
179878
+ directory: args.sessionDirectory ?? args.projectIdentity,
179879
+ limit: 20
179880
+ }
179881
+ });
179882
+ return normalizeSDKResponse(messagesResponse, [], {
179883
+ preferResponseOnMissingData: true
179884
+ });
179885
+ },
179886
+ validateOutput: (messages) => {
179887
+ const text = extractLatestAssistantText(messages) ?? "";
179888
+ const match = text.match(/\{[\s\S]*\}/);
179889
+ if (!match)
179890
+ throw new Error("confirmation evaluator returned no JSON");
179891
+ const parsed = JSON.parse(match[0]);
179892
+ if (typeof parsed.met !== "boolean")
179893
+ throw new Error("confirmation met missing");
179894
+ return parsed.met;
179895
+ }
179896
+ });
179897
+ } finally {
179898
+ promptSignal.cleanup();
179899
+ }
179578
179900
  recordInvocation2({ status: "completed", messages: run.output });
179579
179901
  return run.validated;
179580
179902
  } catch (error51) {
@@ -179789,19 +180111,21 @@ ${list}
179789
180111
  }
179790
180112
  function parseMapMemoriesManifest(text) {
179791
180113
  const out = [];
179792
- for (const m of text.matchAll(/<memory\b([^>]*)\/?>/g)) {
180114
+ const body = extractCompleteManifestBody(text, "mappings");
180115
+ for (const m of body.matchAll(/<memory\b([^>]*)\/?>/g)) {
179793
180116
  const attrs = m[1];
179794
180117
  const idMatch = attrs.match(/\bid\s*=\s*"(\d+)"/);
179795
180118
  if (!idMatch)
179796
- continue;
180119
+ throw new Error("mappings manifest entry missing numeric id");
179797
180120
  const id = Number.parseInt(idMatch[1], 10);
179798
180121
  if (!Number.isInteger(id))
179799
- continue;
180122
+ throw new Error("mappings manifest entry missing numeric id");
179800
180123
  const independent = /\bindependent\s*=\s*"(?:true|1)"/i.test(attrs);
179801
180124
  const filesMatch = attrs.match(/\bfiles\s*=\s*"([^"]*)"/);
179802
180125
  const files = filesMatch ? filesMatch[1].split(",").map((f) => f.trim()).filter(Boolean) : [];
179803
180126
  out.push({ id, files, independent: independent || files.length === 0 });
179804
180127
  }
180128
+ assertNoDuplicateManifestIds(out.map((entry) => entry.id), "mappings");
179805
180129
  return out;
179806
180130
  }
179807
180131
 
@@ -179890,9 +180214,13 @@ async function mapOneBatch(args, batch, sliceMs, signal) {
179890
180214
  });
179891
180215
  },
179892
180216
  validateOutput: (messages) => {
180217
+ if (hasLengthCappedOutput(messages)) {
180218
+ throw new Error("map-memories returned length-capped output");
180219
+ }
179893
180220
  const text = extractLatestAssistantText(messages);
179894
180221
  if (!text)
179895
180222
  throw new Error("map-memories returned no output");
180223
+ parseMapMemoriesManifest(text);
179896
180224
  return text;
179897
180225
  }
179898
180226
  });
@@ -179918,7 +180246,8 @@ async function mapOneBatch(args, batch, sliceMs, signal) {
179918
180246
  }
179919
180247
  async function applyBatchMappings(args, batch, manifestText) {
179920
180248
  const batchIds = new Set(batch.map((m) => m.id));
179921
- const parsed = parseMapMemoriesManifest(manifestText).filter((p) => batchIds.has(p.id));
180249
+ const parsed = parseMapMemoriesManifest(manifestText);
180250
+ assertManifestCoversExactly(parsed.map((entry) => entry.id), batchIds, "mappings");
179922
180251
  if (parsed.length === 0)
179923
180252
  return { mapped: 0, independent: 0 };
179924
180253
  const planned = [];
@@ -179940,12 +180269,7 @@ async function applyBatchMappings(args, batch, manifestText) {
179940
180269
  const now = Date.now();
179941
180270
  let mapped = 0;
179942
180271
  let independent = 0;
179943
- let leaseLost = false;
179944
- args.db.transaction(() => {
179945
- if (!peekLeaseHolderAndExpiry(args.db, args.holderId, args.leaseKey)) {
179946
- leaseLost = true;
179947
- return;
179948
- }
180272
+ runLeaseGuardedWrite(args.db, args.holderId, args.leaseKey, () => {
179949
180273
  for (const item of planned) {
179950
180274
  recordMemoryMapping(args.db, item.id, item.files, now);
179951
180275
  if (item.independent)
@@ -179953,9 +180277,7 @@ async function applyBatchMappings(args, batch, manifestText) {
179953
180277
  else
179954
180278
  mapped += 1;
179955
180279
  }
179956
- })();
179957
- if (leaseLost)
179958
- throw new Error("Dream lease lost during map-memories commit");
180280
+ });
179959
180281
  return { mapped, independent };
179960
180282
  }
179961
180283
  function recordInvocation2(args, startedAt, params) {
@@ -180183,11 +180505,7 @@ async function promotePrimers(args) {
180183
180505
  activePrimers: primers,
180184
180506
  threshold: PRIMER_CLUSTER_THRESHOLD
180185
180507
  });
180186
- args.db.transaction(() => {
180187
- if (!peekLeaseHolderAndExpiry(args.db, args.holderId, args.leaseKey)) {
180188
- leaseLost = true;
180189
- return;
180190
- }
180508
+ runLeaseGuardedWrite(args.db, args.holderId, args.leaseKey, () => {
180191
180509
  for (const cluster of clusters) {
180192
180510
  if (cluster.candidates.length === 0)
180193
180511
  continue;
@@ -180218,7 +180536,7 @@ async function promotePrimers(args) {
180218
180536
  });
180219
180537
  result.promoted += 1;
180220
180538
  }
180221
- })();
180539
+ });
180222
180540
  if (leaseLost)
180223
180541
  throw new Error("Dream lease lost during promote-primers commit");
180224
180542
  log(`[dreamer] primers: candidates=${result.candidates} promoted=${result.promoted} updated=${result.updated}`);
@@ -180486,16 +180804,9 @@ async function refreshOnePrimer(args, primer, sliceMs, signal) {
180486
180804
  log(`[dreamer] refresh-primers: primer #${primer.id} answer not committed (no investigation tool calls)`);
180487
180805
  return false;
180488
180806
  }
180489
- let leaseLost = false;
180490
- args.db.transaction(() => {
180491
- if (!peekLeaseHolderAndExpiry(args.db, args.holderId, args.leaseKey)) {
180492
- leaseLost = true;
180493
- return;
180494
- }
180807
+ runLeaseGuardedWrite(args.db, args.holderId, args.leaseKey, () => {
180495
180808
  updatePrimerAnswer(args.db, primer.id, answer);
180496
- })();
180497
- if (leaseLost)
180498
- throw new Error("Dream lease lost during refresh-primers commit");
180809
+ });
180499
180810
  return true;
180500
180811
  } catch (error51) {
180501
180812
  const desc = describeError(error51);
@@ -180692,8 +181003,7 @@ class OpenCodeRetrospectiveRawProvider {
180692
181003
  LEFT JOIN session_meta m ON m.session_id = sp.session_id
180693
181004
  WHERE sp.project_path = ? AND sp.harness = 'opencode'
180694
181005
  AND COALESCE(m.is_subagent, 0) = 0
180695
- ORDER BY sp.updated_at DESC, sp.session_id DESC
180696
- LIMIT ?`).all(projectIdentity, RETROSPECTIVE_MAX_SESSIONS_PER_RUN);
181006
+ ORDER BY sp.updated_at ASC, sp.session_id ASC`).all(projectIdentity);
180697
181007
  return rows.map((row) => ({
180698
181008
  sessionId: row.session_id,
180699
181009
  updatedAt: typeof row.updated_at === "number" ? row.updated_at : undefined
@@ -180718,6 +181028,12 @@ class OpenCodeRetrospectiveRawProvider {
180718
181028
  return { messages: [], truncated: false };
180719
181029
  }
180720
181030
  }
181031
+ readOldestMessageTimesSince(sessionIds, sinceMs) {
181032
+ const db = this.resolveDb();
181033
+ if (!db || sessionIds.length === 0)
181034
+ return new Map;
181035
+ return readOpenCodeOldestMessageTimesSince(db, sessionIds, sinceMs);
181036
+ }
180721
181037
  readUserMessagesBefore(sessionId, beforeMs, count) {
180722
181038
  const db = this.resolveDb();
180723
181039
  if (!db)
@@ -180739,13 +181055,28 @@ class OpenCodeRetrospectiveRawProvider {
180739
181055
  async function readRetrospectiveScanWindow(provider2, projectIdentity, watermarkMs, overlapUserCount, options) {
180740
181056
  const maxMessages = options?.maxMessagesPerRun ?? RETROSPECTIVE_MAX_MESSAGES_PER_RUN;
180741
181057
  const capPerSession = options?.capPerSession ?? RETROSPECTIVE_MAX_MESSAGES_PER_SESSION;
180742
- const sessionReadPageSize = Math.max(1, Math.floor(options?.maxSessionsPerRun ?? RETROSPECTIVE_MAX_SESSIONS_PER_RUN));
181058
+ const sessionLimit = Math.max(1, Math.floor(options?.maxSessionsPerRun ?? RETROSPECTIVE_MAX_SESSIONS_PER_RUN));
180743
181059
  try {
180744
- const sessions = await provider2.listProjectSessions(projectIdentity);
181060
+ const allSessions = await provider2.listProjectSessions(projectIdentity);
181061
+ const eligibleSessions = allSessions.map((session, index) => ({ session, index })).filter(({ session }) => (session.updatedAt ?? Number.POSITIVE_INFINITY) > watermarkMs);
181062
+ const oldestBySession = provider2.readOldestMessageTimesSince ? await provider2.readOldestMessageTimesSince(eligibleSessions.map(({ session }) => session.sessionId), watermarkMs) : null;
181063
+ const sessions = (oldestBySession ? eligibleSessions.filter(({ session }) => oldestBySession.has(session.sessionId)) : eligibleSessions).sort((a, b) => {
181064
+ const aFrontier = oldestBySession?.get(a.session.sessionId);
181065
+ const bFrontier = oldestBySession?.get(b.session.sessionId);
181066
+ if (aFrontier !== undefined || bFrontier !== undefined) {
181067
+ return (aFrontier ?? Number.POSITIVE_INFINITY) - (bFrontier ?? Number.POSITIVE_INFINITY) || a.index - b.index;
181068
+ }
181069
+ const aUpdated = a.session.updatedAt ?? Number.POSITIVE_INFINITY;
181070
+ const bUpdated = b.session.updatedAt ?? Number.POSITIVE_INFINITY;
181071
+ const byUpdated = aUpdated - bUpdated;
181072
+ return byUpdated || a.index - b.index;
181073
+ });
181074
+ const sessionsToRead = sessions.slice(0, sessionLimit).map(({ session }) => session);
181075
+ const firstExcludedSession = sessions[sessionLimit]?.session;
181076
+ const firstExcludedPendingTs = firstExcludedSession ? oldestBySession?.get(firstExcludedSession.sessionId) : undefined;
180745
181077
  const sinceReads = [];
180746
- for (let i = 0;i < sessions.length; i += sessionReadPageSize) {
180747
- const page = sessions.slice(i, i + sessionReadPageSize);
180748
- sinceReads.push(...await Promise.all(page.map((session) => provider2.readUserMessagesSince(session.sessionId, watermarkMs, capPerSession))));
181078
+ if (sessionsToRead.length > 0) {
181079
+ sinceReads.push(...await Promise.all(sessionsToRead.map((session) => provider2.readUserMessagesSince(session.sessionId, watermarkMs, capPerSession))));
180749
181080
  }
180750
181081
  let saturatedFrontier = Number.POSITIVE_INFINITY;
180751
181082
  for (const read of sinceReads) {
@@ -180767,9 +181098,14 @@ async function readRetrospectiveScanWindow(provider2, projectIdentity, watermark
180767
181098
  if (firstDropped) {
180768
181099
  frontier = Math.min(frontier, firstDropped.ts - 1);
180769
181100
  }
181101
+ if (typeof firstExcludedPendingTs === "number") {
181102
+ frontier = Math.min(frontier, firstExcludedPendingTs - 1);
181103
+ } else if (typeof firstExcludedSession?.updatedAt === "number") {
181104
+ frontier = Math.min(frontier, firstExcludedSession.updatedAt - 1);
181105
+ }
180770
181106
  maxScannedTs = Math.max(watermarkMs, Math.min(maxScannedTs, frontier));
180771
181107
  const keptSessionIds = new Set(keptSince.map((message) => message.sessionId));
180772
- const overlapSessions = sessions.filter((session) => keptSessionIds.has(session.sessionId));
181108
+ const overlapSessions = sessionsToRead.filter((session) => keptSessionIds.has(session.sessionId));
180773
181109
  const overlapBatches = overlapUserCount > 0 && watermarkMs > 0 ? await Promise.all(overlapSessions.map((session) => provider2.readUserMessagesBefore(session.sessionId, watermarkMs, overlapUserCount))) : [];
180774
181110
  const seen = new Set;
180775
181111
  const merged = [];
@@ -180797,6 +181133,24 @@ function readOpenCodeMessagesSince(db, sessionId, sinceMs, capPerSession) {
180797
181133
  const kept = truncated ? rows.slice(0, limit) : rows;
180798
181134
  return { messages: normalizeOpenCodeRows(db, sessionId, kept), truncated };
180799
181135
  }
181136
+ function readOpenCodeOldestMessageTimesSince(db, sessionIds, sinceMs) {
181137
+ const result = new Map;
181138
+ const uniqueIds = Array.from(new Set(sessionIds.filter((id) => id.length > 0)));
181139
+ const chunkSize = 500;
181140
+ for (let i = 0;i < uniqueIds.length; i += chunkSize) {
181141
+ const chunk = uniqueIds.slice(i, i + chunkSize);
181142
+ const placeholders4 = chunk.map(() => "?").join(", ");
181143
+ const rows = db.prepare(`SELECT session_id, MIN(time_created) AS oldest_ts
181144
+ FROM message
181145
+ WHERE time_created > ? AND session_id IN (${placeholders4})
181146
+ GROUP BY session_id`).all(sinceMs, ...chunk);
181147
+ for (const row of rows) {
181148
+ if (typeof row.oldest_ts === "number")
181149
+ result.set(row.session_id, row.oldest_ts);
181150
+ }
181151
+ }
181152
+ return result;
181153
+ }
180800
181154
  function readOpenCodeUserMessagesBefore(db, sessionId, beforeMs, count) {
180801
181155
  const want = Math.max(1, Math.floor(count));
180802
181156
  const window = Math.max(want * 5, 32);
@@ -181089,7 +181443,7 @@ async function partitionVerifyScope(args) {
181089
181443
  continue;
181090
181444
  }
181091
181445
  const files = v?.files ?? [];
181092
- const needs = files.some((file2) => !verificationFileExists(gitRoot, file2) || uncommitted.has(file2) || (changeTimes.get(file2) ?? 0) > verifiedAt);
181446
+ const needs = files.some((file2) => !verificationFileExists(gitRoot, file2) || uncommitted.has(file2) || (changeTimes.get(file2) ?? 0) >= verifiedAt - 1000);
181093
181447
  if (needs)
181094
181448
  inScope.push(toPrompt(m));
181095
181449
  else
@@ -181155,22 +181509,26 @@ function filesOf(s) {
181155
181509
  }
181156
181510
  function parseVerifyManifest(text) {
181157
181511
  const out = { verified: [], updated: [], archived: [] };
181158
- for (const m of text.matchAll(/<verified\b([^>]*)\/?>/g)) {
181512
+ const body = extractCompleteManifestBody(text, "verify");
181513
+ for (const m of body.matchAll(/<verified\b([^>]*)\/?>/g)) {
181159
181514
  const id = Number.parseInt(attrOf(m[1], "id") ?? "", 10);
181160
- if (Number.isInteger(id))
181161
- out.verified.push({ id, files: filesOf(m[1]) });
181515
+ if (!Number.isInteger(id))
181516
+ throw new Error("verify manifest entry missing numeric id");
181517
+ out.verified.push({ id, files: filesOf(m[1]) });
181162
181518
  }
181163
- for (const m of text.matchAll(/<update\b([^>]*?)(?:\/>|>([\s\S]*?)<\/update>)/g)) {
181519
+ for (const m of body.matchAll(/<update\b([^>]*?)(?:\/>|>([\s\S]*?)<\/update>)/g)) {
181164
181520
  const id = Number.parseInt(attrOf(m[1], "id") ?? "", 10);
181165
- if (Number.isInteger(id)) {
181166
- out.updated.push({ id, files: filesOf(m[1]), content: (m[2] ?? "").trim() });
181167
- }
181521
+ if (!Number.isInteger(id))
181522
+ throw new Error("verify manifest entry missing numeric id");
181523
+ out.updated.push({ id, files: filesOf(m[1]), content: (m[2] ?? "").trim() });
181168
181524
  }
181169
- for (const m of text.matchAll(/<archive\b([^>]*)\/?>/g)) {
181525
+ for (const m of body.matchAll(/<archive\b([^>]*)\/?>/g)) {
181170
181526
  const id = Number.parseInt(attrOf(m[1], "id") ?? "", 10);
181171
- if (Number.isInteger(id))
181172
- out.archived.push({ id, reason: attrOf(m[1], "reason") ?? "" });
181527
+ if (!Number.isInteger(id))
181528
+ throw new Error("verify manifest entry missing numeric id");
181529
+ out.archived.push({ id, reason: attrOf(m[1], "reason") ?? "" });
181173
181530
  }
181531
+ assertNoDuplicateManifestIds([...out.verified, ...out.updated, ...out.archived].map((entry) => entry.id), "verify");
181174
181532
  return out;
181175
181533
  }
181176
181534
 
@@ -181263,9 +181621,13 @@ async function verifyOneBatch(args, batch, sliceMs, signal) {
181263
181621
  });
181264
181622
  },
181265
181623
  validateOutput: (messages) => {
181624
+ if (hasLengthCappedOutput(messages)) {
181625
+ throw new Error("verify returned length-capped output");
181626
+ }
181266
181627
  const text = extractLatestAssistantText(messages);
181267
181628
  if (!text)
181268
181629
  throw new Error("verify returned no output");
181630
+ parseVerifyManifest(text);
181269
181631
  return text;
181270
181632
  }
181271
181633
  });
@@ -181292,24 +181654,14 @@ async function verifyOneBatch(args, batch, sliceMs, signal) {
181292
181654
  async function applyVerifyManifest(args, batch, manifestText) {
181293
181655
  const batchIds = new Set(batch.map((m) => m.id));
181294
181656
  const parsed = parseVerifyManifest(manifestText);
181657
+ assertManifestCoversExactly([...parsed.verified, ...parsed.updated, ...parsed.archived].map((entry) => entry.id), batchIds, "verify");
181295
181658
  const now = Date.now();
181296
181659
  const writes = [];
181297
- const verdictCounts = new Map;
181298
- for (const verdict of [...parsed.verified, ...parsed.updated, ...parsed.archived]) {
181299
- if (!batchIds.has(verdict.id))
181300
- continue;
181301
- verdictCounts.set(verdict.id, (verdictCounts.get(verdict.id) ?? 0) + 1);
181302
- }
181303
- const conflictingIds = new Set(Array.from(verdictCounts.entries()).filter(([, count]) => count !== 1).map(([id]) => id));
181304
181660
  for (const v of parsed.verified) {
181305
- if (!batchIds.has(v.id) || conflictingIds.has(v.id))
181306
- continue;
181307
181661
  const files = await normalizeFiles(args, v.files);
181308
181662
  writes.push({ kind: "verify", id: v.id, files });
181309
181663
  }
181310
181664
  for (const u of parsed.updated) {
181311
- if (!batchIds.has(u.id) || conflictingIds.has(u.id))
181312
- continue;
181313
181665
  const content = u.content.trim();
181314
181666
  if (!content || content.length > 20000) {
181315
181667
  const files2 = await normalizeFiles(args, u.files);
@@ -181326,8 +181678,6 @@ async function applyVerifyManifest(args, batch, manifestText) {
181326
181678
  });
181327
181679
  }
181328
181680
  for (const a of parsed.archived) {
181329
- if (!batchIds.has(a.id) || conflictingIds.has(a.id))
181330
- continue;
181331
181681
  writes.push({ kind: "archive", id: a.id, reason: a.reason });
181332
181682
  }
181333
181683
  if (writes.length === 0)
@@ -181335,12 +181685,7 @@ async function applyVerifyManifest(args, batch, manifestText) {
181335
181685
  let verified = 0;
181336
181686
  let updated = 0;
181337
181687
  let archived = 0;
181338
- let leaseLost = false;
181339
- args.db.transaction(() => {
181340
- if (!peekLeaseHolderAndExpiry(args.db, args.holderId, args.leaseKey)) {
181341
- leaseLost = true;
181342
- return;
181343
- }
181688
+ runLeaseGuardedWrite(args.db, args.holderId, args.leaseKey, () => {
181344
181689
  for (const w of writes) {
181345
181690
  const memory = getMemoryById(args.db, w.id);
181346
181691
  if (!isPrimaryMutable(memory))
@@ -181368,9 +181713,7 @@ async function applyVerifyManifest(args, batch, manifestText) {
181368
181713
  archived += 1;
181369
181714
  }
181370
181715
  }
181371
- })();
181372
- if (leaseLost)
181373
- throw new Error("Dream lease lost during verify commit");
181716
+ });
181374
181717
  return { verified, updated, archived };
181375
181718
  }
181376
181719
  async function normalizeFiles(args, rawFiles) {
@@ -181864,11 +182207,7 @@ async function runRetrospectiveTask(config2, ctx, helpers) {
181864
182207
  throw new Error("Dream lease lost during retrospective");
181865
182208
  const sourceSessionId = flagged[0]?.sessionId ?? userMessages[0]?.sessionId ?? "retrospective";
181866
182209
  const learnings = parseRetrospectiveLearnings(deepenRun.validated);
181867
- const applied = db.transaction(() => {
181868
- if (!peekLeaseHolderAndExpiry(db, holderId, leaseKey)) {
181869
- leaseLost = true;
181870
- return null;
181871
- }
182210
+ const applied = runLeaseGuardedWrite(db, holderId, leaseKey, () => {
181872
182211
  const result = applyRetrospectiveLearnings({
181873
182212
  db,
181874
182213
  projectIdentity,
@@ -181879,7 +182218,7 @@ async function runRetrospectiveTask(config2, ctx, helpers) {
181879
182218
  });
181880
182219
  recordRetrospectiveWindowProcessed(db, projectIdentity, windowKey);
181881
182220
  return result;
181882
- })();
182221
+ });
181883
182222
  if (leaseLost || !applied)
181884
182223
  throw new Error("Dream lease lost during retrospective commit");
181885
182224
  log(`[dreamer] retrospective: flagged=${flagged.length} learnings=${learnings.length} memory=${applied.memoryWritten} observations=${applied.observationsInserted} dropped=${applied.observationsDropped} rejected=${applied.rejected.length}`);
@@ -183414,11 +183753,106 @@ function migrateLegacyExperimental(rawConfig, warnings) {
183414
183753
 
183415
183754
  // ../plugin/src/config/project-security.ts
183416
183755
  var HIDDEN_AGENT_KEYS = ["historian", "dreamer", "sidekick"];
183756
+ var HISTORIAN_USER_ONLY_FIELDS = ["model", "fallback_models"];
183417
183757
  var AGENT_ESCALATION_FIELDS = ["prompt", "permission", "tools", "system_prompt"];
183418
183758
  var EMBEDDING_DESTINATION_FIELDS = ["endpoint", "provider"];
183759
+ 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.";
183760
+ 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.";
183761
+ 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.";
183419
183762
  function isPlainObject3(value) {
183420
183763
  return typeof value === "object" && value !== null && !Array.isArray(value);
183421
183764
  }
183765
+ function isValidPercentageThreshold(value) {
183766
+ return typeof value === "number" && Number.isFinite(value) && value >= 20 && value <= 80;
183767
+ }
183768
+ function isValidTokenThreshold(value) {
183769
+ return typeof value === "number" && Number.isFinite(value) && value >= 5000 && value <= 2000000;
183770
+ }
183771
+ function normalizeTrustedPercentageThresholds(value) {
183772
+ if (typeof value === "number" && Number.isFinite(value)) {
183773
+ return { defaultValue: value, overrides: new Map };
183774
+ }
183775
+ if (isPlainObject3(value) && typeof value.default === "number" && Number.isFinite(value.default)) {
183776
+ const overrides = new Map;
183777
+ for (const [key, child] of Object.entries(value)) {
183778
+ if (key === "default")
183779
+ continue;
183780
+ if (typeof child === "number" && Number.isFinite(child)) {
183781
+ overrides.set(key, child);
183782
+ }
183783
+ }
183784
+ return { defaultValue: value.default, overrides };
183785
+ }
183786
+ return { defaultValue: DEFAULT_EXECUTE_THRESHOLD_PERCENTAGE, overrides: new Map };
183787
+ }
183788
+ function normalizeTrustedTokenThresholds(value) {
183789
+ if (!isPlainObject3(value)) {
183790
+ return { defaultValue: undefined, overrides: new Map };
183791
+ }
183792
+ const overrides = new Map;
183793
+ for (const [key, child] of Object.entries(value)) {
183794
+ if (key === "default")
183795
+ continue;
183796
+ if (typeof child === "number" && Number.isFinite(child)) {
183797
+ overrides.set(key, child);
183798
+ }
183799
+ }
183800
+ return {
183801
+ defaultValue: typeof value.default === "number" && Number.isFinite(value.default) ? value.default : undefined,
183802
+ overrides
183803
+ };
183804
+ }
183805
+ function clonePercentageThresholds(value) {
183806
+ return {
183807
+ defaultValue: value.defaultValue,
183808
+ overrides: new Map(value.overrides)
183809
+ };
183810
+ }
183811
+ function cloneTokenThresholds(value) {
183812
+ return {
183813
+ defaultValue: value.defaultValue,
183814
+ overrides: new Map(value.overrides)
183815
+ };
183816
+ }
183817
+ function percentageThresholdsEqual(left, right) {
183818
+ if (left.defaultValue !== right.defaultValue)
183819
+ return false;
183820
+ if (left.overrides.size !== right.overrides.size)
183821
+ return false;
183822
+ for (const [key, value] of left.overrides) {
183823
+ if (right.overrides.get(key) !== value)
183824
+ return false;
183825
+ }
183826
+ return true;
183827
+ }
183828
+ function setMergedPercentageThreshold(mergedRaw, value) {
183829
+ if (value.overrides.size === 0) {
183830
+ mergedRaw.execute_threshold_percentage = value.defaultValue;
183831
+ return;
183832
+ }
183833
+ const serialized = { default: value.defaultValue };
183834
+ for (const [key, threshold] of value.overrides) {
183835
+ serialized[key] = threshold;
183836
+ }
183837
+ mergedRaw.execute_threshold_percentage = serialized;
183838
+ }
183839
+ function setMergedTokenThreshold(mergedRaw, value) {
183840
+ if (value.defaultValue === undefined && value.overrides.size === 0) {
183841
+ delete mergedRaw.execute_threshold_tokens;
183842
+ return;
183843
+ }
183844
+ const serialized = {};
183845
+ if (value.defaultValue !== undefined) {
183846
+ serialized.default = value.defaultValue;
183847
+ }
183848
+ for (const [key, threshold] of value.overrides) {
183849
+ serialized[key] = threshold;
183850
+ }
183851
+ mergedRaw.execute_threshold_tokens = serialized;
183852
+ }
183853
+ function makeProjectThresholdWarning(field, reason) {
183854
+ return `Ignoring ${field} from project config (${reason})`;
183855
+ }
183422
183856
  function stripUnsafeProjectConfigFields(projectRaw) {
183423
183857
  const warnings = [];
183424
183858
  if ("auto_update" in projectRaw) {
@@ -183446,6 +183880,19 @@ function stripUnsafeProjectConfigFields(projectRaw) {
183446
183880
  warnings.push(`Ignoring embedding.${removed.join("/")} from project config ` + "(security: a repository cannot choose where private text is embedded).");
183447
183881
  }
183448
183882
  }
183883
+ const historian = projectRaw.historian;
183884
+ if (isPlainObject3(historian)) {
183885
+ const removed = [];
183886
+ for (const field of HISTORIAN_USER_ONLY_FIELDS) {
183887
+ if (field in historian) {
183888
+ delete historian[field];
183889
+ removed.push(field);
183890
+ }
183891
+ }
183892
+ if (removed.length > 0) {
183893
+ warnings.push(`Ignoring historian.${removed.join("/")} from project config ` + "(security: historian model selection is user-level only; a repository cannot force extra compaction cost).");
183894
+ }
183895
+ }
183449
183896
  for (const agentKey of HIDDEN_AGENT_KEYS) {
183450
183897
  const block = projectRaw[agentKey];
183451
183898
  if (!isPlainObject3(block))
@@ -183463,6 +183910,101 @@ function stripUnsafeProjectConfigFields(projectRaw) {
183463
183910
  }
183464
183911
  return warnings;
183465
183912
  }
183913
+ function constrainProjectThresholdOverrides(args) {
183914
+ const warnings = [];
183915
+ const basePercentage = normalizeTrustedPercentageThresholds(args.trustedBaseConfig.execute_threshold_percentage);
183916
+ const baseTokens = normalizeTrustedTokenThresholds(args.trustedBaseConfig.execute_threshold_tokens);
183917
+ if ("execute_threshold_percentage" in args.projectRaw) {
183918
+ const projectValue = args.projectRaw.execute_threshold_percentage;
183919
+ if (isValidPercentageThreshold(projectValue)) {
183920
+ const constrained = clonePercentageThresholds(basePercentage);
183921
+ constrained.defaultValue = Math.max(basePercentage.defaultValue, projectValue);
183922
+ for (const [modelKey, threshold] of basePercentage.overrides) {
183923
+ const raisedThreshold = Math.max(threshold, projectValue);
183924
+ if (raisedThreshold === constrained.defaultValue) {
183925
+ constrained.overrides.delete(modelKey);
183926
+ } else {
183927
+ constrained.overrides.set(modelKey, raisedThreshold);
183928
+ }
183929
+ }
183930
+ setMergedPercentageThreshold(args.mergedRaw, constrained);
183931
+ if (percentageThresholdsEqual(constrained, basePercentage)) {
183932
+ warnings.push(makeProjectThresholdWarning("execute_threshold_percentage", PERCENTAGE_THRESHOLD_REASON));
183933
+ }
183934
+ } else if (isPlainObject3(projectValue)) {
183935
+ const constrained = clonePercentageThresholds(basePercentage);
183936
+ let touchedValidEntry = false;
183937
+ if (isValidPercentageThreshold(projectValue.default)) {
183938
+ touchedValidEntry = true;
183939
+ if (projectValue.default > basePercentage.defaultValue) {
183940
+ constrained.defaultValue = projectValue.default;
183941
+ } else {
183942
+ warnings.push(makeProjectThresholdWarning("execute_threshold_percentage.default", PERCENTAGE_THRESHOLD_REASON));
183943
+ }
183944
+ }
183945
+ for (const [modelKey, rawValue] of Object.entries(projectValue)) {
183946
+ if (modelKey === "default")
183947
+ continue;
183948
+ if (!isValidPercentageThreshold(rawValue))
183949
+ continue;
183950
+ touchedValidEntry = true;
183951
+ const baseValue = basePercentage.overrides.get(modelKey) ?? basePercentage.defaultValue;
183952
+ if (rawValue > baseValue) {
183953
+ if (rawValue === constrained.defaultValue) {
183954
+ constrained.overrides.delete(modelKey);
183955
+ } else {
183956
+ constrained.overrides.set(modelKey, rawValue);
183957
+ }
183958
+ } else {
183959
+ warnings.push(makeProjectThresholdWarning(`execute_threshold_percentage.${modelKey}`, PERCENTAGE_THRESHOLD_REASON));
183960
+ }
183961
+ }
183962
+ if (touchedValidEntry) {
183963
+ setMergedPercentageThreshold(args.mergedRaw, constrained);
183964
+ }
183965
+ }
183966
+ }
183967
+ if ("execute_threshold_tokens" in args.projectRaw && isPlainObject3(args.projectRaw.execute_threshold_tokens)) {
183968
+ const projectValue = args.projectRaw.execute_threshold_tokens;
183969
+ const constrained = cloneTokenThresholds(baseTokens);
183970
+ let touchedValidEntry = false;
183971
+ if (isValidTokenThreshold(projectValue.default)) {
183972
+ touchedValidEntry = true;
183973
+ if (baseTokens.defaultValue === undefined) {
183974
+ warnings.push(makeProjectThresholdWarning("execute_threshold_tokens.default", TOKEN_THRESHOLD_INTRODUCTION_REASON));
183975
+ } else if (projectValue.default > baseTokens.defaultValue) {
183976
+ constrained.defaultValue = projectValue.default;
183977
+ } else {
183978
+ warnings.push(makeProjectThresholdWarning("execute_threshold_tokens.default", TOKEN_THRESHOLD_REASON));
183979
+ }
183980
+ }
183981
+ for (const [modelKey, rawValue] of Object.entries(projectValue)) {
183982
+ if (modelKey === "default")
183983
+ continue;
183984
+ if (!isValidTokenThreshold(rawValue))
183985
+ continue;
183986
+ touchedValidEntry = true;
183987
+ const baseValue = baseTokens.overrides.get(modelKey) ?? baseTokens.defaultValue;
183988
+ if (baseValue === undefined) {
183989
+ warnings.push(makeProjectThresholdWarning(`execute_threshold_tokens.${modelKey}`, TOKEN_THRESHOLD_INTRODUCTION_REASON));
183990
+ continue;
183991
+ }
183992
+ if (rawValue > baseValue) {
183993
+ if (rawValue === constrained.defaultValue) {
183994
+ constrained.overrides.delete(modelKey);
183995
+ } else {
183996
+ constrained.overrides.set(modelKey, rawValue);
183997
+ }
183998
+ } else {
183999
+ warnings.push(makeProjectThresholdWarning(`execute_threshold_tokens.${modelKey}`, TOKEN_THRESHOLD_REASON));
184000
+ }
184001
+ }
184002
+ if (touchedValidEntry) {
184003
+ setMergedTokenThreshold(args.mergedRaw, constrained);
184004
+ }
184005
+ }
184006
+ return warnings;
184007
+ }
183466
184008
  function normalizeEndpoint3(value) {
183467
184009
  if (typeof value !== "string")
183468
184010
  return;
@@ -183830,6 +184372,7 @@ function loadPiConfig(opts = {}) {
183830
184372
  return a.scope === "user" ? -1 : 1;
183831
184373
  });
183832
184374
  const userRaw = mergeFiles.find((f) => f.scope === "user")?.config;
184375
+ const trustedBaseConfig = parsePiConfig(userRaw ?? {}).config;
183833
184376
  for (const loaded of mergeFiles) {
183834
184377
  const prefix = loaded.scope === "user" ? "[user config]" : "[project config]";
183835
184378
  warnings.push(...loaded.warnings.map((warning) => `${prefix} ${warning}`));
@@ -183842,6 +184385,13 @@ function loadPiConfig(opts = {}) {
183842
184385
  for (const warning of dropInheritedEmbeddingKeyOnRedirect(projectRaw, rawConfig, userRaw)) {
183843
184386
  warnings.push(`${prefix} ${warning}`);
183844
184387
  }
184388
+ for (const warning of constrainProjectThresholdOverrides({
184389
+ mergedRaw: rawConfig,
184390
+ projectRaw,
184391
+ trustedBaseConfig
184392
+ })) {
184393
+ warnings.push(`${prefix} ${warning}`);
184394
+ }
183845
184395
  } else {
183846
184396
  rawConfig = mergeRawConfigs(rawConfig, loaded.config);
183847
184397
  }
@@ -183940,6 +184490,7 @@ function loadPiConfigDetailed(opts = {}) {
183940
184490
  return a.scope === "user" ? -1 : 1;
183941
184491
  });
183942
184492
  const userRaw = mergeFiles.find((f) => f.scope === "user")?.config;
184493
+ const trustedBaseConfig = parsePiConfig(userRaw ?? {}).config;
183943
184494
  for (const loaded of mergeFiles) {
183944
184495
  const prefix = loaded.scope === "user" ? "[user config]" : "[project config]";
183945
184496
  warnings.push(...loaded.warnings.map((warning) => `${prefix} ${warning}`));
@@ -183952,6 +184503,13 @@ function loadPiConfigDetailed(opts = {}) {
183952
184503
  for (const warning of dropInheritedEmbeddingKeyOnRedirect(projectRaw, rawConfig, userRaw)) {
183953
184504
  warnings.push(`${prefix} ${warning}`);
183954
184505
  }
184506
+ for (const warning of constrainProjectThresholdOverrides({
184507
+ mergedRaw: rawConfig,
184508
+ projectRaw,
184509
+ trustedBaseConfig
184510
+ })) {
184511
+ warnings.push(`${prefix} ${warning}`);
184512
+ }
183955
184513
  } else {
183956
184514
  rawConfig = mergeRawConfigs(rawConfig, loaded.config);
183957
184515
  }
@@ -184030,6 +184588,8 @@ function isMidTurnPi(event, _sessionId) {
184030
184588
  }
184031
184589
  if (latestAssistant === null)
184032
184590
  return false;
184591
+ if (hasRealUserAfter(messages, latestAssistantIndex))
184592
+ return false;
184033
184593
  if (latestAssistant.stopReason === "toolUse")
184034
184594
  return true;
184035
184595
  const toolCallIds = getToolCallIds(latestAssistant.content);
@@ -184052,6 +184612,15 @@ function isMidTurnPi(event, _sessionId) {
184052
184612
  }
184053
184613
  return false;
184054
184614
  }
184615
+ function hasRealUserAfter(messages, latestAssistantIndex) {
184616
+ for (const msg of messages.slice(latestAssistantIndex + 1)) {
184617
+ if (msg === null || typeof msg !== "object")
184618
+ continue;
184619
+ if (msg.role === "user")
184620
+ return true;
184621
+ }
184622
+ return false;
184623
+ }
184055
184624
  function getToolCallIds(content) {
184056
184625
  const ids = new Set;
184057
184626
  if (!Array.isArray(content))
@@ -184374,7 +184943,7 @@ class PiRetrospectiveRawProvider {
184374
184943
  updatedAt: typeof info.modified === "number" ? info.modified : undefined
184375
184944
  });
184376
184945
  }
184377
- return result.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
184946
+ return result.sort((a, b) => (a.updatedAt ?? 0) - (b.updatedAt ?? 0));
184378
184947
  }
184379
184948
  async readUserMessagesSince(sessionId, sinceMs, capPerSession) {
184380
184949
  const all = await this.loadUserEntries(sessionId);
@@ -184385,6 +184954,15 @@ class PiRetrospectiveRawProvider {
184385
184954
  truncated: eligible.length > limit
184386
184955
  };
184387
184956
  }
184957
+ async readOldestMessageTimesSince(sessionIds, sinceMs) {
184958
+ const out = new Map;
184959
+ for (const sessionId of sessionIds) {
184960
+ const oldest = (await this.loadUserEntries(sessionId)).filter((message) => message.ts > sinceMs).sort((a, b) => a.ts - b.ts || a.ordinal - b.ordinal)[0];
184961
+ if (oldest)
184962
+ out.set(sessionId, oldest.ts);
184963
+ }
184964
+ return out;
184965
+ }
184388
184966
  async readUserMessagesBefore(sessionId, beforeMs, count) {
184389
184967
  const all = await this.loadUserEntries(sessionId);
184390
184968
  return all.filter((entry) => entry.ts <= beforeMs).sort((a, b) => a.ts - b.ts || a.ordinal - b.ordinal).slice(-Math.max(1, Math.floor(count)));
@@ -184714,6 +185292,7 @@ function registerCtxDreamCommand(pi, deps) {
184714
185292
  projectDir: deps.projectDir,
184715
185293
  projectIdentity: deps.projectIdentity
184716
185294
  };
185295
+ const dreamerEnabled = deps.resolveDreamerEnabled?.(ctx) ?? deps.dreamerEnabled;
184717
185296
  deps.onProjectSeen?.(project.projectIdentity);
184718
185297
  const requested = typeof args === "string" ? args.trim() : String(args ?? "").trim();
184719
185298
  let task;
@@ -184733,7 +185312,7 @@ Unknown task "${requested}".`,
184733
185312
  }
184734
185313
  task = requested;
184735
185314
  }
184736
- if (deps.dreamerEnabled === false) {
185315
+ if (dreamerEnabled === false) {
184737
185316
  sendCtxStatusMessage(pi, {
184738
185317
  title: "/ctx-dream",
184739
185318
  text: "## /ctx-dream\n\nDreamer is disabled for this project (`dreamer.disable=true`).",
@@ -184848,6 +185427,8 @@ function formatEmbedStatusText(coverage, drain) {
184848
185427
  }
184849
185428
 
184850
185429
  // src/commands/ctx-embed.ts
185430
+ var EMBED_PROGRESS_COMPARTMENT_STEP = 8;
185431
+ var EMBED_PROGRESS_MIN_INTERVAL_MS = 1e4;
184851
185432
  function clearPiEmbedSessionState(sessionId) {
184852
185433
  embedPauseBySession.delete(sessionId);
184853
185434
  const ctrl = embedRunStateBySession.get(sessionId);
@@ -184857,7 +185438,7 @@ function clearPiEmbedSessionState(sessionId) {
184857
185438
  }
184858
185439
  autoEmbedAttemptedBySession.delete(sessionId);
184859
185440
  }
184860
- async function runEmbedDrain(db, projectIdentity, sessionId) {
185441
+ async function runEmbedDrain(db, projectIdentity, sessionId, options = {}) {
184861
185442
  const activeCtrl = embedRunStateBySession.get(sessionId);
184862
185443
  if (activeCtrl && !activeCtrl.signal.aborted) {
184863
185444
  return {
@@ -184873,10 +185454,46 @@ Embedding is already running for this session.`,
184873
185454
  prior.abort();
184874
185455
  const controller = new AbortController;
184875
185456
  embedRunStateBySession.set(sessionId, controller);
185457
+ const now = options.now ?? Date.now;
185458
+ let startEmitted = false;
185459
+ let lastProgressEmbedded = 0;
185460
+ let lastProgressAt = 0;
185461
+ const emitProgress = (progress) => {
185462
+ if (progress.total <= 0)
185463
+ return;
185464
+ if (!startEmitted) {
185465
+ startEmitted = true;
185466
+ lastProgressAt = now();
185467
+ options.onStatus?.({
185468
+ text: `## /ctx-embed
185469
+
185470
+ Embedding ${progress.total} compartment${progress.total === 1 ? "" : "s"} of history…`,
185471
+ level: "info"
185472
+ });
185473
+ return;
185474
+ }
185475
+ if (progress.embedded <= 0 || progress.embedded >= progress.total)
185476
+ return;
185477
+ const currentTime = now();
185478
+ const enoughCompartments = progress.embedded - lastProgressEmbedded >= EMBED_PROGRESS_COMPARTMENT_STEP;
185479
+ const enoughTime = currentTime - lastProgressAt >= EMBED_PROGRESS_MIN_INTERVAL_MS && progress.embedded > lastProgressEmbedded;
185480
+ if (!enoughCompartments && !enoughTime)
185481
+ return;
185482
+ lastProgressEmbedded = progress.embedded;
185483
+ lastProgressAt = currentTime;
185484
+ options.onStatus?.({
185485
+ text: `## /ctx-embed
185486
+
185487
+ Embedded ${progress.embedded}/${progress.total} compartments so far…`,
185488
+ level: "info"
185489
+ });
185490
+ };
184876
185491
  let outcome;
184877
185492
  try {
184878
185493
  outcome = await embedSessionCompartmentChunks(db, projectIdentity, sessionId, {
184879
- signal: controller.signal
185494
+ signal: controller.signal,
185495
+ onProgress: emitProgress,
185496
+ ...options.batchSize !== undefined ? { batchSize: options.batchSize } : {}
184880
185497
  });
184881
185498
  } finally {
184882
185499
  if (embedRunStateBySession.get(sessionId) === controller) {
@@ -184949,6 +185566,7 @@ No active Pi session is available.`,
184949
185566
  projectDir: deps.projectDir,
184950
185567
  projectIdentity: deps.projectIdentity
184951
185568
  };
185569
+ const memoryEnabled = deps.resolveMemoryEnabled?.(ctx) ?? deps.memoryEnabled;
184952
185570
  const sub = args.trim().toLowerCase();
184953
185571
  if (sub === "pause") {
184954
185572
  embedPauseBySession.add(sessionId);
@@ -184965,7 +185583,7 @@ Paused at ${cov.session.embedded}/${cov.session.total} compartments embedded.`,
184965
185583
  });
184966
185584
  return;
184967
185585
  }
184968
- if (deps.memoryEnabled === false) {
185586
+ if (memoryEnabled === false) {
184969
185587
  sendCtxStatusMessage(pi, {
184970
185588
  title: "/ctx-embed",
184971
185589
  text: `## /ctx-embed
@@ -184977,7 +185595,12 @@ Memory is disabled for this project, so there is no semantic embedding to backfi
184977
185595
  }
184978
185596
  await ensureProjectRegisteredFromPiDirectory(project.projectDir, deps.db);
184979
185597
  if (sub === "start") {
184980
- const { text, level } = await runEmbedDrain(deps.db, project.projectIdentity, sessionId);
185598
+ const { text, level } = await runEmbedDrain(deps.db, project.projectIdentity, sessionId, {
185599
+ onStatus: (status) => sendCtxStatusMessage(pi, {
185600
+ title: "/ctx-embed",
185601
+ ...status
185602
+ })
185603
+ });
184981
185604
  sendCtxStatusMessage(pi, { title: "/ctx-embed", text, level });
184982
185605
  return;
184983
185606
  }
@@ -186099,6 +186722,8 @@ init_logger();
186099
186722
  // ../plugin/src/hooks/magic-context/read-session-true-raw-tokens.ts
186100
186723
  var MAX_MESSAGE_CACHE_ENTRIES = 1e5;
186101
186724
  var MAX_MESSAGE_CACHE_KEY_BYTES = 64 * 1024 * 1024;
186725
+ var FNV1A_32_OFFSET = 2166136261;
186726
+ var FNV1A_32_PRIME = 16777619;
186102
186727
  var messageEstimateCache = new Map;
186103
186728
  var messageEstimateCacheBytes = 0;
186104
186729
  var EMPTY_BREAKDOWN = {
@@ -186201,6 +186826,23 @@ function recursiveByteLength(value) {
186201
186826
  }
186202
186827
  return String(value).length;
186203
186828
  }
186829
+ function updateFnv1a32(hash2, text) {
186830
+ let next = hash2;
186831
+ for (let index = 0;index < text.length; index += 1) {
186832
+ next ^= text.charCodeAt(index);
186833
+ next = Math.imul(next, FNV1A_32_PRIME) >>> 0;
186834
+ }
186835
+ return next;
186836
+ }
186837
+ function contentStringsHash(fields) {
186838
+ let hash2 = FNV1A_32_OFFSET;
186839
+ for (const field of fields) {
186840
+ hash2 = updateFnv1a32(hash2, `${field.length}:`);
186841
+ hash2 = updateFnv1a32(hash2, field);
186842
+ hash2 = updateFnv1a32(hash2, "\x00");
186843
+ }
186844
+ return hash2.toString(16).padStart(8, "0");
186845
+ }
186204
186846
  function rawPartVersion(part) {
186205
186847
  return part.__magicContextPartUpdatedAt ?? part.updated_at ?? part.updatedAt ?? part.version ?? part.revision ?? "";
186206
186848
  }
@@ -186548,13 +187190,12 @@ function buildTrueRawTokenIndex(sessionId, messages, options) {
186548
187190
  function partContentFingerprint(part) {
186549
187191
  if (!isRecord(part))
186550
187192
  return `${typeof part}:${recursiveByteLength(part)}`;
186551
- const type = partType(part);
186552
187193
  const tool = toolSignalFromPart(part);
186553
187194
  if (tool) {
186554
- return `${type}:${tool.callId}:${tool.inputText.length}:${tool.outputText.length}`;
187195
+ return contentStringsHash([tool.inputText, tool.outputText]);
186555
187196
  }
186556
187197
  const text = firstStringField(part, ["text", "thinking", "reasoning", "content", "url"]) ?? "";
186557
- return `${type}:${text.length}`;
187198
+ return contentStringsHash([text]);
186558
187199
  }
186559
187200
  function computeRawRangeFingerprint(messages, startInclusive, endExclusive) {
186560
187201
  const pieces = [];
@@ -188010,6 +188651,11 @@ function renderMemoryBlockV2(memories, wrapper = "project-memory", renderOptions
188010
188651
  `);
188011
188652
  }
188012
188653
 
188654
+ // ../plugin/src/hooks/magic-context/sentinel.ts
188655
+ function modelAcceptsEmptyContent(providerID) {
188656
+ return providerID === "anthropic";
188657
+ }
188658
+
188013
188659
  // ../plugin/src/hooks/magic-context/supersession-reclaim.ts
188014
188660
  var TODOWRITE_KEEP = 1;
188015
188661
  var CTX_REDUCE_KEEP = 5;
@@ -188806,24 +189452,46 @@ function getMessageSearchStatementWithCutoff(db) {
188806
189452
  return stmt;
188807
189453
  }
188808
189454
  var ftsRowCountStatements = new WeakMap;
189455
+ var ftsRowCountStatementsWithCutoff = new WeakMap;
188809
189456
  var ftsMatchCountStatements = new WeakMap;
188810
- function getSessionFtsRowCount(db, sessionId) {
188811
- let stmt = ftsRowCountStatements.get(db);
188812
- if (!stmt) {
188813
- stmt = db.prepare("SELECT COUNT(*) AS n FROM message_history_fts WHERE session_id = ?");
188814
- ftsRowCountStatements.set(db, stmt);
188815
- }
188816
- const row = stmt.get(sessionId);
189457
+ var ftsMatchCountStatementsWithCutoff = new WeakMap;
189458
+ function getSessionFtsRowCount(db, sessionId, cutoff) {
189459
+ const stmt = cutoff === null ? (() => {
189460
+ let cached2 = ftsRowCountStatements.get(db);
189461
+ if (!cached2) {
189462
+ cached2 = db.prepare("SELECT COUNT(*) AS n FROM message_history_fts WHERE session_id = ?");
189463
+ ftsRowCountStatements.set(db, cached2);
189464
+ }
189465
+ return cached2;
189466
+ })() : (() => {
189467
+ let cached2 = ftsRowCountStatementsWithCutoff.get(db);
189468
+ if (!cached2) {
189469
+ cached2 = db.prepare("SELECT COUNT(*) AS n FROM message_history_fts WHERE session_id = ? AND CAST(message_ordinal AS INTEGER) <= ?");
189470
+ ftsRowCountStatementsWithCutoff.set(db, cached2);
189471
+ }
189472
+ return cached2;
189473
+ })();
189474
+ const row = cutoff === null ? stmt.get(sessionId) : stmt.get(sessionId, cutoff);
188817
189475
  return typeof row?.n === "number" ? row.n : 0;
188818
189476
  }
188819
- function countSessionFtsMatches(db, sessionId, ftsQuery) {
188820
- let stmt = ftsMatchCountStatements.get(db);
188821
- if (!stmt) {
188822
- stmt = db.prepare("SELECT COUNT(*) AS n FROM message_history_fts WHERE session_id = ? AND message_history_fts MATCH ?");
188823
- ftsMatchCountStatements.set(db, stmt);
188824
- }
189477
+ function countSessionFtsMatches(db, sessionId, ftsQuery, cutoff) {
189478
+ const stmt = cutoff === null ? (() => {
189479
+ let cached2 = ftsMatchCountStatements.get(db);
189480
+ if (!cached2) {
189481
+ cached2 = db.prepare("SELECT COUNT(*) AS n FROM message_history_fts WHERE session_id = ? AND message_history_fts MATCH ?");
189482
+ ftsMatchCountStatements.set(db, cached2);
189483
+ }
189484
+ return cached2;
189485
+ })() : (() => {
189486
+ let cached2 = ftsMatchCountStatementsWithCutoff.get(db);
189487
+ if (!cached2) {
189488
+ 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) <= ?");
189489
+ ftsMatchCountStatementsWithCutoff.set(db, cached2);
189490
+ }
189491
+ return cached2;
189492
+ })();
188825
189493
  try {
188826
- const row = stmt.get(sessionId, ftsQuery);
189494
+ const row = cutoff === null ? stmt.get(sessionId, ftsQuery) : stmt.get(sessionId, ftsQuery, cutoff);
188827
189495
  return typeof row?.n === "number" ? row.n : 0;
188828
189496
  } catch {
188829
189497
  return 0;
@@ -189073,7 +189741,7 @@ function searchMessages(args) {
189073
189741
  role: row.role
189074
189742
  }));
189075
189743
  }
189076
- const corpusSize = getSessionFtsRowCount(args.db, args.sessionId);
189744
+ const corpusSize = getSessionFtsRowCount(args.db, args.sessionId, cutoff);
189077
189745
  const queryLists = [];
189078
189746
  if (baseQuery.length > 0) {
189079
189747
  queryLists.push({
@@ -189086,7 +189754,7 @@ function searchMessages(args) {
189086
189754
  const probeQuery = sanitizeFtsQuery(probe);
189087
189755
  if (probeQuery.length === 0)
189088
189756
  continue;
189089
- const df = countSessionFtsMatches(args.db, args.sessionId, probeQuery);
189757
+ const df = countSessionFtsMatches(args.db, args.sessionId, probeQuery, cutoff);
189090
189758
  const weight = probeDiscriminationWeight(df, corpusSize);
189091
189759
  probeWeights.set(probe, weight);
189092
189760
  queryLists.push({
@@ -190510,8 +191178,8 @@ function applyPiHeuristicCleanup(sessionId, db, targets, piMessages, config2, pr
190510
191178
  sessionLog(sessionId, `emergency tiered drop skipped: ${plan.reason}`);
190511
191179
  }
190512
191180
  }
190513
- const staleReduce = collectStaleReduceCallIds(piMessages, buildMessageIdToMaxTagFromTargets(targets), toolAgeCutoff, resolveStableId);
190514
- if (staleReduce.composite.size > 0 || staleReduce.bareCallIds.size > 0) {
191181
+ const staleReduce = config2.staleReduceStripEnabled ? collectStaleReduceCallIds(piMessages, buildMessageIdToMaxTagFromTargets(targets), toolAgeCutoff, resolveStableId) : { composite: new Set, bareCallIds: new Set };
191182
+ if (config2.staleReduceStripEnabled && (staleReduce.composite.size > 0 || staleReduce.bareCallIds.size > 0)) {
190515
191183
  db.transaction(() => {
190516
191184
  for (const tag of tags) {
190517
191185
  if (tag.status !== "active")
@@ -194792,8 +195460,122 @@ function buildReferenceBlocks(args) {
194792
195460
  init_logger();
194793
195461
  var HISTORIAN_AGENT_NAME = "magic-context-historian";
194794
195462
  var DEFAULT_HISTORIAN_TIMEOUT_MS2 = 120000;
195463
+ var MAX_HISTORIAN_RETRIES = 2;
194795
195464
  var HISTORIAN_ALERT_COOLDOWN_MS = 60 * 1000;
194796
195465
  var lastHistorianAlertBySession = new Map;
195466
+ function getHistorianRetryBackoffMs(retryIndex) {
195467
+ if (retryIndex === 0) {
195468
+ return 2000 + Math.floor(Math.random() * 1001);
195469
+ }
195470
+ return 6000 + Math.floor(Math.random() * 2001);
195471
+ }
195472
+ function isTransientHistorianPromptError(message) {
195473
+ const normalized = message.toLowerCase();
195474
+ if (normalized.includes("invalid request") || normalized.includes("bad request") || normalized.includes("unauthorized") || normalized.includes("forbidden") || normalized.includes("authentication") || normalized.includes("auth") || normalized.includes(" 400") || normalized.startsWith("400")) {
195475
+ return false;
195476
+ }
195477
+ return [
195478
+ "429",
195479
+ "rate limit",
195480
+ "timeout",
195481
+ "econnreset",
195482
+ "etimedout",
195483
+ "503",
195484
+ "502",
195485
+ "500",
195486
+ "overloaded"
195487
+ ].some((token) => normalized.includes(token));
195488
+ }
195489
+ function isTransientHistorianRunFailure(result) {
195490
+ if (result.reason === "abort")
195491
+ return false;
195492
+ if (result.reason === "timeout")
195493
+ return true;
195494
+ return isTransientHistorianPromptError(result.error);
195495
+ }
195496
+ function historianAbortResult(startedAt) {
195497
+ return {
195498
+ ok: false,
195499
+ reason: "abort",
195500
+ error: "pi subagent aborted by caller",
195501
+ durationMs: Date.now() - startedAt
195502
+ };
195503
+ }
195504
+ async function sleepWithAbort(ms, signal) {
195505
+ if (signal?.aborted)
195506
+ return true;
195507
+ if (ms <= 0)
195508
+ return signal?.aborted === true;
195509
+ return new Promise((resolve3) => {
195510
+ let settled = false;
195511
+ let timeout;
195512
+ const finish = (aborted2) => {
195513
+ if (settled)
195514
+ return;
195515
+ settled = true;
195516
+ if (timeout)
195517
+ clearTimeout(timeout);
195518
+ signal?.removeEventListener("abort", onAbort);
195519
+ resolve3(aborted2);
195520
+ };
195521
+ const onAbort = () => finish(true);
195522
+ signal?.addEventListener("abort", onAbort, { once: true });
195523
+ timeout = setTimeout(() => finish(false), ms);
195524
+ });
195525
+ }
195526
+ async function runHistorianSubagentWithTransientRetries(args) {
195527
+ const startedAt = Date.now();
195528
+ if (args.options.signal?.aborted)
195529
+ return historianAbortResult(startedAt);
195530
+ for (let retryIndex = 0;retryIndex <= MAX_HISTORIAN_RETRIES; retryIndex += 1) {
195531
+ const attemptStart = Date.now();
195532
+ let result;
195533
+ try {
195534
+ result = await args.runner.run({
195535
+ ...args.options,
195536
+ fallbackModels: undefined
195537
+ });
195538
+ } catch (error51) {
195539
+ const desc = describeError(error51);
195540
+ result = {
195541
+ ok: false,
195542
+ reason: "model_failed",
195543
+ error: desc.brief,
195544
+ durationMs: Date.now() - attemptStart
195545
+ };
195546
+ }
195547
+ if (result.ok)
195548
+ return result;
195549
+ if (result.reason === "abort" || args.options.signal?.aborted) {
195550
+ return result.reason === "abort" ? result : historianAbortResult(startedAt);
195551
+ }
195552
+ const shouldRetry = retryIndex < MAX_HISTORIAN_RETRIES && isTransientHistorianRunFailure(result);
195553
+ if (!shouldRetry)
195554
+ return result;
195555
+ const backoffMs2 = args.retryBackoffMs?.(retryIndex) ?? getHistorianRetryBackoffMs(retryIndex);
195556
+ sessionLog(args.sessionId, `historian[${args.passLabel}] transient failure; retry ${retryIndex + 1}/${MAX_HISTORIAN_RETRIES} on same model after ${backoffMs2}ms: ${result.error}`);
195557
+ const aborted2 = await sleepWithAbort(backoffMs2, args.options.signal);
195558
+ if (aborted2)
195559
+ return historianAbortResult(startedAt);
195560
+ }
195561
+ return historianAbortResult(startedAt);
195562
+ }
195563
+ function buildHistorianFallbackChain(primaryModel, fallbackModels, fallbackModelId) {
195564
+ const seen = new Set;
195565
+ if (primaryModel)
195566
+ seen.add(primaryModel);
195567
+ const chain = [];
195568
+ for (const candidate of fallbackModels ?? []) {
195569
+ if (!candidate || seen.has(candidate))
195570
+ continue;
195571
+ seen.add(candidate);
195572
+ chain.push({ modelId: candidate, kind: "configured" });
195573
+ }
195574
+ if (fallbackModelId && !seen.has(fallbackModelId)) {
195575
+ chain.push({ modelId: fallbackModelId, kind: "session" });
195576
+ }
195577
+ return chain;
195578
+ }
194797
195579
  function parseSourceMessageTime(value) {
194798
195580
  if (typeof value === "number" && Number.isFinite(value))
194799
195581
  return value;
@@ -194842,11 +195624,14 @@ async function runPiHistorian(deps) {
194842
195624
  runner,
194843
195625
  historianModel,
194844
195626
  fallbackModels,
195627
+ fallbackModelId,
194845
195628
  historianChunkTokens,
194846
195629
  boundarySnapshot: providedBoundarySnapshot,
194847
195630
  refreshBoundarySnapshot,
194848
195631
  currentContextLimit,
194849
195632
  historianTimeoutMs = DEFAULT_HISTORIAN_TIMEOUT_MS2,
195633
+ signal,
195634
+ retryBackoffMs,
194850
195635
  twoPass,
194851
195636
  thinkingLevel,
194852
195637
  memoryEnabled,
@@ -194860,7 +195645,9 @@ async function runPiHistorian(deps) {
194860
195645
  forceDrainQuota,
194861
195646
  forceKeepLastCompartment
194862
195647
  } = deps;
195648
+ let issueNotified = false;
194863
195649
  const notify = async (message) => {
195650
+ issueNotified = true;
194864
195651
  if (shouldSuppressHistorianAlert(sessionId)) {
194865
195652
  sessionLog(sessionId, "historian alert suppressed (cooldown)");
194866
195653
  return;
@@ -195051,61 +195838,77 @@ ${chunkText}`,
195051
195838
  retainDrainReservationForRetryThrottle = true;
195052
195839
  const historianSystemPrompt = withContentLanguageDirective(COMPARTMENT_AGENT_SYSTEM_PROMPT, deps.language, { preserveUserQuotes: true });
195053
195840
  const historianEditorSystemPrompt = withContentLanguageDirective(HISTORIAN_EDITOR_SYSTEM_PROMPT, deps.language, { preserveUserQuotes: true });
195054
- const firstResult = await runner.run({
195055
- agent: HISTORIAN_AGENT_NAME,
195056
- systemPrompt: historianSystemPrompt,
195057
- userMessage: prompt,
195058
- model: historianModel,
195059
- fallbackModels,
195060
- timeoutMs: historianTimeoutMs,
195061
- cwd: directory,
195062
- thinkingLevel,
195063
- onProgress: buildProgressLogger("first"),
195064
- accountingSessionId: sessionId,
195065
- accountingSubagent: "historian"
195066
- });
195067
- let validatedPass = await validateHistorianResult(firstResult, sessionId, chunk, priorCompartments, sequenceOffset);
195068
- let validatedDraftText = firstResult.ok ? firstResult.assistantText : null;
195069
- if (validatedPass.kind === "validation-failed") {
195070
- sessionLog(sessionId, `historian: first pass validation failed, retrying with repair prompt: ${validatedPass.error}`);
195071
- const repairPrompt = buildHistorianRepairPrompt(prompt, validatedPass.rawText, validatedPass.error, deps.language);
195072
- const repairResult = await runner.run({
195841
+ const firstResult = await runHistorianSubagentWithTransientRetries({
195842
+ runner,
195843
+ sessionId,
195844
+ passLabel: "first",
195845
+ retryBackoffMs,
195846
+ options: {
195073
195847
  agent: HISTORIAN_AGENT_NAME,
195074
195848
  systemPrompt: historianSystemPrompt,
195075
- userMessage: repairPrompt,
195849
+ userMessage: prompt,
195076
195850
  model: historianModel,
195077
- fallbackModels,
195078
195851
  timeoutMs: historianTimeoutMs,
195079
195852
  cwd: directory,
195853
+ signal,
195080
195854
  thinkingLevel,
195081
- onProgress: buildProgressLogger("repair"),
195855
+ onProgress: buildProgressLogger("first"),
195082
195856
  accountingSessionId: sessionId,
195083
195857
  accountingSubagent: "historian"
195084
- });
195085
- validatedPass = await validateHistorianResult(repairResult, sessionId, chunk, priorCompartments, sequenceOffset);
195086
- if (validatedPass.kind === "ok" && repairResult.ok) {
195087
- validatedDraftText = repairResult.assistantText;
195088
195858
  }
195089
- }
195090
- if (validatedPass.kind !== "ok" && (fallbackModels?.length ?? 0) > 0) {
195091
- const seen = new Set([historianModel].filter(Boolean));
195092
- for (const candidate of fallbackModels ?? []) {
195093
- if (!candidate || seen.has(candidate))
195094
- continue;
195095
- seen.add(candidate);
195096
- sessionLog(sessionId, `historian: escalating to configured fallback model ${candidate}`);
195097
- const fbResult = await runner.run({
195859
+ });
195860
+ let validatedPass = await validateHistorianResult(firstResult, sessionId, chunk, priorCompartments, sequenceOffset);
195861
+ let validatedDraftText = firstResult.ok ? firstResult.assistantText : null;
195862
+ if (validatedPass.kind === "validation-failed") {
195863
+ sessionLog(sessionId, `historian: first pass validation failed, retrying with repair prompt: ${validatedPass.error}`);
195864
+ const repairPrompt = buildHistorianRepairPrompt(prompt, validatedPass.rawText, validatedPass.error, deps.language);
195865
+ const repairResult = await runHistorianSubagentWithTransientRetries({
195866
+ runner,
195867
+ sessionId,
195868
+ passLabel: "repair",
195869
+ retryBackoffMs,
195870
+ options: {
195098
195871
  agent: HISTORIAN_AGENT_NAME,
195099
195872
  systemPrompt: historianSystemPrompt,
195100
- userMessage: prompt,
195101
- model: candidate,
195102
- fallbackModels: undefined,
195873
+ userMessage: repairPrompt,
195874
+ model: historianModel,
195103
195875
  timeoutMs: historianTimeoutMs,
195104
195876
  cwd: directory,
195877
+ signal,
195105
195878
  thinkingLevel,
195106
- onProgress: buildProgressLogger("fallback"),
195879
+ onProgress: buildProgressLogger("repair"),
195107
195880
  accountingSessionId: sessionId,
195108
195881
  accountingSubagent: "historian"
195882
+ }
195883
+ });
195884
+ validatedPass = await validateHistorianResult(repairResult, sessionId, chunk, priorCompartments, sequenceOffset);
195885
+ if (validatedPass.kind === "ok" && repairResult.ok) {
195886
+ validatedDraftText = repairResult.assistantText;
195887
+ }
195888
+ }
195889
+ const fallbackChain = buildHistorianFallbackChain(historianModel, fallbackModels, fallbackModelId);
195890
+ if (validatedPass.kind !== "ok" && !(validatedPass.kind === "spawn-failed" && validatedPass.reason === "abort") && fallbackChain.length > 0) {
195891
+ for (let i = 0;i < fallbackChain.length; i += 1) {
195892
+ const candidate = fallbackChain[i];
195893
+ sessionLog(sessionId, `historian: escalating to ${candidate.kind === "session" ? "session-model last resort" : "configured fallback model"} ${candidate.modelId}`);
195894
+ const fbResult = await runHistorianSubagentWithTransientRetries({
195895
+ runner,
195896
+ sessionId,
195897
+ passLabel: candidate.kind === "session" ? "fallback-session" : "fallback",
195898
+ retryBackoffMs,
195899
+ options: {
195900
+ agent: HISTORIAN_AGENT_NAME,
195901
+ systemPrompt: historianSystemPrompt,
195902
+ userMessage: prompt,
195903
+ model: candidate.modelId,
195904
+ timeoutMs: historianTimeoutMs,
195905
+ cwd: directory,
195906
+ signal,
195907
+ thinkingLevel,
195908
+ onProgress: buildProgressLogger("fallback"),
195909
+ accountingSessionId: sessionId,
195910
+ accountingSubagent: "historian"
195911
+ }
195109
195912
  });
195110
195913
  const fbPass = await validateHistorianResult(fbResult, sessionId, chunk, priorCompartments, sequenceOffset);
195111
195914
  if (fbPass.kind === "ok") {
@@ -195114,6 +195917,10 @@ ${chunkText}`,
195114
195917
  validatedDraftText = fbResult.assistantText;
195115
195918
  break;
195116
195919
  }
195920
+ if (fbPass.kind === "spawn-failed" && fbPass.reason === "abort") {
195921
+ validatedPass = fbPass;
195922
+ break;
195923
+ }
195117
195924
  }
195118
195925
  }
195119
195926
  if (validatedPass.kind !== "ok") {
@@ -195130,17 +195937,24 @@ ${chunkText}`,
195130
195937
  const draftAssistantText = validatedDraftText ?? "";
195131
195938
  if (draftAssistantText.trim().length > 0) {
195132
195939
  sessionLog(sessionId, "historian two-pass: running editor on draft");
195133
- const editorResult = await runner.run({
195134
- agent: HISTORIAN_AGENT_NAME,
195135
- systemPrompt: historianEditorSystemPrompt,
195136
- userMessage: buildHistorianEditorPrompt(draftAssistantText),
195137
- model: historianModel,
195138
- timeoutMs: historianTimeoutMs,
195139
- cwd: directory,
195140
- thinkingLevel,
195141
- onProgress: buildProgressLogger("editor"),
195142
- accountingSessionId: sessionId,
195143
- accountingSubagent: "historian_editor"
195940
+ const editorResult = await runHistorianSubagentWithTransientRetries({
195941
+ runner,
195942
+ sessionId,
195943
+ passLabel: "editor",
195944
+ retryBackoffMs,
195945
+ options: {
195946
+ agent: HISTORIAN_AGENT_NAME,
195947
+ systemPrompt: historianEditorSystemPrompt,
195948
+ userMessage: buildHistorianEditorPrompt(draftAssistantText),
195949
+ model: historianModel,
195950
+ timeoutMs: historianTimeoutMs,
195951
+ cwd: directory,
195952
+ signal,
195953
+ thinkingLevel,
195954
+ onProgress: buildProgressLogger("editor"),
195955
+ accountingSessionId: sessionId,
195956
+ accountingSubagent: "historian_editor"
195957
+ }
195144
195958
  });
195145
195959
  const editorPass = await validateHistorianResult(editorResult, sessionId, chunk, priorCompartments, sequenceOffset);
195146
195960
  if (editorPass.kind === "ok") {
@@ -195353,7 +196167,7 @@ ${chunkText}`,
195353
196167
  const desc = describeError(error51);
195354
196168
  telemetry.failureReason = `exception: ${desc.brief}`;
195355
196169
  sessionLog(sessionId, `historian failure: source=exception ${desc.brief}${desc.stackHead ? ` stackHead="${desc.stackHead}"` : ""}`);
195356
- {
196170
+ if (!issueNotified) {
195357
196171
  const failCount = incrementHistorianFailure(db, sessionId, desc.brief);
195358
196172
  await notify(buildHistorianFailureNotice(failCount, desc.brief));
195359
196173
  }
@@ -197302,6 +198116,8 @@ function registerPiContextHandler(pi, baseOptions) {
197302
198116
  }
197303
198117
  const sessionMeta = sessionMetaForUsage;
197304
198118
  const modelKey = liveModelBySession.get(sessionId);
198119
+ const providerId = typeof ctx.model?.provider === "string" ? ctx.model.provider : undefined;
198120
+ const canUseEmptySentinels = modelAcceptsEmptyContent(providerId);
197305
198121
  if (usageContextLimit === undefined) {
197306
198122
  const modelWindow = ctx.model?.contextWindow;
197307
198123
  if (isSaneLimit(modelWindow)) {
@@ -197312,6 +198128,7 @@ function registerPiContextHandler(pi, baseOptions) {
197312
198128
  usagePercentage = usageInputTokens / usageContextLimit * 100;
197313
198129
  }
197314
198130
  ({ percentage: usagePercentage, inputTokens: usageInputTokens } = applyForwardPressureFloor(usagePercentage, usageInputTokens, piUsage?.tokens, usageContextLimit));
198131
+ const realUsagePercentageBeforeEmergencyBump = usagePercentage;
197315
198132
  if (needsEmergencyBump) {
197316
198133
  sessionLog(sessionId, `transform: overflow recovery flag set — bumping percentage to 95% (detectedLimit=${usageContextLimit ?? "unknown"})`);
197317
198134
  usagePercentage = Math.max(usagePercentage, 95);
@@ -197382,7 +198199,7 @@ function registerPiContextHandler(pi, baseOptions) {
197382
198199
  sessionLog(sessionId, "EMERGENCY: historian wait completed (or timed out)");
197383
198200
  } catch {}
197384
198201
  }
197385
- if (emergencyRecoveryArmed && !inFlightHistorian.has(sessionId) && !hasEligiblePiCompartmentHistory(options.db, sessionId)) {
198202
+ if (emergencyRecoveryArmed && realUsagePercentageBeforeEmergencyBump < FORCE_MATERIALIZATION_PERCENTAGE && !inFlightHistorian.has(sessionId) && !hasEligiblePiCompartmentHistory(options.db, sessionId)) {
197386
198203
  try {
197387
198204
  clearEmergencyRecovery(options.db, sessionId);
197388
198205
  sessionLog(sessionId, "EMERGENCY: disarming recovery — no eligible pre-tail history to compact (would otherwise loop at 95%)");
@@ -197437,6 +198254,7 @@ function registerPiContextHandler(pi, baseOptions) {
197437
198254
  reasoningClearing: {
197438
198255
  clearReasoningAge: options.heuristics?.clearReasoningAge ?? DEFAULT_CLEAR_REASONING_AGE
197439
198256
  },
198257
+ canUseEmptySentinels,
197440
198258
  temporalAwareness: options.injection?.temporalAwareness === true,
197441
198259
  appendCompaction: resolvePiAppendCompaction(ctx),
197442
198260
  readBranchEntries: resolvePiReadBranchEntries(ctx),
@@ -197458,6 +198276,7 @@ function registerPiContextHandler(pi, baseOptions) {
197458
198276
  }
197459
198277
  if (options.historian) {
197460
198278
  maybeFireHistorian({
198279
+ pi,
197461
198280
  ctx,
197462
198281
  sessionId,
197463
198282
  db: options.db,
@@ -197698,6 +198517,7 @@ function sendPiIgnoredNotification(ctx, message) {
197698
198517
  }
197699
198518
  function spawnPiHistorianRun(args) {
197700
198519
  const {
198520
+ pi,
197701
198521
  ctx,
197702
198522
  sessionId,
197703
198523
  db,
@@ -197706,7 +198526,8 @@ function spawnPiHistorianRun(args) {
197706
198526
  unregister,
197707
198527
  boundarySnapshot,
197708
198528
  refreshBoundarySnapshot,
197709
- currentContextLimit
198529
+ currentContextLimit,
198530
+ fallbackModelId
197710
198531
  } = args;
197711
198532
  const holderId = crypto3.randomUUID();
197712
198533
  const runPromise = (async () => {
@@ -197732,6 +198553,7 @@ function spawnPiHistorianRun(args) {
197732
198553
  runner: historian.runner,
197733
198554
  historianModel: historian.model,
197734
198555
  fallbackModels: historian.fallbackModels,
198556
+ fallbackModelId,
197735
198557
  historianChunkTokens: historian.historianChunkTokens,
197736
198558
  boundarySnapshot,
197737
198559
  refreshBoundarySnapshot,
@@ -197744,6 +198566,17 @@ function spawnPiHistorianRun(args) {
197744
198566
  userMemoriesEnabled: historian.userMemoriesEnabled,
197745
198567
  language: historian.language,
197746
198568
  compartmentLeaseHolderId: holderId,
198569
+ notifyIssue: (text) => {
198570
+ if (!isContextHandlerSessionActive(sessionId)) {
198571
+ sessionLog(sessionId, "historian failure notice skipped after session context cleared");
198572
+ return;
198573
+ }
198574
+ sendCtxStatusMessage(pi, {
198575
+ title: "Magic Context",
198576
+ text,
198577
+ level: "warning"
198578
+ });
198579
+ },
197747
198580
  onPublished: () => {
197748
198581
  const sessionStillActive = isContextHandlerSessionActive(sessionId);
197749
198582
  try {
@@ -197893,6 +198726,7 @@ function maybeFireHistorian(args) {
197893
198726
 
197894
198727
  Historian previously failed ${failureState.failureCount} time(s), so Magic Context is retrying history comparting immediately after restart.`);
197895
198728
  spawnPiHistorianRun({
198729
+ pi: args.pi,
197896
198730
  ctx,
197897
198731
  sessionId,
197898
198732
  db,
@@ -197901,7 +198735,8 @@ Historian previously failed ${failureState.failureCount} time(s), so Magic Conte
197901
198735
  unregister,
197902
198736
  boundarySnapshot,
197903
198737
  refreshBoundarySnapshot: resolveRunnablePiBoundarySnapshot,
197904
- currentContextLimit: boundaryContextLimit
198738
+ currentContextLimit: boundaryContextLimit,
198739
+ fallbackModelId: modelKey
197905
198740
  });
197906
198741
  return;
197907
198742
  }
@@ -197923,6 +198758,7 @@ Historian previously failed ${failureState.failureCount} time(s), so Magic Conte
197923
198758
  triggered = true;
197924
198759
  sessionLog(sessionId, `historian trigger fired (reason=${trigger.reason ?? "unknown"}) usage=${usage.percentage.toFixed(1)}% — spawning subagent`);
197925
198760
  spawnPiHistorianRun({
198761
+ pi: args.pi,
197926
198762
  ctx,
197927
198763
  sessionId,
197928
198764
  db,
@@ -197934,7 +198770,8 @@ Historian previously failed ${failureState.failureCount} time(s), so Magic Conte
197934
198770
  triggerBoundarySnapshot: trigger.boundarySnapshot
197935
198771
  }),
197936
198772
  refreshBoundarySnapshot: resolveRunnablePiBoundarySnapshot,
197937
- currentContextLimit: boundaryContextLimit
198773
+ currentContextLimit: boundaryContextLimit,
198774
+ fallbackModelId: modelKey
197938
198775
  });
197939
198776
  } catch (err) {
197940
198777
  const message = err instanceof Error ? err.message : String(err);
@@ -198014,7 +198851,10 @@ async function runPipeline(args) {
198014
198851
  historyBudgetTokens: args.injection.historyBudgetTokens,
198015
198852
  hardSignals: piHardSignals
198016
198853
  }, args.db, getCompartments(args.db, args.sessionId)).value : false;
198017
- const shouldRunHeuristics = args.heuristics !== undefined && (args.forceMaterialization === true || hasPendingMaterialization(args.sessionId) || deferredMaterializeEligible || m0HardFoldThisPass || args.schedulerDecision === "execute" && !alreadyRanHeuristicsThisTurn);
198854
+ const historianRunning = inFlightHistorian.has(args.sessionId);
198855
+ const bypassHistorianGate = args.forceMaterialization === true || m0HardFoldThisPass;
198856
+ const hasPendingMaterializeSignal = hasPendingMaterialization(args.sessionId);
198857
+ const shouldRunHeuristics = args.heuristics !== undefined && (!historianRunning || bypassHistorianGate) && (args.forceMaterialization === true || hasPendingMaterializeSignal || deferredMaterializeEligible || m0HardFoldThisPass || args.schedulerDecision === "execute" && !alreadyRanHeuristicsThisTurn);
198018
198858
  const entryFingerprintByMessageId = buildEntryFingerprintMap(args.messages, stableIdResolver);
198019
198859
  adoptPiFallbackTags(args.db, args.sessionId, args.tagger, entryFingerprintByMessageId, {
198020
198860
  messages: args.messages,
@@ -198048,14 +198888,13 @@ async function runPipeline(args) {
198048
198888
  } catch (err) {
198049
198889
  sessionLog(args.sessionId, `commit-detect failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
198050
198890
  }
198051
- const hasPendingMaterializeSignal = hasPendingMaterialization(args.sessionId);
198052
198891
  const deferredMaterializationWasPending = deferredMaterializationSessions.has(args.sessionId);
198053
198892
  const deferredHistoryRefreshWasPending = deferredHistoryWasPendingAtPassStart;
198054
198893
  const pendingOps = getPendingOps(args.db, args.sessionId);
198055
198894
  const baseShouldApplyPendingOps = args.schedulerDecision === "execute" || args.forceMaterialization || hasPendingMaterializeSignal || m0HardFoldThisPass;
198056
198895
  const deferredMaterialize = canConsumeDeferredLate && deferredMaterializationWasPending;
198057
198896
  const deferredHistoryRefresh = canConsumeDeferredLate && deferredHistoryRefreshWasPending;
198058
- const shouldApplyPendingOps = baseShouldApplyPendingOps || deferredMaterialize;
198897
+ const shouldApplyPendingOps = (baseShouldApplyPendingOps || deferredMaterialize) && (!historianRunning || bypassHistorianGate);
198059
198898
  if (shouldApplyPendingOps) {
198060
198899
  const applyReason = hasPendingMaterializeSignal ? "explicit_flush" : deferredMaterialize ? "deferred_publication" : args.forceMaterialization ? "force_materialization" : m0HardFoldThisPass && args.schedulerDecision !== "execute" ? `m0_hard_fold (drain folded into known m[0] bust, scheduler=${args.schedulerDecision})` : `scheduler_execute (scheduler=${args.schedulerDecision})`;
198061
198900
  sessionLog(args.sessionId, `pending ops WILL APPLY — reason=${applyReason}, pendingOps=${pendingOps.length}, context=${args.contextUsage.percentage.toFixed(1)}%`);
@@ -198144,6 +198983,7 @@ async function runPipeline(args) {
198144
198983
  const tHeuristic = performance.now();
198145
198984
  heuristicsResult = applyPiHeuristicCleanup(args.sessionId, args.db, targets, args.messages, {
198146
198985
  protectedTags: args.protectedTags,
198986
+ staleReduceStripEnabled: args.canUseEmptySentinels,
198147
198987
  emergency: args.forceMaterialization === true && args.emergencyCeilingTokens !== undefined && args.emergencyCeilingTokens > 0 ? {
198148
198988
  currentTotalInputTokens: args.contextUsage.inputTokens,
198149
198989
  ceilingTokens: args.emergencyCeilingTokens
@@ -198729,7 +199569,7 @@ init_data_path();
198729
199569
  function historianResponseDumpDir(directory) {
198730
199570
  return getProjectMagicContextHistorianDir(directory);
198731
199571
  }
198732
- var MAX_HISTORIAN_RETRIES = 2;
199572
+ var MAX_HISTORIAN_RETRIES2 = 2;
198733
199573
  async function runValidatedHistorianPass(args) {
198734
199574
  const firstRun = await runHistorianPrompt({
198735
199575
  ...args,
@@ -198870,7 +199710,7 @@ async function runHistorianPrompt(args) {
198870
199710
  });
198871
199711
  return { ok: false, error: "Historian could not create its child session." };
198872
199712
  }
198873
- for (let retryIndex = 0;retryIndex <= MAX_HISTORIAN_RETRIES; retryIndex += 1) {
199713
+ for (let retryIndex = 0;retryIndex <= MAX_HISTORIAN_RETRIES2; retryIndex += 1) {
198874
199714
  try {
198875
199715
  await promptSyncWithModelSuggestionRetry(client, {
198876
199716
  path: { id: agentSessionId },
@@ -198885,17 +199725,17 @@ async function runHistorianPrompt(args) {
198885
199725
  fallbackModels: modelOverride ? undefined : fallbackModels,
198886
199726
  callContext: agentId === HISTORIAN_EDITOR_AGENT ? "historian:editor" : "historian"
198887
199727
  });
198888
- sessionLog(parentSessionId, `historian: prompt completed (attempt ${retryIndex + 1}/${MAX_HISTORIAN_RETRIES + 1})`);
199728
+ sessionLog(parentSessionId, `historian: prompt completed (attempt ${retryIndex + 1}/${MAX_HISTORIAN_RETRIES2 + 1})`);
198889
199729
  break;
198890
199730
  } catch (error51) {
198891
199731
  const errorMsg = getErrorMessage(error51);
198892
199732
  sessionLog(parentSessionId, `historian: prompt attempt ${retryIndex + 1} failed: ${errorMsg}`);
198893
- const shouldRetry = retryIndex < MAX_HISTORIAN_RETRIES && isTransientHistorianPromptError(errorMsg);
199733
+ const shouldRetry = retryIndex < MAX_HISTORIAN_RETRIES2 && isTransientHistorianPromptError2(errorMsg);
198894
199734
  if (!shouldRetry) {
198895
199735
  throw error51;
198896
199736
  }
198897
- const backoffMs2 = getHistorianRetryBackoffMs(retryIndex);
198898
- sessionLog(parentSessionId, `historian retry ${retryIndex + 1}/${MAX_HISTORIAN_RETRIES} after ${backoffMs2}ms: ${errorMsg}`);
199737
+ const backoffMs2 = getHistorianRetryBackoffMs2(retryIndex);
199738
+ sessionLog(parentSessionId, `historian retry ${retryIndex + 1}/${MAX_HISTORIAN_RETRIES2} after ${backoffMs2}ms: ${errorMsg}`);
198899
199739
  await sleep(backoffMs2);
198900
199740
  }
198901
199741
  }
@@ -198988,13 +199828,13 @@ function parseModelOverride(modelId) {
198988
199828
  }
198989
199829
  return { providerID, modelID };
198990
199830
  }
198991
- function getHistorianRetryBackoffMs(retryIndex) {
199831
+ function getHistorianRetryBackoffMs2(retryIndex) {
198992
199832
  if (retryIndex === 0) {
198993
199833
  return 2000 + Math.floor(Math.random() * 1001);
198994
199834
  }
198995
199835
  return 6000 + Math.floor(Math.random() * 2001);
198996
199836
  }
198997
- function isTransientHistorianPromptError(message) {
199837
+ function isTransientHistorianPromptError2(message) {
198998
199838
  const normalized = message.toLowerCase();
198999
199839
  if (normalized.includes("invalid request") || normalized.includes("bad request") || normalized.includes("unauthorized") || normalized.includes("forbidden") || normalized.includes("authentication") || normalized.includes("auth") || normalized.includes(" 400") || normalized.startsWith("400")) {
199000
199840
  return false;
@@ -199949,6 +200789,15 @@ async function executeContextRecompWithResult(deps, options = {}) {
199949
200789
  published: false
199950
200790
  };
199951
200791
  }
200792
+ options.onLeaseAcquired?.();
200793
+ if (isWrapupInProgress(deps.db, sessionId)) {
200794
+ sessionLog(sessionId, "recomp skipped: /ctx-wrapup became active");
200795
+ releaseCompartmentLease(deps.db, sessionId, holderId);
200796
+ return {
200797
+ message: "## Magic Recomp — Skipped\n\n/ctx-wrapup is already compacting this session. Wait for it to finish, then try `/ctx-recomp` again.",
200798
+ published: false
200799
+ };
200800
+ }
199952
200801
  const renewal = startLeaseRenewal(deps, holderId);
199953
200802
  const runnerDeps = withPublishedCallback({ ...deps, compartmentLeaseHolderId: holderId });
199954
200803
  const promise2 = options.range ? executePartialRecompInternal(runnerDeps, options.range) : executeContextRecompInternal(runnerDeps);
@@ -200238,6 +201087,13 @@ function spawnPiRecompRun(args) {
200238
201087
  // src/commands/ctx-recomp.ts
200239
201088
  var confirmationBySession = new Map;
200240
201089
  var RECOMP_CONFIRMATION_WINDOW_MS = 60000;
201090
+ var RECOMP_USAGE = [
201091
+ "Usage:",
201092
+ "- `/ctx-recomp` — full rebuild from message 1 to the protected tail",
201093
+ "- `/ctx-recomp <start>-<end>` — partial rebuild of a message range (e.g. `/ctx-recomp 1-11322`)",
201094
+ "- `/ctx-recomp --upgrade` — upgrade legacy v1 compartments to v2 layout (Wave 3 runner)"
201095
+ ].join(`
201096
+ `);
200241
201097
  function registerCtxRecompCommand(pi, deps) {
200242
201098
  pi.registerCommand("ctx-recomp", {
200243
201099
  description: "Rebuild Magic Context compartments from raw Pi session history",
@@ -200253,21 +201109,30 @@ No active Pi session is available.`,
200253
201109
  });
200254
201110
  return;
200255
201111
  }
200256
- if (!deps.historianModel) {
201112
+ const currentDeps = deps.resolveRuntimeDeps?.(ctx) ?? deps;
201113
+ const parsed = parseRecompArgs(args);
201114
+ if (parsed.kind === "error") {
200257
201115
  sendCtxStatusMessage(pi, {
200258
201116
  title: "/ctx-recomp",
200259
- text: "## Magic Recomp\n\n/ctx-recomp is unavailable because `historian.model` is not configured.",
201117
+ text: `## Magic Recomp — Invalid Arguments
201118
+
201119
+ ${parsed.message}`,
200260
201120
  level: "error"
200261
201121
  });
200262
201122
  return;
200263
201123
  }
200264
- const parsed = parseRecompArgs(args);
200265
- if (parsed.kind === "error") {
201124
+ if (parsed.kind === "upgrade") {
200266
201125
  sendCtxStatusMessage(pi, {
200267
201126
  title: "/ctx-recomp",
200268
- text: `## Magic Recomp — Invalid Arguments
200269
-
200270
- ${parsed.message}`,
201127
+ text: executeRecompUpgradeStub(currentDeps.db, sessionId),
201128
+ level: "info"
201129
+ });
201130
+ return;
201131
+ }
201132
+ if (!currentDeps.historianModel) {
201133
+ sendCtxStatusMessage(pi, {
201134
+ title: "/ctx-recomp",
201135
+ text: "## Magic Recomp\n\n/ctx-recomp is unavailable because `historian.model` is not configured.",
200271
201136
  level: "error"
200272
201137
  });
200273
201138
  return;
@@ -200277,7 +201142,7 @@ ${parsed.message}`,
200277
201142
  const confirmation = confirmationBySession.get(sessionId);
200278
201143
  const confirmed = confirmation !== undefined && now - confirmation.timestamp < RECOMP_CONFIRMATION_WINDOW_MS && confirmation.argsKey === argsKey;
200279
201144
  if (!confirmed) {
200280
- const warning = buildConfirmationWarning(deps.db, sessionId, parsed);
201145
+ const warning = buildConfirmationWarning(currentDeps.db, sessionId, parsed);
200281
201146
  if (!warning.confirmable)
200282
201147
  confirmationBySession.delete(sessionId);
200283
201148
  else
@@ -200289,7 +201154,7 @@ ${parsed.message}`,
200289
201154
  });
200290
201155
  return;
200291
201156
  }
200292
- if (isWrapupInProgress(deps.db, sessionId)) {
201157
+ if (isWrapupInProgress(currentDeps.db, sessionId)) {
200293
201158
  sendCtxStatusMessage(pi, {
200294
201159
  title: "/ctx-recomp",
200295
201160
  text: "## Magic Recomp\n\n/ctx-wrapup is already compacting this session. Wait for it to finish, then try `/ctx-recomp` again.",
@@ -200323,16 +201188,19 @@ Historian recomp started. Rebuilding compartments and facts from raw Pi session
200323
201188
  spawnPiRecompRun({
200324
201189
  sessionId,
200325
201190
  provider: provider2,
200326
- onStatusChange: () => updateStatusLine(ctx, { db: deps.db, projectIdentity: ctx.cwd }),
201191
+ onStatusChange: () => updateStatusLine(ctx, {
201192
+ db: currentDeps.db,
201193
+ projectIdentity: ctx.cwd
201194
+ }),
200327
201195
  work: async () => {
200328
201196
  const result = await executeContextRecompWithResult({
200329
201197
  client: createPiHistorianClient({
200330
- runner: deps.runner,
200331
- model: deps.historianModel,
200332
- systemPrompt: withContentLanguageDirective(COMPARTMENT_STRUCTURAL_SYSTEM_PROMPT, deps.language, { preserveUserQuotes: true }),
200333
- fallbackModels: deps.historianFallbacks,
200334
- timeoutMs: deps.historianTimeoutMs,
200335
- thinkingLevel: deps.historianThinkingLevel,
201198
+ runner: currentDeps.runner,
201199
+ model: currentDeps.historianModel,
201200
+ systemPrompt: withContentLanguageDirective(COMPARTMENT_STRUCTURAL_SYSTEM_PROMPT, currentDeps.language, { preserveUserQuotes: true }),
201201
+ fallbackModels: currentDeps.historianFallbacks,
201202
+ timeoutMs: currentDeps.historianTimeoutMs,
201203
+ thinkingLevel: currentDeps.historianThinkingLevel,
200336
201204
  directory: ctx.cwd,
200337
201205
  accountingSessionId: sessionId,
200338
201206
  notify: (text) => {
@@ -200343,26 +201211,26 @@ Historian recomp started. Rebuilding compartments and facts from raw Pi session
200343
201211
  });
200344
201212
  }
200345
201213
  }),
200346
- db: deps.db,
201214
+ db: currentDeps.db,
200347
201215
  sessionId,
200348
- historianChunkTokens: deps.historianChunkTokens,
201216
+ historianChunkTokens: currentDeps.historianChunkTokens,
200349
201217
  directory: ctx.cwd,
200350
- historianTimeoutMs: deps.historianTimeoutMs,
200351
- memoryEnabled: deps.memoryEnabled,
200352
- autoPromote: deps.autoPromote,
201218
+ historianTimeoutMs: currentDeps.historianTimeoutMs,
201219
+ memoryEnabled: currentDeps.memoryEnabled,
201220
+ autoPromote: currentDeps.autoPromote,
200353
201221
  ensureProjectRegistered: ensureProjectRegisteredFromPiDirectory,
200354
- fallbackModels: deps.historianFallbacks,
200355
- language: deps.language,
201222
+ fallbackModels: currentDeps.historianFallbacks,
201223
+ language: currentDeps.language,
200356
201224
  fallbackModelId: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined
200357
201225
  }, parsed.kind === "partial" ? { range: parsed.range } : {});
200358
201226
  if (result.published) {
200359
201227
  try {
200360
- clearEmergencyRecovery(deps.db, sessionId);
201228
+ clearEmergencyRecovery(currentDeps.db, sessionId);
200361
201229
  } catch (recoveryError) {
200362
201230
  sessionLog(sessionId, `/ctx-recomp: clearEmergencyRecovery failed (continuing): ${describeError(recoveryError).brief}`);
200363
201231
  }
200364
201232
  try {
200365
- stagePiRecompMarker({ db: deps.db, sessionId, ctx });
201233
+ stagePiRecompMarker({ db: currentDeps.db, sessionId, ctx });
200366
201234
  } catch (markerError) {
200367
201235
  sessionLog(sessionId, `/ctx-recomp: marker staging failed (recomp already published; continuing): ${describeError(markerError).brief}`);
200368
201236
  }
@@ -200383,11 +201251,15 @@ function parseRecompArgs(raw) {
200383
201251
  const trimmed = raw.trim();
200384
201252
  if (trimmed.length === 0)
200385
201253
  return { kind: "full" };
201254
+ if (trimmed === "--upgrade")
201255
+ return { kind: "upgrade" };
200386
201256
  const match = trimmed.match(/^(\d+)\s*-\s*(\d+)$/);
200387
201257
  if (!match) {
200388
201258
  return {
200389
201259
  kind: "error",
200390
- message: "Usage:\n- `/ctx-recomp` — full rebuild from message 1 to the protected tail\n- `/ctx-recomp <start>-<end>` — partial rebuild of a message range"
201260
+ message: `Invalid /ctx-recomp arguments: \`${trimmed}\`.
201261
+
201262
+ ${RECOMP_USAGE}`
200391
201263
  };
200392
201264
  }
200393
201265
  const start = Number.parseInt(match[1], 10);
@@ -200401,6 +201273,21 @@ function parseRecompArgs(raw) {
200401
201273
  };
200402
201274
  return { kind: "partial", range: { start, end } };
200403
201275
  }
201276
+ function executeRecompUpgradeStub(db, sessionId) {
201277
+ const legacyCount = getCompartments(db, sessionId).filter((compartment) => compartment.legacy === 1).length;
201278
+ if (legacyCount === 0) {
201279
+ return `## Magic Recomp Upgrade
201280
+
201281
+ Nothing to upgrade: this session has no legacy compartments.`;
201282
+ }
201283
+ return [
201284
+ "## Magic Recomp Upgrade",
201285
+ "",
201286
+ `Found ${legacyCount} legacy compartment${legacyCount === 1 ? "" : "s"} for this session.`,
201287
+ "The `--upgrade` flag is deprecated. Run `/ctx-session-upgrade` to upgrade this session."
201288
+ ].join(`
201289
+ `);
201290
+ }
200404
201291
  function buildConfirmationWarning(db, sessionId, parsed) {
200405
201292
  const compartments = getCompartments(db, sessionId);
200406
201293
  if (parsed.kind === "partial") {
@@ -200680,7 +201567,8 @@ No active Pi session is available.`,
200680
201567
  });
200681
201568
  return;
200682
201569
  }
200683
- if (!deps.historianModel) {
201570
+ const currentDeps = deps.resolveRuntimeDeps?.(ctx) ?? deps;
201571
+ if (!currentDeps.historianModel) {
200684
201572
  sendCtxStatusMessage(pi, {
200685
201573
  title: "/ctx-session-upgrade",
200686
201574
  text: "## Session Upgrade\n\nUnavailable because `historian.model` is not configured.",
@@ -200688,7 +201576,7 @@ No active Pi session is available.`,
200688
201576
  });
200689
201577
  return;
200690
201578
  }
200691
- if (isWrapupInProgress(deps.db, sessionId)) {
201579
+ if (isWrapupInProgress(currentDeps.db, sessionId)) {
200692
201580
  sendCtxStatusMessage(pi, {
200693
201581
  title: "/ctx-session-upgrade",
200694
201582
  text: `## Session Upgrade
@@ -200708,27 +201596,27 @@ An upgrade or recomp is already running for this session in the background. Wait
200708
201596
  });
200709
201597
  return;
200710
201598
  }
200711
- const compartments = getCompartments(deps.db, sessionId);
201599
+ const compartments = getCompartments(currentDeps.db, sessionId);
200712
201600
  const upgradableCount = compartments.filter((c) => c.legacy === 1 || !c.p1 || c.p1.trim() === "").length;
200713
201601
  const sessionMainModel = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
200714
- const migrationEnabled = deps.memoryEnabled;
201602
+ const migrationEnabled = currentDeps.memoryEnabled;
200715
201603
  const runMigration = async () => {
200716
201604
  if (!migrationEnabled) {
200717
201605
  return "Memory migration skipped (memory disabled).";
200718
201606
  }
200719
201607
  try {
200720
201608
  const outcome = await runPiMemoryMigration({
200721
- db: deps.db,
200722
- runner: deps.runner,
201609
+ db: currentDeps.db,
201610
+ runner: currentDeps.runner,
200723
201611
  primaryModel: sessionMainModel,
200724
- model: deps.historianModel,
200725
- fallbackModels: deps.historianFallbacks,
200726
- timeoutMs: deps.historianTimeoutMs,
200727
- thinkingLevel: deps.historianThinkingLevel,
201612
+ model: currentDeps.historianModel,
201613
+ fallbackModels: currentDeps.historianFallbacks,
201614
+ timeoutMs: currentDeps.historianTimeoutMs,
201615
+ thinkingLevel: currentDeps.historianThinkingLevel,
200728
201616
  directory: ctx.cwd,
200729
201617
  sessionId,
200730
- userMemoriesEnabled: deps.userMemoriesEnabled,
200731
- language: deps.language
201618
+ userMemoriesEnabled: currentDeps.userMemoriesEnabled,
201619
+ language: currentDeps.language
200732
201620
  });
200733
201621
  return outcome.summary;
200734
201622
  } catch (error51) {
@@ -200737,7 +201625,7 @@ An upgrade or recomp is already running for this session in the background. Wait
200737
201625
  };
200738
201626
  if (upgradableCount === 0) {
200739
201627
  const projectPath = resolveProjectIdentity(ctx.cwd);
200740
- const migrationPending = migrationEnabled && !isMemoryMigrationDone(deps.db, projectPath);
201628
+ const migrationPending = migrationEnabled && !isMemoryMigrationDone(currentDeps.db, projectPath);
200741
201629
  if (!migrationPending) {
200742
201630
  sendCtxStatusMessage(pi, {
200743
201631
  title: "/ctx-session-upgrade",
@@ -200763,7 +201651,10 @@ Compartments are already current. Re-organizing project memories. This may take
200763
201651
  provider: {
200764
201652
  readMessages: () => readPiSessionMessages(ctx)
200765
201653
  },
200766
- onStatusChange: () => updateStatusLine(ctx, { db: deps.db, projectIdentity: ctx.cwd }),
201654
+ onStatusChange: () => updateStatusLine(ctx, {
201655
+ db: currentDeps.db,
201656
+ projectIdentity: ctx.cwd
201657
+ }),
200767
201658
  work: async () => {
200768
201659
  const summary = await runMigration();
200769
201660
  sendCtxStatusMessage(pi, {
@@ -200789,35 +201680,38 @@ Rebuilding compartments into the v2 format and re-organizing project memories. T
200789
201680
  spawnPiRecompRun({
200790
201681
  sessionId,
200791
201682
  provider: provider2,
200792
- onStatusChange: () => updateStatusLine(ctx, { db: deps.db, projectIdentity: ctx.cwd }),
201683
+ onStatusChange: () => updateStatusLine(ctx, {
201684
+ db: currentDeps.db,
201685
+ projectIdentity: ctx.cwd
201686
+ }),
200793
201687
  work: async () => {
200794
201688
  const recompResult = await executeContextRecompWithResult({
200795
201689
  client: createPiHistorianClient({
200796
- runner: deps.runner,
200797
- model: deps.historianModel,
200798
- fallbackModels: deps.historianFallbacks,
200799
- timeoutMs: deps.historianTimeoutMs,
200800
- thinkingLevel: deps.historianThinkingLevel,
201690
+ runner: currentDeps.runner,
201691
+ model: currentDeps.historianModel,
201692
+ fallbackModels: currentDeps.historianFallbacks,
201693
+ timeoutMs: currentDeps.historianTimeoutMs,
201694
+ thinkingLevel: currentDeps.historianThinkingLevel,
200801
201695
  directory: ctx.cwd,
200802
201696
  accountingSessionId: sessionId,
200803
- systemPrompt: withContentLanguageDirective(COMPARTMENT_STRUCTURAL_SYSTEM_PROMPT, deps.language, { preserveUserQuotes: true }),
201697
+ systemPrompt: withContentLanguageDirective(COMPARTMENT_STRUCTURAL_SYSTEM_PROMPT, currentDeps.language, { preserveUserQuotes: true }),
200804
201698
  notify: (text) => sendCtxStatusMessage(pi, {
200805
201699
  title: "/ctx-session-upgrade",
200806
201700
  text,
200807
201701
  level: "info"
200808
201702
  })
200809
201703
  }),
200810
- db: deps.db,
201704
+ db: currentDeps.db,
200811
201705
  sessionId,
200812
- historianChunkTokens: deps.historianChunkTokens,
201706
+ historianChunkTokens: currentDeps.historianChunkTokens,
200813
201707
  directory: ctx.cwd,
200814
- historianTimeoutMs: deps.historianTimeoutMs,
200815
- memoryEnabled: deps.memoryEnabled,
200816
- autoPromote: deps.autoPromote,
201708
+ historianTimeoutMs: currentDeps.historianTimeoutMs,
201709
+ memoryEnabled: currentDeps.memoryEnabled,
201710
+ autoPromote: currentDeps.autoPromote,
200817
201711
  ensureProjectRegistered: ensureProjectRegisteredFromPiDirectory,
200818
- fallbackModels: deps.historianFallbacks,
201712
+ fallbackModels: currentDeps.historianFallbacks,
200819
201713
  fallbackModelId: sessionMainModel,
200820
- language: deps.language
201714
+ language: currentDeps.language
200821
201715
  }, {});
200822
201716
  if (!recompResult.published || !isRecompComplete(recompResult.message)) {
200823
201717
  const reason = contextualizeUpgradeReason(isRecompFailure(recompResult.message) ? extractRecompReason(recompResult.message) : `Compartments were not fully rebuilt: ${extractRecompReason(recompResult.message)}`);
@@ -200831,7 +201725,7 @@ ${reason}`,
200831
201725
  return;
200832
201726
  }
200833
201727
  try {
200834
- stagePiRecompMarker({ db: deps.db, sessionId, ctx });
201728
+ stagePiRecompMarker({ db: currentDeps.db, sessionId, ctx });
200835
201729
  } catch (markerError) {
200836
201730
  sessionLog(sessionId, `pi /ctx-session-upgrade marker staging failed (non-fatal, recomp already published): ${describeError(markerError).brief}`);
200837
201731
  }
@@ -201029,7 +201923,7 @@ function formatThresholdPercent(value) {
201029
201923
  // package.json
201030
201924
  var package_default = {
201031
201925
  name: "@cortexkit/pi-magic-context",
201032
- version: "0.31.2",
201926
+ version: "0.31.4",
201033
201927
  type: "module",
201034
201928
  description: "Pi coding agent extension for Magic Context — cross-session memory and context management",
201035
201929
  main: "dist/index.js",
@@ -201180,6 +202074,7 @@ function renderInner(s, theme, innerWidth) {
201180
202074
  lines.push(`${theme.fg("accent", theme.bold("⚡ Magic Context Status"))} ${theme.fg("muted", `v${package_default.version}`)}`);
201181
202075
  lines.push("");
201182
202076
  lines.push(`Context ${theme.fg(pctColor, theme.bold(`${s.usagePercentage.toFixed(1)}%`))} · ${fmt2(s.inputTokens)} / ${s.contextLimit > 0 ? fmt2(s.contextLimit) : "?"} tokens`);
202077
+ lines.push(`Work tokens ${fmt2(s.newWorkTokens)} new · ${fmt2(s.totalInputTokens)} total input`);
201183
202078
  lines.push(renderBar(s, innerWidth));
201184
202079
  for (const seg of breakdownSegments(s)) {
201185
202080
  const pct = (seg.tokens / (s.inputTokens || 1) * 100).toFixed(1);
@@ -201275,6 +202170,7 @@ ${safeStringify(tool.parameters)}`);
201275
202170
  }
201276
202171
  } catch {}
201277
202172
  const conversationTokens = Math.max(0, inputTokens - systemPromptTokens - compartmentTokens - factTokens - memoryTokens - docsTokens - profileTokens - toolCallTokens - toolDefinitionTokens);
202173
+ const workMetrics = getSessionWorkMetrics(deps.db, sessionId);
201278
202174
  const modelKey = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
201279
202175
  const threshold = resolveExecuteThresholdDetail(deps.executeThresholdPercentage ?? 65, modelKey, 65, {
201280
202176
  tokensConfig: deps.executeThresholdTokens,
@@ -201338,6 +202234,8 @@ ${safeStringify(tool.parameters)}`);
201338
202234
  conversationTokens,
201339
202235
  toolCallTokens,
201340
202236
  toolDefinitionTokens,
202237
+ newWorkTokens: workMetrics.newWorkTokens,
202238
+ totalInputTokens: workMetrics.totalInputTokens,
201341
202239
  upgradeNeededCount: safeRead(() => countCompartmentsNeedingUpgrade(deps.db, sessionId), 0),
201342
202240
  recompInFlight: isPiRecompInFlight(sessionId)
201343
202241
  };
@@ -201492,8 +202390,9 @@ function registerCtxStatusCommand(pi, deps) {
201492
202390
  pi.registerCommand("ctx-status", {
201493
202391
  description: "Show Magic Context status for the current Pi session",
201494
202392
  handler: async (_args, ctx) => {
201495
- const projectIdentity = deps.resolveProject?.(ctx).projectIdentity ?? deps.projectIdentity;
201496
- const currentDeps = { ...deps, projectIdentity };
202393
+ const runtimeDeps = deps.resolveStatusDeps?.(ctx) ?? deps;
202394
+ const projectIdentity = runtimeDeps.resolveProject?.(ctx).projectIdentity ?? runtimeDeps.projectIdentity;
202395
+ const currentDeps = { ...runtimeDeps, projectIdentity };
201497
202396
  const sessionId = resolveSessionId(ctx);
201498
202397
  if (!sessionId) {
201499
202398
  sendCtxStatusMessage(pi, {
@@ -201574,6 +202473,11 @@ function readHistorianState(db, sessionId, meta3) {
201574
202473
  import * as crypto4 from "node:crypto";
201575
202474
  var DEFAULT_MESSAGES_TO_KEEP = 20;
201576
202475
  var LEASE_WAIT_MS = 1000;
202476
+ var MAX_WRAPUP_LEASE_WAIT_MS = 10 * 60 * 1000;
202477
+ function resolveWrapupLeaseWaitTimeout(deps) {
202478
+ const configured = deps.wrapupLeaseWaitTimeoutMs ?? MAX_WRAPUP_LEASE_WAIT_MS;
202479
+ return Number.isFinite(configured) && configured >= 0 ? configured : MAX_WRAPUP_LEASE_WAIT_MS;
202480
+ }
201577
202481
  function parseWrapupArgs(raw) {
201578
202482
  const trimmed = raw.trim();
201579
202483
  if (trimmed === "")
@@ -201608,7 +202512,8 @@ No active Pi session is available.`,
201608
202512
  });
201609
202513
  return;
201610
202514
  }
201611
- const sessionMeta = getOrCreateSessionMeta(deps.db, sessionId);
202515
+ const currentDeps = deps.resolveRuntimeDeps?.(ctx) ?? deps;
202516
+ const sessionMeta = getOrCreateSessionMeta(currentDeps.db, sessionId);
201612
202517
  if (sessionMeta.isSubagent) {
201613
202518
  sendCtxStatusMessage(pi, {
201614
202519
  title: "/ctx-wrapup",
@@ -201619,7 +202524,7 @@ No active Pi session is available.`,
201619
202524
  });
201620
202525
  return;
201621
202526
  }
201622
- if (!deps.historianModel) {
202527
+ if (!currentDeps.historianModel) {
201623
202528
  sendCtxStatusMessage(pi, {
201624
202529
  title: "/ctx-wrapup",
201625
202530
  text: "## Magic Wrapup\n\n/ctx-wrapup is unavailable because `historian.model` is not configured.",
@@ -201638,7 +202543,7 @@ ${parsed.message}`,
201638
202543
  });
201639
202544
  return;
201640
202545
  }
201641
- const result = await runPiWrapup(pi, deps, ctx, sessionId, parsed.messagesToKeep);
202546
+ const result = await runPiWrapup(pi, currentDeps, ctx, sessionId, parsed.messagesToKeep);
201642
202547
  sendCtxStatusMessage(pi, {
201643
202548
  title: "/ctx-wrapup",
201644
202549
  text: result,
@@ -201790,11 +202695,12 @@ Eligible history is about ${initialPlan.snapshot.trueRawEligibleTokens.toLocaleS
201790
202695
  Chunk ${chunkIndex}: wrapping messages ${plan.snapshot.offset}-${plan.snapshot.eligibleEndOrdinal - 1} (~${plan.snapshot.trueRawEligibleTokens.toLocaleString()} eligible tokens remain).`,
201791
202696
  level: "info"
201792
202697
  });
201793
- const leaseHolder = await acquireCompartmentLeaseEventually(deps.db, sessionId, renewWrapupMarker);
201794
- if (!leaseHolder) {
201795
- failure = ownershipLost ? `${ownershipLostReason}; wrapped up through message ${lastEnd}. Run /ctx-wrapup again to continue.` : "Another Magic Context rebuild started while wrapup was waiting. Run /ctx-wrapup again to continue.";
202698
+ const leaseResult = await acquireCompartmentLeaseEventually(deps.db, sessionId, renewWrapupMarker, resolveWrapupLeaseWaitTimeout(deps));
202699
+ if (!leaseResult.ok) {
202700
+ failure = ownershipLost ? `${ownershipLostReason}; wrapped up through message ${lastEnd}. Run /ctx-wrapup again to continue.` : leaseResult.reason === "timeout" ? `Timed out waiting for another process to release the compartment-state lease; wrapped up through message ${lastEnd}. Run /ctx-wrapup again to continue.` : "Another Magic Context rebuild started while wrapup was waiting. Run /ctx-wrapup again to continue.";
201796
202701
  break;
201797
202702
  }
202703
+ const leaseHolder = leaseResult.holderId;
201798
202704
  const leaseRenewal = setInterval(() => {
201799
202705
  try {
201800
202706
  renewCompartmentLease(deps.db, sessionId, leaseHolder);
@@ -201812,6 +202718,7 @@ Chunk ${chunkIndex}: wrapping messages ${plan.snapshot.offset}-${plan.snapshot.e
201812
202718
  runner: deps.runner,
201813
202719
  historianModel: deps.historianModel,
201814
202720
  fallbackModels: deps.historianFallbacks,
202721
+ fallbackModelId: modelKey,
201815
202722
  historianChunkTokens: deps.historianChunkTokens,
201816
202723
  boundarySnapshot: plan.snapshot,
201817
202724
  refreshBoundarySnapshot: () => resolveWrapupProtectedTailBoundary({
@@ -201897,15 +202804,19 @@ function resolvePiContextLimit(ctx) {
201897
202804
  }
201898
202805
  return 128000;
201899
202806
  }
201900
- async function acquireCompartmentLeaseEventually(db, sessionId, renewWrapupMarker) {
202807
+ async function acquireCompartmentLeaseEventually(db, sessionId, renewWrapupMarker, maxWaitMs) {
202808
+ const waitStartedAt = Date.now();
202809
+ const remainingMs = () => Math.max(0, waitStartedAt + maxWaitMs - Date.now());
201901
202810
  for (;; ) {
202811
+ if (remainingMs() <= 0)
202812
+ return { ok: false, reason: "timeout" };
201902
202813
  const holderId = crypto4.randomUUID();
201903
202814
  const lease = acquireCompartmentLease(db, sessionId, holderId);
201904
202815
  if (lease)
201905
- return holderId;
202816
+ return { ok: true, holderId };
201906
202817
  if (!renewWrapupMarker({}))
201907
- return null;
201908
- await new Promise((resolve3) => setTimeout(resolve3, LEASE_WAIT_MS));
202818
+ return { ok: false, reason: "ownership_lost" };
202819
+ await new Promise((resolve3) => setTimeout(resolve3, Math.min(LEASE_WAIT_MS, remainingMs())));
201909
202820
  }
201910
202821
  }
201911
202822
  function estimateChunks(tokens, chunkTokens) {
@@ -206393,16 +207304,16 @@ __export(exports_typebox, {
206393
207304
  // src/tools/ctx-expand.ts
206394
207305
  var ParamsSchema = exports_typebox.Object({
206395
207306
  start: exports_typebox.Optional(exports_typebox.Number({
206396
- description: "Start message ordinal (from compartment start attribute)"
207307
+ description: `First message ordinal to expand — a compartment's start="N" attribute, or an ordinal from a ctx_search message hit`
206397
207308
  })),
206398
207309
  end: exports_typebox.Optional(exports_typebox.Number({
206399
- description: "End message ordinal (from compartment end attribute)"
207310
+ description: `Last message ordinal to expand (inclusive) — a compartment's end="M" attribute`
206400
207311
  })),
206401
207312
  verbose: exports_typebox.Optional(exports_typebox.Boolean({
206402
207313
  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."
206403
207314
  })),
206404
207315
  message: exports_typebox.Optional(exports_typebox.Number({
206405
- 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."
207316
+ 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."
206406
207317
  }))
206407
207318
  });
206408
207319
  function ok(text) {
@@ -206553,25 +207464,45 @@ function formatMemoryList(memories) {
206553
207464
  id: String(m.id),
206554
207465
  category: m.category,
206555
207466
  status: m.status,
207467
+ verification: m.verificationStatus,
206556
207468
  updated: new Date(m.updatedAt).toISOString(),
206557
207469
  content: m.content.replace(/\s+/g, " ").trim()
206558
207470
  }));
207471
+ const headers = {
207472
+ id: "ID",
207473
+ category: "CATEGORY",
207474
+ status: "STATUS",
207475
+ verification: "VERIFY",
207476
+ updated: "UPDATED",
207477
+ content: "CONTENT"
207478
+ };
206559
207479
  const widths = {
206560
- id: Math.max(2, ...rows.map((r) => r.id.length)),
206561
- category: Math.max(8, ...rows.map((r) => r.category.length)),
206562
- status: Math.max(6, ...rows.map((r) => r.status.length)),
206563
- updated: Math.max(7, ...rows.map((r) => r.updated.length))
207480
+ id: Math.max(headers.id.length, ...rows.map((r) => r.id.length)),
207481
+ category: Math.max(headers.category.length, ...rows.map((r) => r.category.length)),
207482
+ status: Math.max(headers.status.length, ...rows.map((r) => r.status.length)),
207483
+ verification: Math.max(headers.verification.length, ...rows.map((r) => r.verification.length)),
207484
+ updated: Math.max(headers.updated.length, ...rows.map((r) => r.updated.length))
206564
207485
  };
206565
207486
  const fmt3 = (r) => [
206566
207487
  r.id.padEnd(widths.id),
206567
207488
  r.category.padEnd(widths.category),
206568
207489
  r.status.padEnd(widths.status),
207490
+ r.verification.padEnd(widths.verification),
206569
207491
  r.updated.padEnd(widths.updated),
206570
207492
  r.content
206571
207493
  ].join(" | ");
206572
207494
  return [
206573
207495
  `Found ${rows.length} active ${rows.length === 1 ? "memory" : "memories"}:`,
206574
207496
  "",
207497
+ fmt3(headers),
207498
+ [
207499
+ "-".repeat(widths.id),
207500
+ "-".repeat(widths.category),
207501
+ "-".repeat(widths.status),
207502
+ "-".repeat(widths.verification),
207503
+ "-".repeat(widths.updated),
207504
+ "-------"
207505
+ ].join("-+-"),
206575
207506
  ...rows.map(fmt3)
206576
207507
  ].join(`
206577
207508
  `);
@@ -206641,8 +207572,9 @@ function createCtxMemoryTool(deps) {
206641
207572
  const isOwn = targetIdentityForStoredPath(memory2.projectPath) === projectIdentity;
206642
207573
  if (isOwn)
206643
207574
  return true;
206644
- return toolShareCategories === null || toolShareCategories.includes(memory2.category);
207575
+ return toolShareCategories?.includes(memory2.category) ?? false;
206645
207576
  };
207577
+ const memoryOwnedByTool = (memory2) => workspaceIdentitySet.identities.length > 1 ? targetIdentityForStoredPath(memory2.projectPath) === projectIdentity : storedPathBelongsToIdentity(memory2.projectPath, projectIdentity);
206646
207578
  const snapshot = getProjectEmbeddingSnapshot(projectIdentity);
206647
207579
  if (snapshot ? !snapshot.features.memoryEnabled : deps.memoryEnabled === false) {
206648
207580
  return err2("Cross-session memory is disabled for this project.");
@@ -206689,7 +207621,8 @@ function createCtxMemoryTool(deps) {
206689
207621
  return err2("Error: 'content' is required when action is 'update'.");
206690
207622
  }
206691
207623
  const memory2 = getMemoryById(deps.db, updateId);
206692
- if (!memory2 || !memoryVisibleToTool(memory2)) {
207624
+ const updateAllowed = memory2 ? dreamerAllowed ? memoryVisibleToTool(memory2) : memoryOwnedByTool(memory2) : false;
207625
+ if (!memory2 || !updateAllowed) {
206693
207626
  return err2(`Error: Memory with ID ${updateId} was not found.`);
206694
207627
  }
206695
207628
  if (!dreamerAllowed && !isPrimaryMutableMemory(memory2)) {
@@ -206736,7 +207669,7 @@ function createCtxMemoryTool(deps) {
206736
207669
  return err2("Error: One or more source memories were not found.");
206737
207670
  }
206738
207671
  if (!dreamerAllowed) {
206739
- const foreign = sourceMemories.find((memory2) => !memoryVisibleToTool(memory2));
207672
+ const foreign = sourceMemories.find((memory2) => !memoryOwnedByTool(memory2));
206740
207673
  if (foreign) {
206741
207674
  return err2(`Error: Memory with ID ${foreign.id} was not found.`);
206742
207675
  }
@@ -206838,7 +207771,8 @@ function createCtxMemoryTool(deps) {
206838
207771
  const archiveIds = [...new Set(rawArchiveIds)];
206839
207772
  for (const memoryId of archiveIds) {
206840
207773
  const memory2 = getMemoryById(deps.db, memoryId);
206841
- if (!memory2 || !memoryVisibleToTool(memory2)) {
207774
+ const archiveAllowed = memory2 ? dreamerAllowed ? memoryVisibleToTool(memory2) : memoryOwnedByTool(memory2) : false;
207775
+ if (!memory2 || !archiveAllowed) {
206842
207776
  return err2(`Error: Memory with ID ${memoryId} was not found.`);
206843
207777
  }
206844
207778
  if (!dreamerAllowed && !isPrimaryMutableMemory(memory2)) {
@@ -206977,6 +207911,7 @@ function createCtxNoteTool(deps) {
206977
207911
  parameters: ParamsSchema3,
206978
207912
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
206979
207913
  const sessionId = ctx.sessionManager.getSessionId();
207914
+ const dreamerEnabled = deps.resolveDreamerEnabled?.(ctx) ?? deps.dreamerEnabled;
206980
207915
  const action2 = params.action ?? (params.content?.trim() ? "write" : "read");
206981
207916
  if (action2 === "write") {
206982
207917
  const content = params.content?.trim();
@@ -206985,7 +207920,7 @@ function createCtxNoteTool(deps) {
206985
207920
  const anchorOrdinal = captureAnchorOrdinal(deps.db, sessionId);
206986
207921
  const surfaceCondition = params.surface_condition?.trim();
206987
207922
  if (surfaceCondition) {
206988
- if (deps.dreamerEnabled !== true) {
207923
+ if (dreamerEnabled !== true) {
206989
207924
  return err3("Error: Smart notes require dreamer to be enabled. Enable dreamer in magic-context.jsonc to use surface_condition.");
206990
207925
  }
206991
207926
  const projectIdentity = resolveProjectIdentity(ctx.cwd);
@@ -206994,6 +207929,7 @@ function createCtxNoteTool(deps) {
206994
207929
  }
206995
207930
  const note2 = addNote(deps.db, "smart", {
206996
207931
  content,
207932
+ sessionId,
206997
207933
  projectPath: projectIdentity,
206998
207934
  surfaceCondition,
206999
207935
  anchorOrdinal
@@ -207245,6 +208181,7 @@ function createCtxReduceTool(deps) {
207245
208181
  parameters: ParamsSchema4,
207246
208182
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
207247
208183
  const sessionId = ctx.sessionManager.getSessionId();
208184
+ const protectedTags = Math.max(0, Math.floor(deps.resolveProtectedTags?.(ctx) ?? deps.protectedTags));
207248
208185
  if (!params.drop) {
207249
208186
  return err4("Error: 'drop' must be provided.");
207250
208187
  }
@@ -207262,7 +208199,7 @@ function createCtxReduceTool(deps) {
207262
208199
  return err4(`Error: Unknown tag(s) ${formatIds(unknownIds)}. Check available tags in conversation.`);
207263
208200
  }
207264
208201
  const activeTags = allTags.filter((tag) => tag.status === "active");
207265
- const protectedTagIds = activeTags.map((tag) => tag.tagNumber).sort((left, right) => right - left).slice(0, deps.protectedTags);
208202
+ const protectedTagIds = activeTags.map((tag) => tag.tagNumber).sort((left, right) => right - left).slice(0, protectedTags);
207266
208203
  const protectedSet = new Set(protectedTagIds);
207267
208204
  const tagStatusMap = new Map(allTags.map((tag) => [tag.tagNumber, tag.status]));
207268
208205
  const pendingOps = getPendingOps(deps.db, sessionId);
@@ -207489,18 +208426,430 @@ function createCtxSearchTool(deps) {
207489
208426
  };
207490
208427
  }
207491
208428
 
208429
+ // src/tools/todo-view-pi.ts
208430
+ import {
208431
+ truncateToWidth as truncateToWidth2
208432
+ } from "@earendil-works/pi-tui";
208433
+ var TODO_TOOL_NAME = "todowrite";
208434
+ var TODOS_COMMAND_NAME = "todos";
208435
+ var WIDGET_KEY = "magic-context-todos";
208436
+ var MAX_TODO_CONTENT_ROWS = 12;
208437
+ var STATUS_GLYPH = {
208438
+ pending: "○",
208439
+ in_progress: "◐",
208440
+ completed: "✓",
208441
+ cancelled: "✗"
208442
+ };
208443
+ var STATUS_COLOR = {
208444
+ pending: "dim",
208445
+ in_progress: "warning",
208446
+ completed: "success",
208447
+ cancelled: "error"
208448
+ };
208449
+ var snapshotsBySession = new Map;
208450
+ function isTodoStatus2(value) {
208451
+ return TODO_STATUSES.includes(value);
208452
+ }
208453
+ function isTodoPriority2(value) {
208454
+ return TODO_PRIORITIES.includes(value);
208455
+ }
208456
+ function cloneTodos(todos) {
208457
+ return todos.map((todo) => ({ ...todo }));
208458
+ }
208459
+ function parseTodos(input) {
208460
+ if (!Array.isArray(input))
208461
+ return null;
208462
+ const todos = [];
208463
+ for (const item of input) {
208464
+ if (item === null || typeof item !== "object")
208465
+ return null;
208466
+ const raw = item;
208467
+ if (typeof raw.content !== "string" || !isTodoStatus2(raw.status)) {
208468
+ return null;
208469
+ }
208470
+ const todo = {
208471
+ content: raw.content,
208472
+ status: raw.status
208473
+ };
208474
+ if (isTodoPriority2(raw.priority))
208475
+ todo.priority = raw.priority;
208476
+ if (typeof raw.id === "string" && raw.id.length > 0)
208477
+ todo.id = raw.id;
208478
+ todos.push(todo);
208479
+ }
208480
+ return todos;
208481
+ }
208482
+ function parseTodoStateJson(stateJson) {
208483
+ if (!stateJson)
208484
+ return null;
208485
+ try {
208486
+ return parseTodos(JSON.parse(stateJson));
208487
+ } catch {
208488
+ return null;
208489
+ }
208490
+ }
208491
+ function setTodoSnapshot(sessionId, todos) {
208492
+ const parsed = parseTodos(todos);
208493
+ if (parsed === null)
208494
+ return false;
208495
+ snapshotsBySession.set(sessionId, { todos: parsed });
208496
+ return true;
208497
+ }
208498
+ function seedTodoSnapshotFromStateJson(sessionId, stateJson) {
208499
+ const parsed = parseTodoStateJson(stateJson);
208500
+ if (parsed === null) {
208501
+ snapshotsBySession.delete(sessionId);
208502
+ return false;
208503
+ }
208504
+ snapshotsBySession.set(sessionId, { todos: parsed });
208505
+ return true;
208506
+ }
208507
+ function getTodoSnapshot(sessionId) {
208508
+ if (!sessionId)
208509
+ return { todos: [] };
208510
+ const snapshot = snapshotsBySession.get(sessionId);
208511
+ return { todos: snapshot ? cloneTodos(snapshot.todos) : [] };
208512
+ }
208513
+ function clearTodoSnapshot(sessionId) {
208514
+ snapshotsBySession.delete(sessionId);
208515
+ }
208516
+ function getSessionId(ctx) {
208517
+ try {
208518
+ const id = ctx.sessionManager?.getSessionId?.();
208519
+ return typeof id === "string" && id.length > 0 ? id : undefined;
208520
+ } catch {
208521
+ return;
208522
+ }
208523
+ }
208524
+ function countTodos(todos) {
208525
+ const counts = {
208526
+ total: todos.length,
208527
+ pending: 0,
208528
+ inProgress: 0,
208529
+ completed: 0,
208530
+ cancelled: 0
208531
+ };
208532
+ for (const todo of todos) {
208533
+ switch (todo.status) {
208534
+ case "pending":
208535
+ counts.pending++;
208536
+ break;
208537
+ case "in_progress":
208538
+ counts.inProgress++;
208539
+ break;
208540
+ case "completed":
208541
+ counts.completed++;
208542
+ break;
208543
+ case "cancelled":
208544
+ counts.cancelled++;
208545
+ break;
208546
+ }
208547
+ }
208548
+ return counts;
208549
+ }
208550
+ function activeTitleCount(todos) {
208551
+ return todos.filter((todo) => !TITLE_DONE_STATUSES.has(todo.status)).length;
208552
+ }
208553
+ function formatCounts(counts) {
208554
+ if (counts.total === 0)
208555
+ return "No todos";
208556
+ const parts = [`${counts.completed}/${counts.total} completed`];
208557
+ if (counts.inProgress > 0)
208558
+ parts.push(`${counts.inProgress} in progress`);
208559
+ if (counts.pending > 0)
208560
+ parts.push(`${counts.pending} pending`);
208561
+ if (counts.cancelled > 0)
208562
+ parts.push(`${counts.cancelled} cancelled`);
208563
+ return parts.join(" · ");
208564
+ }
208565
+ function formatTodoLine(todo, theme, options = {}) {
208566
+ const glyph = theme.fg(STATUS_COLOR[todo.status], STATUS_GLYPH[todo.status]);
208567
+ const id = options.showId && todo.id ? `${theme.fg("accent", `#${todo.id}`)} ` : "";
208568
+ const color = todo.status === "pending" || todo.status === "in_progress" ? "text" : "dim";
208569
+ let content = theme.fg(color, todo.content);
208570
+ if (todo.status === "completed" || todo.status === "cancelled") {
208571
+ content = theme.strikethrough(content);
208572
+ }
208573
+ return `${glyph} ${id}${content}`;
208574
+ }
208575
+ function formatCommandLine(todo) {
208576
+ const id = todo.id ? `#${todo.id} ` : "";
208577
+ return ` ${STATUS_GLYPH[todo.status]} ${id}${todo.content}`;
208578
+ }
208579
+ function lineComponent(renderLines) {
208580
+ return {
208581
+ render: renderLines,
208582
+ invalidate() {}
208583
+ };
208584
+ }
208585
+ function capTodoRows(rows) {
208586
+ if (rows.length <= MAX_TODO_CONTENT_ROWS) {
208587
+ return { visible: [...rows], hiddenCount: 0 };
208588
+ }
208589
+ const visibleCount = Math.max(0, MAX_TODO_CONTENT_ROWS - 1);
208590
+ return {
208591
+ visible: rows.slice(0, visibleCount),
208592
+ hiddenCount: rows.length - visibleCount
208593
+ };
208594
+ }
208595
+ function renderTodowriteCall(args, theme) {
208596
+ const todos = parseTodos(args.todos) ?? [];
208597
+ const active = activeTitleCount(todos);
208598
+ const activeColor = active > 0 ? "warning" : "success";
208599
+ const line = `${theme.fg("toolTitle", theme.bold("Todos"))} ${theme.fg("muted", "—")} ${theme.fg(activeColor, `${active} active`)}`;
208600
+ return lineComponent((width) => [truncateToWidth2(line, width, "…")]);
208601
+ }
208602
+ function renderTodowriteResult(result, theme) {
208603
+ const detailsTodos = result.details?.todos;
208604
+ let todos = parseTodos(detailsTodos);
208605
+ if (todos === null && Array.isArray(result.content)) {
208606
+ const text = result.content.find((part) => part !== null && typeof part === "object" && part.type === "text" && typeof part.text === "string")?.text;
208607
+ if (text) {
208608
+ try {
208609
+ todos = parseTodos(JSON.parse(text));
208610
+ } catch {}
208611
+ }
208612
+ }
208613
+ const renderedTodos = todos ?? [];
208614
+ return lineComponent((width) => {
208615
+ if (renderedTodos.length === 0)
208616
+ return [theme.fg("dim", "No todos")];
208617
+ const { visible, hiddenCount } = capTodoRows(renderedTodos);
208618
+ const lines = visible.map((todo) => truncateToWidth2(formatTodoLine(todo, theme, { showId: true }), width, "…"));
208619
+ if (hiddenCount > 0) {
208620
+ lines.push(truncateToWidth2(theme.fg("dim", `+${hiddenCount} more`), width, "…"));
208621
+ }
208622
+ return lines;
208623
+ });
208624
+ }
208625
+ function registerTodosCommand(pi) {
208626
+ pi.registerCommand(TODOS_COMMAND_NAME, {
208627
+ description: "Show the current Magic Context todo list",
208628
+ handler: async (_args, ctx) => {
208629
+ if (!ctx.hasUI) {
208630
+ ctx.ui.notify("The /todos command requires interactive UI.", "error");
208631
+ return;
208632
+ }
208633
+ const sessionId = getSessionId(ctx);
208634
+ const todos = getTodoSnapshot(sessionId).todos;
208635
+ if (todos.length === 0) {
208636
+ ctx.ui.notify("No todos yet.", "info");
208637
+ return;
208638
+ }
208639
+ const counts = countTodos(todos);
208640
+ const lines = [formatCounts(counts)];
208641
+ const groups = [
208642
+ ["pending", "── Pending ──"],
208643
+ ["in_progress", "── In Progress ──"],
208644
+ ["completed", "── Completed ──"],
208645
+ ["cancelled", "── Cancelled ──"]
208646
+ ];
208647
+ for (const [status, heading] of groups) {
208648
+ const group = todos.filter((todo) => todo.status === status);
208649
+ if (group.length === 0)
208650
+ continue;
208651
+ lines.push(heading);
208652
+ const { visible, hiddenCount } = capTodoRows(group);
208653
+ for (const todo of visible)
208654
+ lines.push(formatCommandLine(todo));
208655
+ if (hiddenCount > 0)
208656
+ lines.push(` +${hiddenCount} more`);
208657
+ }
208658
+ ctx.ui.notify(lines.join(`
208659
+ `), "info");
208660
+ }
208661
+ });
208662
+ }
208663
+ function todoKey(todo, index) {
208664
+ return todo.id ? `id:${todo.id}` : `pos:${index}:${todo.content}`;
208665
+ }
208666
+ function isOverlayLive(todo) {
208667
+ return todo.status === "pending" || todo.status === "in_progress";
208668
+ }
208669
+
208670
+ class TodoOverlay {
208671
+ uiCtx;
208672
+ sessionId;
208673
+ widgetRegistered = false;
208674
+ tui;
208675
+ completedTaskIdsPendingHide = new Set;
208676
+ hiddenCompletedTaskIds = new Set;
208677
+ lastTodoKeys = new Set;
208678
+ setUICtx(sessionId, ctx) {
208679
+ if (ctx !== this.uiCtx || sessionId !== this.sessionId) {
208680
+ this.uiCtx = ctx;
208681
+ this.sessionId = sessionId;
208682
+ this.widgetRegistered = false;
208683
+ this.tui = undefined;
208684
+ this.resetCompletedDisplayState();
208685
+ }
208686
+ }
208687
+ update(sessionId = this.sessionId) {
208688
+ if (!this.uiCtx || !this.sessionId || sessionId !== this.sessionId)
208689
+ return;
208690
+ const todos = getTodoSnapshot(this.sessionId).todos;
208691
+ this.pruneCompletedDisplayState(todos);
208692
+ const visible = this.selectOverlayTodos(todos);
208693
+ if (visible.length === 0) {
208694
+ if (this.widgetRegistered) {
208695
+ this.uiCtx.setWidget(WIDGET_KEY, undefined);
208696
+ this.widgetRegistered = false;
208697
+ this.tui = undefined;
208698
+ }
208699
+ return;
208700
+ }
208701
+ if (!this.widgetRegistered) {
208702
+ this.uiCtx.setWidget(WIDGET_KEY, (tui, theme) => {
208703
+ this.tui = tui;
208704
+ return {
208705
+ render: (width) => this.renderWidget(theme, width),
208706
+ invalidate: () => {
208707
+ this.widgetRegistered = false;
208708
+ this.tui = undefined;
208709
+ }
208710
+ };
208711
+ }, { placement: "aboveEditor" });
208712
+ this.widgetRegistered = true;
208713
+ } else {
208714
+ this.tui?.requestRender();
208715
+ }
208716
+ }
208717
+ resetCompletedDisplayState() {
208718
+ this.completedTaskIdsPendingHide.clear();
208719
+ this.hiddenCompletedTaskIds.clear();
208720
+ this.lastTodoKeys.clear();
208721
+ }
208722
+ hideCompletedTasksFromPreviousTurn() {
208723
+ if (this.completedTaskIdsPendingHide.size === 0)
208724
+ return;
208725
+ for (const taskId of this.completedTaskIdsPendingHide) {
208726
+ this.hiddenCompletedTaskIds.add(taskId);
208727
+ }
208728
+ this.completedTaskIdsPendingHide.clear();
208729
+ this.update();
208730
+ }
208731
+ disposeSession(sessionId) {
208732
+ if (this.sessionId !== sessionId)
208733
+ return;
208734
+ this.dispose();
208735
+ }
208736
+ dispose() {
208737
+ if (this.uiCtx)
208738
+ this.uiCtx.setWidget(WIDGET_KEY, undefined);
208739
+ this.widgetRegistered = false;
208740
+ this.tui = undefined;
208741
+ this.uiCtx = undefined;
208742
+ this.sessionId = undefined;
208743
+ this.resetCompletedDisplayState();
208744
+ }
208745
+ pruneCompletedDisplayState(todos) {
208746
+ const currentKeys = new Set(todos.map((todo, index) => todoKey(todo, index)));
208747
+ const hasSharedKeys = [...currentKeys].some((key) => this.lastTodoKeys.has(key));
208748
+ if (this.lastTodoKeys.size > 0 && currentKeys.size > 0 && !hasSharedKeys) {
208749
+ this.resetCompletedDisplayState();
208750
+ }
208751
+ this.lastTodoKeys = currentKeys;
208752
+ const completedKeys = new Set(todos.map((todo, index) => ({ todo, key: todoKey(todo, index) })).filter(({ todo }) => todo.status === "completed").map(({ key }) => key));
208753
+ for (const taskId of this.completedTaskIdsPendingHide) {
208754
+ if (!completedKeys.has(taskId))
208755
+ this.completedTaskIdsPendingHide.delete(taskId);
208756
+ }
208757
+ for (const taskId of this.hiddenCompletedTaskIds) {
208758
+ if (!completedKeys.has(taskId))
208759
+ this.hiddenCompletedTaskIds.delete(taskId);
208760
+ }
208761
+ }
208762
+ selectOverlayTodos(todos) {
208763
+ return todos.map((todo, index) => ({ todo, key: todoKey(todo, index) })).filter(({ todo, key }) => {
208764
+ if (isOverlayLive(todo))
208765
+ return true;
208766
+ return todo.status === "completed" && !this.hiddenCompletedTaskIds.has(key);
208767
+ });
208768
+ }
208769
+ renderWidget(theme, width) {
208770
+ if (!this.sessionId)
208771
+ return [];
208772
+ const todos = getTodoSnapshot(this.sessionId).todos;
208773
+ this.pruneCompletedDisplayState(todos);
208774
+ const overlayTodos = this.selectOverlayTodos(todos);
208775
+ if (overlayTodos.length === 0)
208776
+ return [];
208777
+ const counts = countTodos(todos);
208778
+ const hasInProgress = counts.inProgress > 0;
208779
+ const headingColor = hasInProgress ? "accent" : "dim";
208780
+ const headingIcon = hasInProgress ? "●" : "○";
208781
+ const heading = `${theme.fg(headingColor, headingIcon)} ${theme.fg(headingColor, "Todos")} ${theme.fg("muted", "—")} ${theme.fg("dim", formatCounts(counts))}`;
208782
+ const truncate3 = (line) => truncateToWidth2(line, width, "…");
208783
+ const lines = [truncate3(heading)];
208784
+ const { visible, hiddenCount } = capTodoRows(overlayTodos);
208785
+ for (const [index, { todo, key }] of visible.entries()) {
208786
+ const isLast = index === visible.length - 1 && hiddenCount === 0;
208787
+ const branch = theme.fg("dim", isLast ? "└─" : "├─");
208788
+ lines.push(truncate3(`${branch} ${formatTodoLine(todo, theme, { showId: true })}`));
208789
+ if (todo.status === "completed")
208790
+ this.completedTaskIdsPendingHide.add(key);
208791
+ }
208792
+ if (hiddenCount > 0) {
208793
+ lines.push(truncate3(`${theme.fg("dim", "└─")} ${theme.fg("dim", `+${hiddenCount} more`)}`));
208794
+ }
208795
+ return lines;
208796
+ }
208797
+ }
208798
+ function registerTodoOverlay(pi, deps) {
208799
+ const overlay = new TodoOverlay;
208800
+ pi.on("session_start", async (_event, ctx) => {
208801
+ const sessionId = getSessionId(ctx);
208802
+ if (!sessionId)
208803
+ return;
208804
+ seedTodoSnapshotFromStateJson(sessionId, deps.readLastTodoState(sessionId));
208805
+ if (!ctx.hasUI)
208806
+ return;
208807
+ overlay.setUICtx(sessionId, ctx.ui);
208808
+ overlay.update(sessionId);
208809
+ });
208810
+ pi.on("agent_start", async () => {
208811
+ overlay.hideCompletedTasksFromPreviousTurn();
208812
+ });
208813
+ pi.on("session_shutdown", async (_event, ctx) => {
208814
+ const sessionId = getSessionId(ctx);
208815
+ if (!sessionId)
208816
+ return;
208817
+ clearTodoSnapshot(sessionId);
208818
+ overlay.disposeSession(sessionId);
208819
+ });
208820
+ pi.on("session_before_switch", async (_event, ctx) => {
208821
+ const sessionId = getSessionId(ctx);
208822
+ if (!sessionId)
208823
+ return;
208824
+ clearTodoSnapshot(sessionId);
208825
+ overlay.disposeSession(sessionId);
208826
+ });
208827
+ return overlay;
208828
+ }
208829
+ function registerTodoStateLifecycle(pi, deps) {
208830
+ pi.on("session_start", async (_event, ctx) => {
208831
+ const sessionId = getSessionId(ctx);
208832
+ if (!sessionId)
208833
+ return;
208834
+ seedTodoSnapshotFromStateJson(sessionId, deps.readLastTodoState(sessionId));
208835
+ });
208836
+ pi.on("session_shutdown", async (_event, ctx) => {
208837
+ const sessionId = getSessionId(ctx);
208838
+ if (sessionId)
208839
+ clearTodoSnapshot(sessionId);
208840
+ });
208841
+ pi.on("session_before_switch", async (_event, ctx) => {
208842
+ const sessionId = getSessionId(ctx);
208843
+ if (sessionId)
208844
+ clearTodoSnapshot(sessionId);
208845
+ });
208846
+ }
208847
+
207492
208848
  // src/tools/todowrite.ts
207493
- var STATUS_VALUES = [
207494
- "pending",
207495
- "in_progress",
207496
- "completed",
207497
- "cancelled"
207498
- ];
207499
- var PRIORITY_VALUES = ["high", "medium", "low"];
207500
208849
  var TodoItem = exports_typebox.Object({
207501
208850
  content: exports_typebox.String({ description: "Brief description of the task" }),
207502
- status: exports_typebox.Union(STATUS_VALUES.map((v) => exports_typebox.Literal(v))),
207503
- priority: exports_typebox.Optional(exports_typebox.Union(PRIORITY_VALUES.map((v) => exports_typebox.Literal(v)))),
208851
+ status: exports_typebox.Union(TODO_STATUSES.map((v) => exports_typebox.Literal(v))),
208852
+ priority: exports_typebox.Optional(exports_typebox.Union(TODO_PRIORITIES.map((v) => exports_typebox.Literal(v)))),
207504
208853
  id: exports_typebox.Optional(exports_typebox.String({ description: "Optional stable id for the todo" }))
207505
208854
  });
207506
208855
  var TodowriteParams = exports_typebox.Object({
@@ -207508,34 +208857,24 @@ var TodowriteParams = exports_typebox.Object({
207508
208857
  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."
207509
208858
  })
207510
208859
  });
207511
- var TOOL_DESCRIPTION = [
207512
- "Manage your task list for this session.",
207513
- "",
207514
- "Use this tool to plan multi-step work, track in-flight tasks, and",
207515
- "mark progress as you complete steps. Pass the COMPLETE updated list",
207516
- "of todos every time — this tool replaces the prior list rather than",
207517
- "appending to it.",
207518
- "",
207519
- "Task states:",
207520
- " - pending: not started yet",
207521
- " - in_progress: currently working on (limit to ONE task at a time)",
207522
- " - completed: finished successfully",
207523
- " - cancelled: no longer needed",
207524
- "",
207525
- "Use this tool proactively for non-trivial work spanning 3+ steps.",
207526
- "Skip it for single-shot answers or trivial 1-2 step tasks."
207527
- ].join(`
207528
- `);
208860
+ var PROMPT_SNIPPET = "Manage a task list to track multi-step progress";
208861
+ var PROMPT_GUIDELINES = [
208862
+ "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.",
208863
+ "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.",
208864
+ "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.",
208865
+ "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."
208866
+ ];
207529
208867
  function createTodowriteTool() {
207530
208868
  return {
207531
- name: "todowrite",
208869
+ name: TODO_TOOL_NAME,
207532
208870
  label: "Todos",
207533
- description: TOOL_DESCRIPTION,
208871
+ description: "Manage the session task list.",
208872
+ promptSnippet: PROMPT_SNIPPET,
208873
+ promptGuidelines: PROMPT_GUIDELINES,
207534
208874
  parameters: TodowriteParams,
207535
208875
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
207536
208876
  const todos = params.todos ?? [];
207537
- const completed = todos.filter((t) => t.status === "completed").length;
207538
- const active = todos.length - completed;
208877
+ const active = todos.filter((todo) => !TITLE_DONE_STATUSES.has(todo.status)).length;
207539
208878
  return {
207540
208879
  content: [
207541
208880
  {
@@ -207549,6 +208888,12 @@ function createTodowriteTool() {
207549
208888
  truncated: false
207550
208889
  }
207551
208890
  };
208891
+ },
208892
+ renderCall(args, theme) {
208893
+ return renderTodowriteCall(args, theme);
208894
+ },
208895
+ renderResult(result, _opts, theme) {
208896
+ return renderTodowriteResult(result, theme);
207552
208897
  }
207553
208898
  };
207554
208899
  }
@@ -207574,15 +208919,22 @@ function registerMagicContextTools(pi, opts) {
207574
208919
  if (!opts.sessionScopedToolsDisabled) {
207575
208920
  pi.registerTool(createCtxNoteTool({
207576
208921
  db: opts.db,
207577
- dreamerEnabled: opts.dreamerEnabled ?? false
208922
+ dreamerEnabled: opts.dreamerEnabled ?? false,
208923
+ resolveDreamerEnabled: opts.resolveDreamerEnabled
207578
208924
  }));
207579
208925
  pi.registerTool(createCtxExpandTool({ db: opts.db }));
207580
208926
  }
207581
- pi.registerTool(createTodowriteTool());
208927
+ if (opts.todowriteEnabled !== false) {
208928
+ pi.registerTool(createTodowriteTool());
208929
+ if (opts.todowriteCommandEnabled !== false) {
208930
+ registerTodosCommand(pi);
208931
+ }
208932
+ }
207582
208933
  if (!opts.sessionScopedToolsDisabled) {
207583
208934
  pi.registerTool(createCtxReduceTool({
207584
208935
  db: opts.db,
207585
- protectedTags: opts.protectedTags ?? 20
208936
+ protectedTags: opts.protectedTags ?? 20,
208937
+ resolveProtectedTags: opts.resolveProtectedTags
207586
208938
  }));
207587
208939
  }
207588
208940
  }
@@ -207594,6 +208946,10 @@ function resolveCurrentProject(ctx) {
207594
208946
  const projectIdentity = resolveProjectIdentityOrFallback(projectDir);
207595
208947
  return { projectDir, projectIdentity };
207596
208948
  }
208949
+ function signalPiDeferredCompactionMarkerDrain(sessionId) {
208950
+ signalPiDeferredHistoryRefresh(sessionId);
208951
+ signalPiDeferredMaterialization(sessionId);
208952
+ }
207597
208953
  function persistPiMessageEndModelMeta(args) {
207598
208954
  if (!args.message || typeof args.message !== "object")
207599
208955
  return;
@@ -207609,6 +208965,55 @@ function persistPiMessageEndModelMeta(args) {
207609
208965
  updateSessionMeta(args.db, args.sessionId, { cacheTtl });
207610
208966
  }
207611
208967
  }
208968
+ function getCompatiblePiTodoCapture(todos) {
208969
+ if (!Array.isArray(todos))
208970
+ return null;
208971
+ const normalized = normalizeTodoStateJson(todos);
208972
+ if (normalized === null)
208973
+ return null;
208974
+ return { normalized, todos };
208975
+ }
208976
+ function applyCompatiblePiTodoCapture(args) {
208977
+ if (args.todowriteEnabled) {
208978
+ setTodoSnapshot(args.sessionId, args.capture.todos);
208979
+ args.todoOverlay?.update(args.sessionId);
208980
+ }
208981
+ if (args.persist) {
208982
+ updateSessionMeta(args.db, args.sessionId, {
208983
+ lastTodoState: args.capture.normalized
208984
+ });
208985
+ }
208986
+ }
208987
+ function capturePiTodowriteArgsIfCompatible(args) {
208988
+ const capture = getCompatiblePiTodoCapture(args.todos);
208989
+ if (capture === null)
208990
+ return false;
208991
+ applyCompatiblePiTodoCapture({ ...args, capture });
208992
+ return true;
208993
+ }
208994
+ function capturePiTodowriteMessageIfCompatible(args) {
208995
+ const msg = args.message;
208996
+ if (msg?.role !== "assistant" || !Array.isArray(msg.content)) {
208997
+ return false;
208998
+ }
208999
+ for (const block of msg.content) {
209000
+ if (!block || typeof block !== "object")
209001
+ continue;
209002
+ const b = block;
209003
+ if (b.type !== "toolCall")
209004
+ continue;
209005
+ if (typeof b.name !== "string")
209006
+ continue;
209007
+ if (b.name !== "todowrite")
209008
+ continue;
209009
+ const capture = getCompatiblePiTodoCapture(b.arguments?.todos);
209010
+ if (capture === null)
209011
+ continue;
209012
+ applyCompatiblePiTodoCapture({ ...args, capture });
209013
+ return true;
209014
+ }
209015
+ return false;
209016
+ }
207612
209017
  function info(message, data) {
207613
209018
  log(`${PREFIX} ${message}`, data);
207614
209019
  }
@@ -207616,6 +209021,7 @@ function warn(message, data) {
207616
209021
  log(`${PREFIX} WARN ${message}`, data);
207617
209022
  }
207618
209023
  var migratedConfigDirs = new Set;
209024
+ var loggedPiConfigDirs = new Set;
207619
209025
  function ensureConfigLocationsMigrated(dir) {
207620
209026
  if (migratedConfigDirs.has(dir))
207621
209027
  return;
@@ -207625,6 +209031,28 @@ function ensureConfigLocationsMigrated(dir) {
207625
209031
  info: (m) => info(m)
207626
209032
  });
207627
209033
  }
209034
+ function logPiConfigLoad(args) {
209035
+ const key = resolve3(args.dir);
209036
+ if (args.dedupe && loggedPiConfigDirs.has(key))
209037
+ return;
209038
+ if (args.dedupe) {
209039
+ loggedPiConfigDirs.add(key);
209040
+ }
209041
+ if (args.loadedFromPaths.length > 0) {
209042
+ info(`config loaded from: ${args.loadedFromPaths.join(", ")}`);
209043
+ } else {
209044
+ info("config: no magic-context.jsonc found, using schema defaults");
209045
+ }
209046
+ for (const warning of args.warnings) {
209047
+ warn(`config: ${warning}`);
209048
+ }
209049
+ }
209050
+ var __test = {
209051
+ logPiConfigLoad,
209052
+ resetLoggedPiConfigDirs() {
209053
+ loggedPiConfigDirs.clear();
209054
+ }
209055
+ };
207628
209056
  function formatTokens(value) {
207629
209057
  return value.toLocaleString();
207630
209058
  }
@@ -207798,8 +209226,7 @@ async function src_default2(pi) {
207798
209226
  try {
207799
209227
  const pendingPiMarkerSessions = getSessionsWithPendingPiMarker(db);
207800
209228
  for (const sid of pendingPiMarkerSessions) {
207801
- signalPiDeferredHistoryRefresh(sid);
207802
- signalPiDeferredMaterialization(sid);
209229
+ signalPiDeferredCompactionMarkerDrain(sid);
207803
209230
  }
207804
209231
  if (pendingPiMarkerSessions.length > 0) {
207805
209232
  log(`${PREFIX} rehydrated ${pendingPiMarkerSessions.length} Pi deferred compaction marker session(s)`);
@@ -207812,14 +209239,12 @@ async function src_default2(pi) {
207812
209239
  const { config: config2, warnings, loadedFromPaths } = loadPiConfig({
207813
209240
  cwd: projectDir
207814
209241
  });
207815
- if (loadedFromPaths.length > 0) {
207816
- info(`config loaded from: ${loadedFromPaths.join(", ")}`);
207817
- } else {
207818
- info("config: no magic-context.jsonc found, using schema defaults");
207819
- }
207820
- for (const w of warnings) {
207821
- warn(`config: ${w}`);
207822
- }
209242
+ logPiConfigLoad({
209243
+ dir: projectDir,
209244
+ loadedFromPaths,
209245
+ warnings,
209246
+ dedupe: true
209247
+ });
207823
209248
  setSqlitePragmaConfig({
207824
209249
  cacheSizeMb: config2.sqlite.cache_size_mb,
207825
209250
  mmapSizeMb: config2.sqlite.mmap_size_mb
@@ -207832,26 +209257,7 @@ async function src_default2(pi) {
207832
209257
  }
207833
209258
  await ensureProjectRegisteredFromPiDirectory(projectDir, db);
207834
209259
  info(`registered embedding config for project ${projectIdentity}`);
207835
- registerMagicContextTools(pi, {
207836
- db,
207837
- ensureProjectRegistered: ensureProjectRegisteredFromPiDirectory,
207838
- allowDreamerActions: false,
207839
- memoryToolEnabled: true,
207840
- protectedTags: config2.protected_tags ?? 20,
207841
- dreamerEnabled: isDreamerRunnable(config2)
207842
- });
207843
- info("registered tools: ctx_search, ctx_memory, ctx_note, ctx_expand, ctx_reduce");
207844
- const historianConfig = resolveHistorianFromConfig(config2);
207845
- if (historianConfig) {
207846
- historianConfig.onStatusChange = (ctx) => {
207847
- updateStatusLine(ctx, {
207848
- db,
207849
- projectIdentity: resolveCurrentProject(ctx).projectIdentity
207850
- });
207851
- };
207852
- }
207853
- const autoSearchConfig = resolveAutoSearchFromConfig(config2);
207854
- const contextOptionsByDir = new Map;
209260
+ const projectDepsByDir = new Map;
207855
209261
  const buildContextOptions = (cfg, hist, auto) => ({
207856
209262
  db: database,
207857
209263
  smartDrops: cfg.smart_drops === true,
@@ -207892,46 +209298,117 @@ async function src_default2(pi) {
207892
209298
  });
207893
209299
  }
207894
209300
  });
207895
- function resolveContextOptionsForProject(dir) {
207896
- const cached2 = contextOptionsByDir.get(dir);
207897
- if (cached2)
207898
- return cached2;
207899
- ensureConfigLocationsMigrated(dir);
207900
- const switchedConfig = loadPiConfig({ cwd: dir }).config;
207901
- const switchedHistorian = resolveHistorianFromConfig(switchedConfig);
207902
- if (switchedHistorian) {
207903
- switchedHistorian.onStatusChange = (ctx) => {
209301
+ function buildProjectDeps(dir, identity, cfg) {
209302
+ const hist = resolveHistorianFromConfig(cfg);
209303
+ if (hist) {
209304
+ hist.onStatusChange = (ctx) => {
207904
209305
  updateStatusLine(ctx, {
207905
209306
  db: database,
207906
209307
  projectIdentity: resolveCurrentProject(ctx).projectIdentity
207907
209308
  });
207908
209309
  };
207909
209310
  }
207910
- const built = buildContextOptions(switchedConfig, switchedHistorian, resolveAutoSearchFromConfig(switchedConfig));
207911
- contextOptionsByDir.set(dir, built);
209311
+ const auto = resolveAutoSearchFromConfig(cfg);
209312
+ return {
209313
+ projectDir: dir,
209314
+ projectIdentity: identity,
209315
+ config: cfg,
209316
+ historianConfig: hist,
209317
+ autoSearchConfig: auto,
209318
+ contextOptions: buildContextOptions(cfg, hist, auto),
209319
+ sidekickConfig: resolveSidekickFromConfig(cfg),
209320
+ dreamerConfig: resolveDreamerFromConfig(cfg),
209321
+ dreamerEnabled: isDreamerRunnable(cfg)
209322
+ };
209323
+ }
209324
+ function resolveProjectDepsForDir(dir, identityOverride) {
209325
+ const cached2 = projectDepsByDir.get(dir);
209326
+ if (cached2)
209327
+ return cached2;
209328
+ ensureConfigLocationsMigrated(dir);
209329
+ const switchedLoad = loadPiConfig({ cwd: dir });
209330
+ logPiConfigLoad({
209331
+ dir,
209332
+ loadedFromPaths: switchedLoad.loadedFromPaths,
209333
+ warnings: switchedLoad.warnings,
209334
+ dedupe: true
209335
+ });
209336
+ const switchedConfig = switchedLoad.config;
209337
+ const switchedIdentity = identityOverride ?? resolveProjectIdentityOrFallback(dir);
209338
+ const built = buildProjectDeps(dir, switchedIdentity, switchedConfig);
209339
+ projectDepsByDir.set(dir, built);
207912
209340
  return built;
207913
209341
  }
207914
- const bootContextOptions = buildContextOptions(config2, historianConfig, autoSearchConfig);
207915
- contextOptionsByDir.set(projectDir, bootContextOptions);
207916
- registerPiContextHandler(pi, bootContextOptions);
207917
- info(historianConfig ? `registered historian trigger (model=${historianConfig.model}, executeThreshold=${formatExecuteThresholdForLog(historianConfig.executeThresholdPercentage)})` : "registered historian trigger: DISABLED (set historian.model in magic-context.jsonc)");
207918
- info(autoSearchConfig.enabled ? `registered auto-search hint (threshold=${autoSearchConfig.scoreThreshold}, minChars=${autoSearchConfig.minPromptChars})` : "registered auto-search hint: DISABLED (memory.auto_search.enabled=false)");
207919
- const sidekickConfig = resolveSidekickFromConfig(config2);
207920
- registerCtxAugCommand(pi, sidekickConfig);
207921
- info(sidekickConfig ? `registered /ctx-aug (sidekick model=${sidekickConfig.model})` : "registered /ctx-aug (sidekick disabled — set sidekick.disable=false and sidekick.model in config)");
209342
+ function resolveCurrentProjectDeps(ctx) {
209343
+ const currentProject = resolveCurrentProject(ctx);
209344
+ return resolveProjectDepsForDir(currentProject.projectDir, currentProject.projectIdentity);
209345
+ }
209346
+ function resolveContextOptionsForProject(dir) {
209347
+ return resolveProjectDepsForDir(dir).contextOptions;
209348
+ }
209349
+ const bootProjectDeps = buildProjectDeps(projectDir, projectIdentity, config2);
209350
+ projectDepsByDir.set(projectDir, bootProjectDeps);
209351
+ const todowriteEnabled = bootProjectDeps.config.todowrite.enabled !== false;
209352
+ const todowriteOverlayEnabled = todowriteEnabled && bootProjectDeps.config.todowrite.overlay !== false;
209353
+ registerMagicContextTools(pi, {
209354
+ db,
209355
+ ensureProjectRegistered: ensureProjectRegisteredFromPiDirectory,
209356
+ allowDreamerActions: false,
209357
+ memoryToolEnabled: true,
209358
+ protectedTags: config2.protected_tags ?? 20,
209359
+ resolveProtectedTags: (ctx) => resolveCurrentProjectDeps(ctx).config.protected_tags ?? 20,
209360
+ dreamerEnabled: isDreamerRunnable(config2),
209361
+ resolveDreamerEnabled: (ctx) => resolveCurrentProjectDeps(ctx).dreamerEnabled,
209362
+ todowriteEnabled
209363
+ });
209364
+ info(todowriteEnabled ? "registered tools: ctx_search, ctx_memory, ctx_note, ctx_expand, todowrite, ctx_reduce; registered /todos" : "registered tools: ctx_search, ctx_memory, ctx_note, ctx_expand, ctx_reduce (todowrite disabled)");
209365
+ const readLastTodoState = (sessionId) => getOrCreateSessionMeta(db, sessionId).lastTodoState;
209366
+ if (todowriteEnabled) {
209367
+ registerTodoStateLifecycle(pi, { readLastTodoState });
209368
+ }
209369
+ const todoOverlay = todowriteOverlayEnabled ? registerTodoOverlay(pi, {
209370
+ readLastTodoState
209371
+ }) : undefined;
209372
+ info(todowriteOverlayEnabled ? "registered todowrite overlay" : "registered todowrite overlay: DISABLED (todowrite.enabled=false or todowrite.overlay=false)");
209373
+ registerPiContextHandler(pi, bootProjectDeps.contextOptions);
209374
+ info(bootProjectDeps.historianConfig ? `registered historian trigger (model=${bootProjectDeps.historianConfig.model}, executeThreshold=${formatExecuteThresholdForLog(bootProjectDeps.historianConfig.executeThresholdPercentage)})` : "registered historian trigger: DISABLED (set historian.model in magic-context.jsonc)");
209375
+ info(bootProjectDeps.autoSearchConfig.enabled ? `registered auto-search hint (threshold=${bootProjectDeps.autoSearchConfig.scoreThreshold}, minChars=${bootProjectDeps.autoSearchConfig.minPromptChars})` : "registered auto-search hint: DISABLED (memory.auto_search.enabled=false)");
209376
+ registerCtxAugCommand(pi, (ctx) => resolveCurrentProjectDeps(ctx).sidekickConfig);
209377
+ info(bootProjectDeps.sidekickConfig ? `registered /ctx-aug (sidekick model=${bootProjectDeps.sidekickConfig.model})` : "registered /ctx-aug (sidekick disabled — set sidekick.disable=false and sidekick.model in config)");
209378
+ const recompRunner = new PiSubagentRunner;
209379
+ const wrapupRunner = new PiSubagentRunner;
209380
+ const upgradeRunner = new PiSubagentRunner;
207922
209381
  registerCtxStatusCommand(pi, {
207923
209382
  db,
207924
209383
  projectIdentity,
207925
209384
  resolveProject: resolveCurrentProject,
207926
- protectedTags: config2.protected_tags,
207927
- executeThresholdPercentage: config2.execute_threshold_percentage,
207928
- historyBudgetPercentage: config2.history_budget_percentage,
207929
- injectionBudgetTokens: config2.memory?.injection_budget_tokens,
207930
- commitClusterTrigger: config2.commit_cluster_trigger,
207931
- executeThresholdTokens: config2.execute_threshold_tokens,
209385
+ protectedTags: bootProjectDeps.config.protected_tags,
209386
+ executeThresholdPercentage: bootProjectDeps.config.execute_threshold_percentage,
209387
+ historyBudgetPercentage: bootProjectDeps.config.history_budget_percentage,
209388
+ injectionBudgetTokens: bootProjectDeps.config.memory?.injection_budget_tokens,
209389
+ commitClusterTrigger: bootProjectDeps.config.commit_cluster_trigger,
209390
+ executeThresholdTokens: bootProjectDeps.config.execute_threshold_tokens,
207932
209391
  dreamer: {
207933
- runnable: isDreamerRunnable(config2),
207934
- scheduleSummary: summarizeDreamSchedule(config2.dreamer)
209392
+ runnable: bootProjectDeps.dreamerEnabled,
209393
+ scheduleSummary: summarizeDreamSchedule(bootProjectDeps.config.dreamer)
209394
+ },
209395
+ resolveStatusDeps: (ctx) => {
209396
+ const current = resolveCurrentProjectDeps(ctx);
209397
+ return {
209398
+ db,
209399
+ projectIdentity: current.projectIdentity,
209400
+ resolveProject: resolveCurrentProject,
209401
+ protectedTags: current.config.protected_tags,
209402
+ executeThresholdPercentage: current.config.execute_threshold_percentage,
209403
+ historyBudgetPercentage: current.config.history_budget_percentage,
209404
+ injectionBudgetTokens: current.config.memory?.injection_budget_tokens,
209405
+ commitClusterTrigger: current.config.commit_cluster_trigger,
209406
+ executeThresholdTokens: current.config.execute_threshold_tokens,
209407
+ dreamer: {
209408
+ runnable: current.dreamerEnabled,
209409
+ scheduleSummary: summarizeDreamSchedule(current.config.dreamer)
209410
+ }
209411
+ };
207935
209412
  }
207936
209413
  });
207937
209414
  info("registered /ctx-status");
@@ -207941,45 +209418,94 @@ async function src_default2(pi) {
207941
209418
  info("registered /ctx-flush");
207942
209419
  registerCtxRecompCommand(pi, {
207943
209420
  db,
207944
- runner: new PiSubagentRunner,
207945
- historianModel: historianConfig?.model,
207946
- historianChunkTokens: deriveHistorianChunkTokens(resolveHistorianContextLimit(historianConfig?.model)),
207947
- historianFallbacks: historianConfig?.fallbackModels,
207948
- historianTimeoutMs: config2.historian_timeout_ms,
207949
- historianThinkingLevel: historianConfig?.thinkingLevel,
207950
- language: config2.language,
207951
- memoryEnabled: config2.memory.enabled,
207952
- autoPromote: config2.memory.auto_promote
209421
+ runner: recompRunner,
209422
+ historianModel: bootProjectDeps.historianConfig?.model,
209423
+ historianChunkTokens: deriveHistorianChunkTokens(resolveHistorianContextLimit(bootProjectDeps.historianConfig?.model)),
209424
+ historianFallbacks: bootProjectDeps.historianConfig?.fallbackModels,
209425
+ historianTimeoutMs: bootProjectDeps.config.historian_timeout_ms,
209426
+ historianThinkingLevel: bootProjectDeps.historianConfig?.thinkingLevel,
209427
+ language: bootProjectDeps.config.language,
209428
+ memoryEnabled: bootProjectDeps.config.memory.enabled,
209429
+ autoPromote: bootProjectDeps.config.memory.auto_promote,
209430
+ resolveRuntimeDeps: (ctx) => {
209431
+ const current = resolveCurrentProjectDeps(ctx);
209432
+ return {
209433
+ db,
209434
+ runner: recompRunner,
209435
+ historianModel: current.historianConfig?.model,
209436
+ historianChunkTokens: deriveHistorianChunkTokens(resolveHistorianContextLimit(current.historianConfig?.model)),
209437
+ historianFallbacks: current.historianConfig?.fallbackModels,
209438
+ historianTimeoutMs: current.config.historian_timeout_ms,
209439
+ historianThinkingLevel: current.historianConfig?.thinkingLevel,
209440
+ language: current.config.language,
209441
+ memoryEnabled: current.config.memory.enabled,
209442
+ autoPromote: current.config.memory.auto_promote
209443
+ };
209444
+ }
207953
209445
  });
207954
209446
  info("registered /ctx-recomp");
207955
209447
  registerCtxWrapupCommand(pi, {
207956
209448
  db,
207957
- runner: new PiSubagentRunner,
207958
- historianModel: historianConfig?.model,
207959
- historianChunkTokens: deriveHistorianChunkTokens(resolveHistorianContextLimit(historianConfig?.model)),
207960
- historianFallbacks: historianConfig?.fallbackModels,
207961
- historianTimeoutMs: config2.historian_timeout_ms,
207962
- historianThinkingLevel: historianConfig?.thinkingLevel,
207963
- language: config2.language,
207964
- memoryEnabled: config2.memory.enabled,
207965
- autoPromote: config2.memory.auto_promote,
207966
- userMemoriesEnabled: userMemoryCollectionEnabled(config2.dreamer),
207967
- executeThresholdPercentage: config2.execute_threshold_percentage,
207968
- executeThresholdTokens: config2.execute_threshold_tokens
209449
+ runner: wrapupRunner,
209450
+ historianModel: bootProjectDeps.historianConfig?.model,
209451
+ historianChunkTokens: deriveHistorianChunkTokens(resolveHistorianContextLimit(bootProjectDeps.historianConfig?.model)),
209452
+ historianFallbacks: bootProjectDeps.historianConfig?.fallbackModels,
209453
+ historianTimeoutMs: bootProjectDeps.config.historian_timeout_ms,
209454
+ historianThinkingLevel: bootProjectDeps.historianConfig?.thinkingLevel,
209455
+ language: bootProjectDeps.config.language,
209456
+ memoryEnabled: bootProjectDeps.config.memory.enabled,
209457
+ autoPromote: bootProjectDeps.config.memory.auto_promote,
209458
+ userMemoriesEnabled: userMemoryCollectionEnabled(bootProjectDeps.config.dreamer),
209459
+ executeThresholdPercentage: bootProjectDeps.config.execute_threshold_percentage,
209460
+ executeThresholdTokens: bootProjectDeps.config.execute_threshold_tokens,
209461
+ resolveRuntimeDeps: (ctx) => {
209462
+ const current = resolveCurrentProjectDeps(ctx);
209463
+ return {
209464
+ db,
209465
+ runner: wrapupRunner,
209466
+ historianModel: current.historianConfig?.model,
209467
+ historianChunkTokens: deriveHistorianChunkTokens(resolveHistorianContextLimit(current.historianConfig?.model)),
209468
+ historianFallbacks: current.historianConfig?.fallbackModels,
209469
+ historianTimeoutMs: current.config.historian_timeout_ms,
209470
+ historianThinkingLevel: current.historianConfig?.thinkingLevel,
209471
+ language: current.config.language,
209472
+ memoryEnabled: current.config.memory.enabled,
209473
+ autoPromote: current.config.memory.auto_promote,
209474
+ userMemoriesEnabled: userMemoryCollectionEnabled(current.config.dreamer),
209475
+ executeThresholdPercentage: current.config.execute_threshold_percentage,
209476
+ executeThresholdTokens: current.config.execute_threshold_tokens
209477
+ };
209478
+ }
207969
209479
  });
207970
209480
  info("registered /ctx-wrapup");
207971
209481
  registerCtxSessionUpgradeCommand(pi, {
207972
209482
  db,
207973
- runner: new PiSubagentRunner,
207974
- historianModel: historianConfig?.model,
207975
- historianChunkTokens: deriveHistorianChunkTokens(resolveHistorianContextLimit(historianConfig?.model)),
207976
- historianFallbacks: historianConfig?.fallbackModels,
207977
- historianTimeoutMs: config2.historian_timeout_ms,
207978
- historianThinkingLevel: historianConfig?.thinkingLevel,
207979
- language: config2.language,
207980
- memoryEnabled: config2.memory.enabled,
207981
- autoPromote: config2.memory.auto_promote,
207982
- userMemoriesEnabled: userMemoryCollectionEnabled(config2.dreamer)
209483
+ runner: upgradeRunner,
209484
+ historianModel: bootProjectDeps.historianConfig?.model,
209485
+ historianChunkTokens: deriveHistorianChunkTokens(resolveHistorianContextLimit(bootProjectDeps.historianConfig?.model)),
209486
+ historianFallbacks: bootProjectDeps.historianConfig?.fallbackModels,
209487
+ historianTimeoutMs: bootProjectDeps.config.historian_timeout_ms,
209488
+ historianThinkingLevel: bootProjectDeps.historianConfig?.thinkingLevel,
209489
+ language: bootProjectDeps.config.language,
209490
+ memoryEnabled: bootProjectDeps.config.memory.enabled,
209491
+ autoPromote: bootProjectDeps.config.memory.auto_promote,
209492
+ userMemoriesEnabled: userMemoryCollectionEnabled(bootProjectDeps.config.dreamer),
209493
+ resolveRuntimeDeps: (ctx) => {
209494
+ const current = resolveCurrentProjectDeps(ctx);
209495
+ return {
209496
+ db,
209497
+ runner: upgradeRunner,
209498
+ historianModel: current.historianConfig?.model,
209499
+ historianChunkTokens: deriveHistorianChunkTokens(resolveHistorianContextLimit(current.historianConfig?.model)),
209500
+ historianFallbacks: current.historianConfig?.fallbackModels,
209501
+ historianTimeoutMs: current.config.historian_timeout_ms,
209502
+ historianThinkingLevel: current.historianConfig?.thinkingLevel,
209503
+ language: current.config.language,
209504
+ memoryEnabled: current.config.memory.enabled,
209505
+ autoPromote: current.config.memory.auto_promote,
209506
+ userMemoriesEnabled: userMemoryCollectionEnabled(current.config.dreamer)
209507
+ };
209508
+ }
207983
209509
  });
207984
209510
  info("registered /ctx-session-upgrade");
207985
209511
  registerCtxDreamCommand(pi, {
@@ -207987,7 +209513,8 @@ async function src_default2(pi) {
207987
209513
  projectDir,
207988
209514
  projectIdentity,
207989
209515
  resolveProject: resolveCurrentProject,
207990
- dreamerEnabled: isDreamerRunnable(config2),
209516
+ dreamerEnabled: bootProjectDeps.dreamerEnabled,
209517
+ resolveDreamerEnabled: (ctx) => resolveCurrentProjectDeps(ctx).dreamerEnabled,
207991
209518
  onProjectSeen: (identity) => seenDreamerProjectIdentities.add(identity)
207992
209519
  });
207993
209520
  info("registered /ctx-dream");
@@ -207995,26 +209522,27 @@ async function src_default2(pi) {
207995
209522
  db,
207996
209523
  projectDir,
207997
209524
  projectIdentity,
207998
- memoryEnabled: config2.memory.enabled,
209525
+ memoryEnabled: bootProjectDeps.config.memory.enabled,
209526
+ resolveMemoryEnabled: (ctx) => resolveCurrentProjectDeps(ctx).config.memory.enabled,
207999
209527
  resolveProject: resolveCurrentProject
208000
209528
  });
208001
209529
  info("registered /ctx-embed");
208002
- const dreamerConfig = resolveDreamerFromConfig(config2);
209530
+ const dreamerConfig = bootProjectDeps.dreamerConfig;
208003
209531
  if (dreamerConfig) {
208004
209532
  registerPiDreamerProject({
208005
209533
  db,
208006
209534
  projectDir,
208007
209535
  projectIdentity,
208008
209536
  config: dreamerConfig,
208009
- embeddingConfig: config2.embedding,
208010
- memoryEnabled: config2.memory.enabled,
208011
- language: config2.language,
208012
- gitCommitIndexing: config2.memory.git_commit_indexing,
209537
+ embeddingConfig: bootProjectDeps.config.embedding,
209538
+ memoryEnabled: bootProjectDeps.config.memory.enabled,
209539
+ language: bootProjectDeps.config.language,
209540
+ gitCommitIndexing: bootProjectDeps.config.memory.git_commit_indexing,
208013
209541
  onAdjunctsRefreshNeeded: signalPiSystemPromptRefreshForProject
208014
209542
  });
208015
209543
  info(`registered dreamer (${summarizeDreamSchedule(dreamerConfig)})`);
208016
209544
  } else {
208017
- info(isDreamerRunnable(config2) ? "registered dreamer: DISABLED (no dreamer config)" : "registered dreamer: DISABLED (dreamer.disable=true or no dreamer config)");
209545
+ info(bootProjectDeps.dreamerEnabled ? "registered dreamer: DISABLED (no dreamer config)" : "registered dreamer: DISABLED (dreamer.disable=true or no dreamer config)");
208018
209546
  }
208019
209547
  pi.on("before_agent_start", async (event, ctx) => {
208020
209548
  try {
@@ -208035,13 +209563,14 @@ async function src_default2(pi) {
208035
209563
  }
208036
209564
  } catch {}
208037
209565
  try {
208038
- const currentProject = resolveCurrentProject(ctx);
209566
+ const effectiveProjectDeps = resolveCurrentProjectDeps(ctx);
209567
+ const currentProject = {
209568
+ projectDir: effectiveProjectDeps.projectDir,
209569
+ projectIdentity: effectiveProjectDeps.projectIdentity
209570
+ };
209571
+ const effectiveConfig = effectiveProjectDeps.config;
208039
209572
  seenDreamerProjectIdentities.add(currentProject.projectIdentity);
208040
- const switchedProject = currentProject.projectDir !== projectDir;
208041
- if (switchedProject)
208042
- ensureConfigLocationsMigrated(currentProject.projectDir);
208043
- const effectiveConfig = switchedProject ? loadPiConfig({ cwd: currentProject.projectDir }).config : config2;
208044
- const effectiveDreamerConfig = switchedProject ? resolveDreamerFromConfig(effectiveConfig) : dreamerConfig;
209573
+ const effectiveDreamerConfig = effectiveProjectDeps.dreamerConfig;
208045
209574
  if (effectiveDreamerConfig) {
208046
209575
  try {
208047
209576
  registerPiDreamerProject({
@@ -208051,13 +209580,14 @@ async function src_default2(pi) {
208051
209580
  config: effectiveDreamerConfig,
208052
209581
  embeddingConfig: effectiveConfig.embedding,
208053
209582
  memoryEnabled: effectiveConfig.memory.enabled,
209583
+ language: effectiveConfig.language,
208054
209584
  gitCommitIndexing: effectiveConfig.memory.git_commit_indexing,
208055
209585
  onAdjunctsRefreshNeeded: signalPiSystemPromptRefreshForProject
208056
209586
  });
208057
209587
  } catch (err5) {
208058
209588
  warn("before_agent_start: registerPiDreamerProject threw:", err5);
208059
209589
  }
208060
- } else if (switchedProject) {
209590
+ } else {
208061
209591
  try {
208062
209592
  unregisterPiDreamerProject({
208063
209593
  projectIdentity: currentProject.projectIdentity
@@ -208084,11 +209614,10 @@ async function src_default2(pi) {
208084
209614
  const smForDrain = sm;
208085
209615
  const canDrain = typeof smForDrain.appendCompaction === "function" && typeof smForDrain.getBranch === "function";
208086
209616
  if (canDrain && getPendingPiCompactionMarkerState(db, sessionId)) {
208087
- signalPiDeferredHistoryRefresh(sessionId);
208088
- signalPiPendingMaterialization(sessionId);
209617
+ signalPiDeferredCompactionMarkerDrain(sessionId);
208089
209618
  }
208090
209619
  } catch {}
208091
- if (ctx.hasUI && historianConfig?.model) {
209620
+ if (ctx.hasUI && effectiveProjectDeps.historianConfig?.model) {
208092
209621
  maybeSendUpgradeReminder({
208093
209622
  client: null,
208094
209623
  db,
@@ -208109,7 +209638,6 @@ async function src_default2(pi) {
208109
209638
  return;
208110
209639
  }
208111
209640
  const isCacheBusting = sessionId ? hasSystemPromptRefresh(sessionId) : true;
208112
- const effectiveDreamerRunnable = switchedProject ? isDreamerRunnable(effectiveConfig) : isDreamerRunnable(config2);
208113
209641
  const block = buildMagicContextBlock({
208114
209642
  db,
208115
209643
  cwd: currentProject.projectDir,
@@ -208118,7 +209646,7 @@ async function src_default2(pi) {
208118
209646
  includeGuidance: true,
208119
209647
  protectedTags: effectiveConfig.protected_tags,
208120
209648
  ctxReduceCallable: true,
208121
- dreamerEnabled: effectiveDreamerRunnable,
209649
+ dreamerEnabled: effectiveProjectDeps.dreamerEnabled,
208122
209650
  temporalAwarenessEnabled: effectiveConfig.temporal_awareness ?? false,
208123
209651
  cavemanTextCompressionEnabled: effectiveConfig.caveman_text_compression?.enabled === true,
208124
209652
  language: effectiveConfig.language,
@@ -208176,14 +209704,14 @@ ${block}` : event.systemPrompt;
208176
209704
  const todoArgs = event.args;
208177
209705
  const todos = todoArgs?.todos;
208178
209706
  const sessionMeta = Array.isArray(todos) ? getOrCreateSessionMeta(db, sessionId) : null;
208179
- if (sessionMeta && !sessionMeta.isSubagent) {
208180
- const normalizedTodos = normalizeTodoStateJson(todos);
208181
- if (normalizedTodos !== null) {
208182
- updateSessionMeta(db, sessionId, {
208183
- lastTodoState: normalizedTodos
208184
- });
208185
- }
208186
- }
209707
+ capturePiTodowriteArgsIfCompatible({
209708
+ db,
209709
+ sessionId,
209710
+ todos,
209711
+ todowriteEnabled,
209712
+ todoOverlay,
209713
+ persist: Boolean(sessionMeta && !sessionMeta.isSubagent)
209714
+ });
208187
209715
  if (Array.isArray(todos) && todos.length > 0 && todos.every((t) => t.status === "completed" || t.status === "cancelled")) {
208188
209716
  if (sessionMeta && !sessionMeta.isSubagent) {
208189
209717
  onNoteTrigger(db, sessionId, "todos_complete");
@@ -208264,7 +209792,7 @@ ${block}` : event.systemPrompt;
208264
209792
  db,
208265
209793
  sessionId,
208266
209794
  message: event.message,
208267
- cacheTtlConfig: config2.cache_ttl
209795
+ cacheTtlConfig: resolveCurrentProjectDeps(ctx).config.cache_ttl
208268
209796
  });
208269
209797
  const piUsage = ctx.getContextUsage?.();
208270
209798
  const piContextWindow = piUsage && typeof piUsage.contextWindow === "number" && piUsage.contextWindow > 0 ? piUsage.contextWindow : ctx.model?.contextWindow ?? 0;
@@ -208286,32 +209814,14 @@ ${block}` : event.systemPrompt;
208286
209814
  try {
208287
209815
  const sessionMetaForTodo = getOrCreateSessionMeta(db, sessionId);
208288
209816
  if (!sessionMetaForTodo.isSubagent) {
208289
- const msg = event.message;
208290
- if (msg && msg.role === "assistant" && Array.isArray(msg.content)) {
208291
- for (const block of msg.content) {
208292
- if (!block || typeof block !== "object")
208293
- continue;
208294
- const b = block;
208295
- if (b.type !== "toolCall")
208296
- continue;
208297
- if (typeof b.name !== "string")
208298
- continue;
208299
- if (b.name !== "todowrite") {
208300
- continue;
208301
- }
208302
- const args = b.arguments;
208303
- const todos = args?.todos;
208304
- if (!Array.isArray(todos))
208305
- continue;
208306
- const normalized = normalizeTodoStateJson(todos);
208307
- if (normalized === null)
208308
- continue;
208309
- updateSessionMeta(db, sessionId, {
208310
- lastTodoState: normalized
208311
- });
208312
- break;
208313
- }
208314
- }
209817
+ capturePiTodowriteMessageIfCompatible({
209818
+ db,
209819
+ sessionId,
209820
+ message: event.message,
209821
+ todowriteEnabled,
209822
+ todoOverlay,
209823
+ persist: true
209824
+ });
208315
209825
  }
208316
209826
  } catch (err5) {
208317
209827
  warn("message_end: synthetic todowrite capture failed:", err5);
@@ -208397,10 +209907,14 @@ function formatExecuteThresholdForLog(value) {
208397
209907
  return overrides.length > 0 ? `${base} (${overrides.join(", ")})` : base;
208398
209908
  }
208399
209909
  export {
209910
+ signalPiDeferredCompactionMarkerDrain,
208400
209911
  resolveSidekickFromConfig,
208401
209912
  resolveHistorianFromConfig,
208402
209913
  resolveDreamerFromConfig,
208403
209914
  persistPiPressureFromMessageEnd,
208404
209915
  persistPiMessageEndModelMeta,
208405
- src_default2 as default
209916
+ src_default2 as default,
209917
+ capturePiTodowriteMessageIfCompatible,
209918
+ capturePiTodowriteArgsIfCompatible,
209919
+ __test
208406
209920
  };