@cortexkit/pi-magic-context 0.32.0 → 0.32.1

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
@@ -14951,7 +14951,9 @@ __export(exports_rpc_notifications, {
14951
14951
  registerNotificationSink: () => registerNotificationSink,
14952
14952
  pushNotification: () => pushNotification,
14953
14953
  isTuiConnected: () => isTuiConnected,
14954
- drainNotifications: () => drainNotifications
14954
+ drainNotifications: () => drainNotifications,
14955
+ acknowledgeNotifications: () => acknowledgeNotifications,
14956
+ __resetNotificationStateForTests: () => __resetNotificationStateForTests
14955
14957
  });
14956
14958
  function registerNotificationSink(sink) {
14957
14959
  sinks.add(sink);
@@ -14960,7 +14962,11 @@ function registerNotificationSink(sink) {
14960
14962
  };
14961
14963
  }
14962
14964
  function notificationMatchesSink(notification, sink) {
14963
- return notification.sessionId === undefined || sink.sessionId === undefined || notification.sessionId === sink.sessionId;
14965
+ if (notification.sessionId === undefined)
14966
+ return true;
14967
+ if (sink.sessionId !== undefined)
14968
+ return notification.sessionId === sink.sessionId;
14969
+ return sink.protocol !== 2;
14964
14970
  }
14965
14971
  function pushNotification(type, payload, sessionId) {
14966
14972
  const notification = { id: nextNotificationId++, type, payload, sessionId };
@@ -14973,23 +14979,31 @@ function pushNotification(type, payload, sessionId) {
14973
14979
  } catch {}
14974
14980
  }
14975
14981
  if (queue.length > 100) {
14976
- const newestPerSession = new Map;
14977
- for (const n of queue) {
14978
- const prev = newestPerSession.get(n.sessionId);
14979
- if (prev === undefined || n.id > prev) {
14980
- newestPerSession.set(n.sessionId, n.id);
14981
- }
14982
- }
14983
- const mustKeep = new Set(newestPerSession.values());
14984
- const byNewest = [...queue].sort((a, b) => b.id - a.id);
14985
- const kept = [];
14986
- for (const n of byNewest) {
14987
- if (kept.length < 50 || mustKeep.has(n.id))
14988
- kept.push(n);
14982
+ const reservedIds = new Set;
14983
+ const reservedScopes = new Set;
14984
+ for (let i = queue.length - 1;i >= 0 && reservedScopes.size < 25; i -= 1) {
14985
+ const candidate = queue[i];
14986
+ const scope = candidate.sessionId ?? "\x00global";
14987
+ if (reservedScopes.has(scope))
14988
+ continue;
14989
+ reservedScopes.add(scope);
14990
+ reservedIds.add(candidate.id);
14989
14991
  }
14990
- queue = kept.sort((a, b) => a.id - b.id);
14992
+ const evictionIndex = queue.findIndex((candidate) => !reservedIds.has(candidate.id));
14993
+ queue.splice(evictionIndex >= 0 ? evictionIndex : 0, 1);
14991
14994
  }
14992
14995
  }
14996
+ function acknowledgeNotifications(ids) {
14997
+ const acknowledged = new Set(ids.filter((id) => Number.isSafeInteger(id) && id > 0));
14998
+ if (acknowledged.size === 0)
14999
+ return;
15000
+ queue = queue.filter((notification) => !acknowledged.has(notification.id));
15001
+ }
15002
+ function __resetNotificationStateForTests() {
15003
+ queue = [];
15004
+ nextNotificationId = 1;
15005
+ sinks.clear();
15006
+ }
14993
15007
  function cursor(value) {
14994
15008
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
14995
15009
  }
@@ -15032,7 +15046,9 @@ function isTuiConnected(sessionId) {
15032
15046
  if (sessionId === undefined)
15033
15047
  return true;
15034
15048
  for (const sink of sinks) {
15035
- if (sink.sessionId === undefined || sink.sessionId === sessionId)
15049
+ if (sink.sessionId === sessionId)
15050
+ return true;
15051
+ if (sink.sessionId === undefined && sink.protocol !== 2)
15036
15052
  return true;
15037
15053
  }
15038
15054
  return false;
@@ -16088,11 +16104,11 @@ function buildNodeSqliteDatabaseClass(DatabaseSync) {
16088
16104
  }
16089
16105
  transaction(fn) {
16090
16106
  const self2 = this;
16091
- const wrapped = function(...args) {
16107
+ const execute = (mode, receiver, args) => {
16092
16108
  const nested = self2.isTransaction === true;
16093
- self2.exec(nested ? `SAVEPOINT ${SAVEPOINT}` : "BEGIN");
16109
+ self2.exec(nested ? `SAVEPOINT ${SAVEPOINT}` : `BEGIN${mode ? ` ${mode}` : ""}`);
16094
16110
  try {
16095
- const result = fn.apply(this, args);
16111
+ const result = fn.apply(receiver, args);
16096
16112
  self2.exec(nested ? `RELEASE ${SAVEPOINT}` : "COMMIT");
16097
16113
  return result;
16098
16114
  } catch (error) {
@@ -16105,6 +16121,21 @@ function buildNodeSqliteDatabaseClass(DatabaseSync) {
16105
16121
  throw error;
16106
16122
  }
16107
16123
  };
16124
+ const wrapped = function(...args) {
16125
+ return execute("", this, args);
16126
+ };
16127
+ wrapped.default = function(...args) {
16128
+ return execute("", this, args);
16129
+ };
16130
+ wrapped.deferred = function(...args) {
16131
+ return execute("DEFERRED", this, args);
16132
+ };
16133
+ wrapped.immediate = function(...args) {
16134
+ return execute("IMMEDIATE", this, args);
16135
+ };
16136
+ wrapped.exclusive = function(...args) {
16137
+ return execute("EXCLUSIVE", this, args);
16138
+ };
16108
16139
  return wrapped;
16109
16140
  }
16110
16141
  }
@@ -148826,6 +148857,66 @@ function readRawSessionMessagesFromDb(db, sessionId) {
148826
148857
  };
148827
148858
  });
148828
148859
  }
148860
+ function readRawSessionMessagePageFromDb(db, sessionId, afterOrdinal, limit, finalWatermark = Number.MAX_SAFE_INTEGER) {
148861
+ const remaining = Math.max(0, Math.floor(finalWatermark) - Math.floor(afterOrdinal));
148862
+ const pageSize = Math.min(Math.max(1, Math.floor(limit)), remaining);
148863
+ if (pageSize === 0)
148864
+ return [];
148865
+ const messageRows = db.prepare(`SELECT id, data, time_created, time_updated
148866
+ FROM message
148867
+ WHERE session_id = ?
148868
+ AND NOT (
148869
+ CASE WHEN json_valid(data) = 1
148870
+ THEN COALESCE(json_extract(data, '$.summary'), 0)
148871
+ ELSE 0 END = 1
148872
+ AND CASE WHEN json_valid(data) = 1
148873
+ THEN COALESCE(json_extract(data, '$.finish'), '')
148874
+ ELSE '' END = 'stop'
148875
+ )
148876
+ ORDER BY time_created ASC, id ASC
148877
+ LIMIT ? OFFSET ?`).all(sessionId, pageSize, Math.max(0, Math.floor(afterOrdinal))).filter(isRawMessageRow).map((row, index) => ({
148878
+ ...row,
148879
+ ordinal: Math.floor(afterOrdinal) + index + 1
148880
+ }));
148881
+ if (messageRows.length === 0)
148882
+ return [];
148883
+ const placeholders = messageRows.map(() => "?").join(", ");
148884
+ const partRows = db.prepare(`SELECT message_id, data, time_updated
148885
+ FROM part
148886
+ WHERE session_id = ? AND message_id IN (${placeholders})
148887
+ ORDER BY time_created ASC, id ASC`).all(sessionId, ...messageRows.map((row) => row.id)).filter(isRawPartRow);
148888
+ const partsByMessageId = new Map;
148889
+ for (const part of partRows) {
148890
+ const list = partsByMessageId.get(part.message_id) ?? [];
148891
+ list.push(attachRawPartVersion(parseJsonUnknown(part.data), part.time_updated));
148892
+ partsByMessageId.set(part.message_id, list);
148893
+ }
148894
+ return messageRows.map((row) => {
148895
+ const info = parseJsonRecord(row.data);
148896
+ return {
148897
+ ordinal: row.ordinal,
148898
+ id: row.id,
148899
+ role: typeof info?.role === "string" ? info.role : "unknown",
148900
+ parts: partsByMessageId.get(row.id) ?? [],
148901
+ createdAt: row.time_created ?? null,
148902
+ version: row.time_updated ?? null
148903
+ };
148904
+ });
148905
+ }
148906
+ function countRawSessionMessageOrdinalsFromDb(db, sessionId) {
148907
+ const row = db.prepare(`SELECT COUNT(*) AS count
148908
+ FROM message
148909
+ WHERE session_id = ?
148910
+ AND NOT (
148911
+ CASE WHEN json_valid(data) = 1
148912
+ THEN COALESCE(json_extract(data, '$.summary'), 0)
148913
+ ELSE 0 END = 1
148914
+ AND CASE WHEN json_valid(data) = 1
148915
+ THEN COALESCE(json_extract(data, '$.finish'), '')
148916
+ ELSE '' END = 'stop'
148917
+ )`).get(sessionId);
148918
+ return typeof row?.count === "number" ? row.count : 0;
148919
+ }
148829
148920
  function isAnchorRow(row) {
148830
148921
  return row !== null && typeof row === "object" && typeof row.time_created === "number" && typeof row.id === "string";
148831
148922
  }
@@ -149133,6 +149224,31 @@ function readRawSessionMessages(sessionId) {
149133
149224
  }
149134
149225
  return readRawSessionMessagesFromSource(sessionId);
149135
149226
  }
149227
+ function readRawSessionMessagePage(sessionId, afterOrdinal, limit, finalWatermark) {
149228
+ const provider = sessionProviders.get(sessionId);
149229
+ if (provider?.readMessagePage) {
149230
+ return provider.readMessagePage(afterOrdinal, limit, finalWatermark);
149231
+ }
149232
+ if (provider) {
149233
+ return provider.readMessages().filter((message) => message.ordinal > afterOrdinal && message.ordinal <= finalWatermark).slice(0, limit);
149234
+ }
149235
+ if (!openCodeDbExists())
149236
+ return [];
149237
+ return withReadOnlySessionDb((db) => readRawSessionMessagePageFromDb(db, sessionId, afterOrdinal, limit, finalWatermark));
149238
+ }
149239
+ function getRawSessionMessageOrdinalCount(sessionId) {
149240
+ const provider = sessionProviders.get(sessionId);
149241
+ if (provider) {
149242
+ if (provider.getMessageCount)
149243
+ return provider.getMessageCount();
149244
+ return provider.readMessages().length;
149245
+ }
149246
+ if (!openCodeDbExists())
149247
+ return 0;
149248
+ return withReadOnlySessionDb((db) => countRawSessionMessageOrdinalsFromDb(db, sessionId));
149249
+ }
149250
+ readRawSessionMessages.readPage = readRawSessionMessagePage;
149251
+ readRawSessionMessages.getCount = getRawSessionMessageOrdinalCount;
149136
149252
  function primeTailRawMessageCache(args) {
149137
149253
  const { sessionId, lastCompartmentEnd, anchorMessageId } = args;
149138
149254
  if (!activeRawMessageCache)
@@ -149413,11 +149529,10 @@ function clearCompressionDepthRange(db, sessionId, startOrdinal, endOrdinal) {
149413
149529
  // ../plugin/src/features/magic-context/message-index.ts
149414
149530
  var lastIndexedStatements = new WeakMap;
149415
149531
  var insertMessageStatements = new WeakMap;
149416
- var upsertIndexStatements = new WeakMap;
149417
- var upsertCleanIndexStatements = new WeakMap;
149532
+ var upsertProgressStatements = new WeakMap;
149418
149533
  var upsertDirtyFloorStatements = new WeakMap;
149419
149534
  var deleteFtsStatements = new WeakMap;
149420
- var deleteFtsFromOrdinalStatements = new WeakMap;
149535
+ var deleteFtsRangeStatements = new WeakMap;
149421
149536
  var deleteIndexStatements = new WeakMap;
149422
149537
  var countIndexedMessageStatements = new WeakMap;
149423
149538
  function normalizeIndexText(text) {
@@ -149439,19 +149554,11 @@ function getInsertMessageStatement(db) {
149439
149554
  }
149440
149555
  return stmt;
149441
149556
  }
149442
- function getUpsertIndexStatement(db) {
149443
- let stmt = upsertIndexStatements.get(db);
149444
- if (!stmt) {
149445
- stmt = db.prepare("INSERT INTO message_history_index (session_id, last_indexed_ordinal, updated_at, harness) VALUES (?, ?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET last_indexed_ordinal = excluded.last_indexed_ordinal, updated_at = excluded.updated_at");
149446
- upsertIndexStatements.set(db, stmt);
149447
- }
149448
- return stmt;
149449
- }
149450
- function getUpsertCleanIndexStatement(db) {
149451
- let stmt = upsertCleanIndexStatements.get(db);
149557
+ function getUpsertProgressStatement(db) {
149558
+ let stmt = upsertProgressStatements.get(db);
149452
149559
  if (!stmt) {
149453
- 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");
149454
- upsertCleanIndexStatements.set(db, stmt);
149560
+ 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 = excluded.last_indexed_ordinal, dirty_floor_ordinal = excluded.dirty_floor_ordinal, updated_at = excluded.updated_at");
149561
+ upsertProgressStatements.set(db, stmt);
149455
149562
  }
149456
149563
  return stmt;
149457
149564
  }
@@ -149463,27 +149570,11 @@ function getUpsertDirtyFloorStatement(db) {
149463
149570
  }
149464
149571
  return stmt;
149465
149572
  }
149466
- function getDeleteFtsStatement(db) {
149467
- let stmt = deleteFtsStatements.get(db);
149468
- if (!stmt) {
149469
- stmt = db.prepare("DELETE FROM message_history_fts WHERE session_id = ?");
149470
- deleteFtsStatements.set(db, stmt);
149471
- }
149472
- return stmt;
149473
- }
149474
- function getDeleteFtsFromOrdinalStatement(db) {
149475
- let stmt = deleteFtsFromOrdinalStatements.get(db);
149476
- if (!stmt) {
149477
- stmt = db.prepare("DELETE FROM message_history_fts WHERE session_id = ? AND CAST(message_ordinal AS INTEGER) >= ?");
149478
- deleteFtsFromOrdinalStatements.set(db, stmt);
149479
- }
149480
- return stmt;
149481
- }
149482
- function getDeleteIndexStatement(db) {
149483
- let stmt = deleteIndexStatements.get(db);
149573
+ function getDeleteFtsRangeStatement(db) {
149574
+ let stmt = deleteFtsRangeStatements.get(db);
149484
149575
  if (!stmt) {
149485
- stmt = db.prepare("DELETE FROM message_history_index WHERE session_id = ?");
149486
- deleteIndexStatements.set(db, stmt);
149576
+ stmt = db.prepare("DELETE FROM message_history_fts WHERE session_id = ? AND CAST(message_ordinal AS INTEGER) BETWEEN ? AND ?");
149577
+ deleteFtsRangeStatements.set(db, stmt);
149487
149578
  }
149488
149579
  return stmt;
149489
149580
  }
@@ -149511,16 +149602,17 @@ function isMessageAlreadyIndexed(db, sessionId, messageId) {
149511
149602
  const row = getCountIndexedMessageStatement(db).get(sessionId, messageId);
149512
149603
  return (typeof row?.count === "number" ? row.count : 0) > 0;
149513
149604
  }
149514
- function advanceIndexWatermark(db, sessionId, ordinal, now) {
149515
- const current = getLastIndexedOrdinal(db, sessionId);
149516
- getUpsertIndexStatement(db).run(sessionId, Math.max(current, ordinal), now, getHarness());
149605
+ function setIndexProgress(db, sessionId, watermark, dirtyFloor, now) {
149606
+ getUpsertProgressStatement(db).run(sessionId, Math.max(0, Math.floor(watermark)), dirtyFloor ?? 0, now, getHarness());
149517
149607
  }
149518
- function clearIndexedMessages(db, sessionId) {
149519
- db.transaction(() => {
149520
- getDeleteFtsStatement(db).run(sessionId);
149521
- getDeleteIndexStatement(db).run(sessionId);
149522
- clearCompressionDepth(db, sessionId);
149523
- })();
149608
+ function getMessageIndexReconciliationStartOrdinal(db, sessionId) {
149609
+ const watermark = getLastIndexedOrdinal(db, sessionId);
149610
+ const dirtyFloor = getDirtyIndexFloor(db, sessionId);
149611
+ return dirtyFloor === null ? watermark : Math.min(watermark, dirtyFloor - 1);
149612
+ }
149613
+ function isMessageIndexReconciledThrough(db, sessionId, finalWatermark) {
149614
+ const dirtyFloor = getDirtyIndexFloor(db, sessionId);
149615
+ return getLastIndexedOrdinal(db, sessionId) >= finalWatermark && dirtyFloor === null;
149524
149616
  }
149525
149617
  function getIndexableContent(role, parts) {
149526
149618
  if (role === "user") {
@@ -149535,22 +149627,21 @@ function getIndexableContent(role, parts) {
149535
149627
  return "";
149536
149628
  }
149537
149629
  function indexSingleMessageInTransaction(db, sessionId, message, now) {
149538
- if (message.role !== "user" && message.role !== "assistant") {
149539
- advanceIndexWatermark(db, sessionId, message.ordinal, now);
149540
- return false;
149541
- }
149542
- const content = getIndexableContent(message.role, message.parts);
149543
- if (content.length === 0) {
149544
- advanceIndexWatermark(db, sessionId, message.ordinal, now);
149630
+ const currentWatermark = getLastIndexedOrdinal(db, sessionId);
149631
+ const dirtyFloor = getDirtyIndexFloor(db, sessionId);
149632
+ if (message.ordinal !== currentWatermark + 1 || dirtyFloor !== null && dirtyFloor !== message.ordinal) {
149545
149633
  return false;
149546
149634
  }
149547
- if (isMessageAlreadyIndexed(db, sessionId, message.id)) {
149548
- advanceIndexWatermark(db, sessionId, message.ordinal, now);
149549
- return false;
149635
+ let inserted = false;
149636
+ if (message.role === "user" || message.role === "assistant") {
149637
+ const content = getIndexableContent(message.role, message.parts);
149638
+ if (content.length > 0 && !isMessageAlreadyIndexed(db, sessionId, message.id)) {
149639
+ getInsertMessageStatement(db).run(sessionId, message.ordinal, message.id, message.role, content);
149640
+ inserted = true;
149641
+ }
149550
149642
  }
149551
- getInsertMessageStatement(db).run(sessionId, message.ordinal, message.id, message.role, content);
149552
- advanceIndexWatermark(db, sessionId, message.ordinal, now);
149553
- return true;
149643
+ setIndexProgress(db, sessionId, message.ordinal, null, now);
149644
+ return inserted;
149554
149645
  }
149555
149646
  function indexSingleMessage(db, sessionId, message) {
149556
149647
  db.exec("BEGIN IMMEDIATE");
@@ -149568,38 +149659,44 @@ function indexSingleMessage(db, sessionId, message) {
149568
149659
  }
149569
149660
  }
149570
149661
  }
149571
- function indexMessagesAfterOrdinal(db, sessionId, messages, lastIndexedOrdinal, finalWatermark = messages.length) {
149662
+ function indexMessagesAfterOrdinal(db, sessionId, messages, _lastIndexedOrdinal, finalWatermark = messages.length) {
149572
149663
  const now = Date.now();
149573
149664
  let inserted = 0;
149574
149665
  db.exec("BEGIN IMMEDIATE");
149575
149666
  let committed = false;
149576
149667
  try {
149577
- let effectiveWatermark = Math.max(lastIndexedOrdinal, getLastIndexedOrdinal(db, sessionId));
149668
+ const currentWatermark = getLastIndexedOrdinal(db, sessionId);
149578
149669
  const dirtyFloor = getDirtyIndexFloor(db, sessionId);
149579
- if (dirtyFloor !== null) {
149580
- const rewindOrdinal = Math.max(1, Math.min(dirtyFloor, finalWatermark + 1));
149581
- if (rewindOrdinal <= finalWatermark) {
149582
- getDeleteFtsFromOrdinalStatement(db).run(sessionId, rewindOrdinal);
149583
- }
149584
- effectiveWatermark = Math.min(effectiveWatermark, rewindOrdinal - 1);
149670
+ const effectiveWatermark = dirtyFloor === null ? currentWatermark : Math.min(currentWatermark, Math.max(0, dirtyFloor - 1));
149671
+ if (dirtyFloor !== null && dirtyFloor <= finalWatermark) {
149672
+ getDeleteFtsRangeStatement(db).run(sessionId, dirtyFloor, finalWatermark);
149585
149673
  }
149586
- const insertMessage = getInsertMessageStatement(db);
149674
+ const messagesByOrdinal = new Map;
149587
149675
  for (const message of messages) {
149588
- if (message.ordinal <= effectiveWatermark) {
149589
- continue;
149676
+ if (message.ordinal > effectiveWatermark && message.ordinal <= finalWatermark) {
149677
+ messagesByOrdinal.set(message.ordinal, message);
149590
149678
  }
149591
- if (message.role !== "user" && message.role !== "assistant") {
149679
+ }
149680
+ let coveredWatermark = effectiveWatermark;
149681
+ while (coveredWatermark < finalWatermark && messagesByOrdinal.has(coveredWatermark + 1)) {
149682
+ coveredWatermark += 1;
149683
+ }
149684
+ for (let ordinal = effectiveWatermark + 1;ordinal <= coveredWatermark; ordinal++) {
149685
+ const message = messagesByOrdinal.get(ordinal);
149686
+ if (!message || message.role !== "user" && message.role !== "assistant") {
149592
149687
  continue;
149593
149688
  }
149594
149689
  const content = getIndexableContent(message.role, message.parts);
149595
- if (content.length === 0) {
149690
+ if (content.length === 0 || isMessageAlreadyIndexed(db, sessionId, message.id)) {
149596
149691
  continue;
149597
149692
  }
149598
- insertMessage.run(sessionId, message.ordinal, message.id, message.role, content);
149599
- inserted++;
149693
+ getInsertMessageStatement(db).run(sessionId, message.ordinal, message.id, message.role, content);
149694
+ inserted += 1;
149600
149695
  }
149601
- const newWatermark = Math.max(effectiveWatermark, finalWatermark);
149602
- getUpsertCleanIndexStatement(db).run(sessionId, newWatermark, now, getHarness());
149696
+ const missingFloor = coveredWatermark < finalWatermark ? coveredWatermark + 1 : null;
149697
+ const preservedFloor = dirtyFloor !== null && dirtyFloor > finalWatermark ? dirtyFloor : null;
149698
+ const nextDirtyFloor = missingFloor === null ? preservedFloor : preservedFloor === null ? missingFloor : Math.min(missingFloor, preservedFloor);
149699
+ setIndexProgress(db, sessionId, coveredWatermark, nextDirtyFloor, now);
149603
149700
  db.exec("COMMIT");
149604
149701
  committed = true;
149605
149702
  } finally {
@@ -149630,6 +149727,7 @@ function isDatabaseLockedError(error) {
149630
149727
  return false;
149631
149728
  }
149632
149729
  var INCREMENTAL_DEBOUNCE_MS = 100;
149730
+ var RECONCILIATION_BATCH_SIZE = 100;
149633
149731
  var reconciledSessions = new Set;
149634
149732
  var reconciliationScheduledSessions = new Set;
149635
149733
  var sessionLocks = new Map;
@@ -149644,6 +149742,9 @@ function defer(fn) {
149644
149742
  }
149645
149743
  setTimeout(fn, 0);
149646
149744
  }
149745
+ function yieldToEventLoop() {
149746
+ return new Promise((resolve) => defer(resolve));
149747
+ }
149647
149748
  function runWithSessionLock(sessionId, operation) {
149648
149749
  const previous = sessionLocks.get(sessionId) ?? Promise.resolve();
149649
149750
  const run = previous.catch(() => {
@@ -149670,24 +149771,27 @@ function logIndexingError(sessionId, action, error) {
149670
149771
  log(`[message-index-async] ${action} failed for ${sessionId}:`, error);
149671
149772
  }
149672
149773
  async function reconcileSessionIndex(db, sessionId, readMessages) {
149673
- await runWithSessionLock(sessionId, () => {
149674
- if (reconciledSessions.has(sessionId)) {
149774
+ await runWithSessionLock(sessionId, async () => {
149775
+ if (reconciledSessions.has(sessionId))
149675
149776
  return;
149777
+ let fallbackSnapshot = null;
149778
+ const finalWatermark = readMessages.getCount ? readMessages.getCount(sessionId) : (fallbackSnapshot = readMessages(sessionId)).length;
149779
+ let cursor = getMessageIndexReconciliationStartOrdinal(db, sessionId);
149780
+ while (cursor < finalWatermark) {
149781
+ const pageEnd = Math.min(finalWatermark, cursor + RECONCILIATION_BATCH_SIZE);
149782
+ const messages = readMessages.readPage ? readMessages.readPage(sessionId, cursor, RECONCILIATION_BATCH_SIZE, finalWatermark) : (fallbackSnapshot ?? []).filter((message) => message.ordinal > cursor && message.ordinal <= pageEnd);
149783
+ indexMessagesAfterOrdinal(db, sessionId, messages, cursor, pageEnd);
149784
+ const nextCursor = getMessageIndexReconciliationStartOrdinal(db, sessionId);
149785
+ if (nextCursor <= cursor)
149786
+ break;
149787
+ cursor = nextCursor;
149788
+ if (cursor < finalWatermark) {
149789
+ await yieldToEventLoop();
149790
+ }
149676
149791
  }
149677
- const messages = readMessages(sessionId);
149678
- if (messages.length === 0) {
149679
- clearIndexedMessages(db, sessionId);
149792
+ if (isMessageIndexReconciledThrough(db, sessionId, finalWatermark)) {
149680
149793
  reconciledSessions.add(sessionId);
149681
- return;
149682
- }
149683
- let lastIndexedOrdinal = getLastIndexedOrdinal(db, sessionId);
149684
- if (lastIndexedOrdinal > messages.length) {
149685
- clearIndexedMessages(db, sessionId);
149686
- lastIndexedOrdinal = 0;
149687
149794
  }
149688
- const watermark = Math.min(lastIndexedOrdinal, messages.length);
149689
- indexMessagesAfterOrdinal(db, sessionId, messages, watermark, messages.length);
149690
- reconciledSessions.add(sessionId);
149691
149795
  });
149692
149796
  }
149693
149797
  function scheduleReconciliation(db, sessionId, readMessages) {
@@ -149711,16 +149815,23 @@ function scheduleIncrementalIndex(db, sessionId, messageId, messageSource) {
149711
149815
  const timer = setTimeout(() => {
149712
149816
  incrementalTimers.delete(key);
149713
149817
  pendingIncrementalKeys.add(key);
149714
- let attemptedOrdinal = null;
149715
149818
  runWithSessionLock(sessionId, () => {
149716
149819
  const message = typeof messageSource === "function" ? messageSource(sessionId, messageId) : messageSource;
149717
149820
  if (!message)
149718
149821
  return;
149719
- attemptedOrdinal = message.ordinal;
149822
+ const currentWatermark = getLastIndexedOrdinal(db, sessionId);
149823
+ if (message.ordinal <= currentWatermark && isMessageIndexReconciledThrough(db, sessionId, currentWatermark)) {
149824
+ completedIncrementalKeys.add(key);
149825
+ return;
149826
+ }
149827
+ const wasReconciled = reconciledSessions.delete(sessionId);
149828
+ markMessageIndexDirty(db, sessionId, Math.min(message.ordinal, getLastIndexedOrdinal(db, sessionId) + 1));
149720
149829
  indexSingleMessage(db, sessionId, message);
149830
+ if (wasReconciled && isMessageIndexReconciledThrough(db, sessionId, message.ordinal)) {
149831
+ reconciledSessions.add(sessionId);
149832
+ }
149721
149833
  completedIncrementalKeys.add(key);
149722
149834
  }).catch((error) => {
149723
- markMessageIndexDirty(db, sessionId, attemptedOrdinal ?? getLastIndexedOrdinal(db, sessionId) + 1);
149724
149835
  reconciledSessions.delete(sessionId);
149725
149836
  logIndexingError(sessionId, `incremental index for ${messageId}`, error);
149726
149837
  }).finally(() => {
@@ -149860,9 +149971,11 @@ function parseReportedLimit(message) {
149860
149971
 
149861
149972
  // ../plugin/src/features/magic-context/session-project-backfill.ts
149862
149973
  init_logger();
149974
+ import { randomUUID } from "node:crypto";
149863
149975
  import { existsSync as existsSync5 } from "node:fs";
149864
149976
 
149865
149977
  // ../plugin/src/features/magic-context/session-project-storage.ts
149978
+ var SESSION_CHUNK_REPAIR_BATCH_SIZE = 100;
149866
149979
  var upsertSessionProjectStatements = new WeakMap;
149867
149980
  var repairSessionChunkProjectStatements = new WeakMap;
149868
149981
  var repairProjectChunkProjectStatements = new WeakMap;
@@ -149884,9 +149997,14 @@ function getRepairSessionChunkProjectStatement(db) {
149884
149997
  if (!stmt) {
149885
149998
  stmt = db.prepare(`UPDATE compartment_chunk_embeddings
149886
149999
  SET project_path = ?
149887
- WHERE session_id = ?
149888
- AND harness = ?
149889
- AND project_path <> ?`);
150000
+ WHERE id IN (
150001
+ SELECT id
150002
+ FROM compartment_chunk_embeddings
150003
+ WHERE session_id = ?
150004
+ AND harness = ?
150005
+ AND project_path <> ?
150006
+ LIMIT ?
150007
+ )`);
149890
150008
  repairSessionChunkProjectStatements.set(db, stmt);
149891
150009
  }
149892
150010
  return stmt;
@@ -149924,7 +150042,7 @@ function recordSessionProjectIdentity(db, sessionId, projectPath) {
149924
150042
  const now = Date.now();
149925
150043
  db.transaction(() => {
149926
150044
  getUpsertSessionProjectStatement(db).run(sessionId, harness, projectPath, now);
149927
- getRepairSessionChunkProjectStatement(db).run(projectPath, sessionId, harness, projectPath);
150045
+ getRepairSessionChunkProjectStatement(db).run(projectPath, sessionId, harness, projectPath, SESSION_CHUNK_REPAIR_BATCH_SIZE);
149928
150046
  })();
149929
150047
  }
149930
150048
  function repairMisScopedCompartmentChunkEmbeddingsForProject(db, projectPath) {
@@ -149935,18 +150053,30 @@ function repairMisScopedCompartmentChunkEmbeddingsForProject(db, projectPath) {
149935
150053
 
149936
150054
  // ../plugin/src/features/magic-context/session-project-backfill.ts
149937
150055
  var LEASE_TTL_MS = 10 * 60 * 1000;
150056
+ var SESSION_PAGE_SIZE = 100;
149938
150057
  var SESSION_QUERY_CHUNK_SIZE = 250;
149939
- var YIELD_EVERY_NEW_DIRECTORIES = 10;
150058
+ var YIELD_EVERY_PROCESSED_SESSIONS = 25;
150059
+ var YIELD_EVERY_IDENTITY_RESOLUTIONS = 2;
149940
150060
  function ensureBackfillStateTable(db) {
149941
150061
  db.exec(`
149942
150062
  CREATE TABLE IF NOT EXISTS session_project_backfill_state (
149943
150063
  harness TEXT PRIMARY KEY,
149944
150064
  status TEXT NOT NULL CHECK (status IN ('running', 'completed')),
150065
+ holder_id TEXT,
149945
150066
  started_at INTEGER,
149946
150067
  lease_expires_at INTEGER,
149947
150068
  completed_at INTEGER
149948
150069
  );
149949
150070
  `);
150071
+ const columns = db.prepare("PRAGMA table_info(session_project_backfill_state)").all();
150072
+ if (!columns.some((column) => column.name === "holder_id")) {
150073
+ try {
150074
+ db.exec("ALTER TABLE session_project_backfill_state ADD COLUMN holder_id TEXT");
150075
+ } catch (error) {
150076
+ if (!/duplicate column name/i.test(String(error)))
150077
+ throw error;
150078
+ }
150079
+ }
149950
150080
  }
149951
150081
  function withImmediateTransaction(db, fn) {
149952
150082
  db.exec("BEGIN IMMEDIATE");
@@ -149964,54 +150094,52 @@ function withImmediateTransaction(db, fn) {
149964
150094
  function defaultYieldFn() {
149965
150095
  return new Promise((resolve) => setTimeout(resolve, 0));
149966
150096
  }
149967
- function acquireBackfillLease(db, harness, now) {
150097
+ function acquireBackfillLease(db, harness, holderId, now) {
149968
150098
  ensureBackfillStateTable(db);
149969
150099
  return withImmediateTransaction(db, () => {
149970
- const current = db.prepare(`SELECT harness, status, started_at, lease_expires_at, completed_at
150100
+ const current = db.prepare(`SELECT harness, status, holder_id, started_at, lease_expires_at, completed_at
149971
150101
  FROM session_project_backfill_state
149972
150102
  WHERE harness = ?`).get(harness);
149973
- if (current?.status === "completed") {
150103
+ if (current?.status === "completed")
149974
150104
  return "already_completed";
149975
- }
149976
150105
  if (current?.status === "running" && current.lease_expires_at !== null && current.lease_expires_at > now) {
149977
150106
  return "blocked_by_lease";
149978
150107
  }
149979
150108
  db.prepare(`INSERT INTO session_project_backfill_state(
149980
150109
  harness,
149981
150110
  status,
150111
+ holder_id,
149982
150112
  started_at,
149983
150113
  lease_expires_at,
149984
150114
  completed_at
149985
150115
  )
149986
- VALUES (?, 'running', ?, ?, NULL)
150116
+ VALUES (?, 'running', ?, ?, ?, NULL)
149987
150117
  ON CONFLICT(harness) DO UPDATE SET
149988
150118
  status = 'running',
150119
+ holder_id = excluded.holder_id,
149989
150120
  started_at = excluded.started_at,
149990
150121
  lease_expires_at = excluded.lease_expires_at,
149991
- completed_at = NULL`).run(harness, now, now + LEASE_TTL_MS);
149992
- return "completed";
150122
+ completed_at = NULL`).run(harness, holderId, now, now + LEASE_TTL_MS);
150123
+ return "acquired";
149993
150124
  });
149994
150125
  }
149995
- function renewBackfillLease(db, harness, now) {
150126
+ function renewBackfillLease(db, harness, holderId, now) {
149996
150127
  ensureBackfillStateTable(db);
149997
- db.prepare(`UPDATE session_project_backfill_state
149998
- SET lease_expires_at = ?
149999
- WHERE harness = ? AND status = 'running'`).run(now + LEASE_TTL_MS, harness);
150128
+ return db.prepare(`UPDATE session_project_backfill_state
150129
+ SET lease_expires_at = ?
150130
+ WHERE harness = ? AND status = 'running' AND holder_id = ?`).run(now + LEASE_TTL_MS, harness, holderId).changes === 1;
150131
+ }
150132
+ function markBackfillCompleted(db, harness, holderId, now) {
150133
+ ensureBackfillStateTable(db);
150134
+ return db.prepare(`UPDATE session_project_backfill_state
150135
+ SET status = 'completed', lease_expires_at = NULL, completed_at = ?
150136
+ WHERE harness = ? AND status = 'running' AND holder_id = ?`).run(now, harness, holderId).changes === 1;
150000
150137
  }
150001
- function markBackfillCompleted(db, harness, now) {
150138
+ function markBackfillRetryPending(db, harness, holderId, now) {
150002
150139
  ensureBackfillStateTable(db);
150003
- db.prepare(`INSERT INTO session_project_backfill_state(
150004
- harness,
150005
- status,
150006
- started_at,
150007
- lease_expires_at,
150008
- completed_at
150009
- )
150010
- VALUES (?, 'completed', ?, NULL, ?)
150011
- ON CONFLICT(harness) DO UPDATE SET
150012
- status = 'completed',
150013
- lease_expires_at = NULL,
150014
- completed_at = excluded.completed_at`).run(harness, now, now);
150140
+ return db.prepare(`UPDATE session_project_backfill_state
150141
+ SET lease_expires_at = ?
150142
+ WHERE harness = ? AND status = 'running' AND holder_id = ?`).run(now, harness, holderId).changes === 1;
150015
150143
  }
150016
150144
  function dedupeSessions(sessions) {
150017
150145
  const sessionsById = new Map;
@@ -150019,14 +150147,7 @@ function dedupeSessions(sessions) {
150019
150147
  if (!session.sessionId)
150020
150148
  continue;
150021
150149
  const existing = sessionsById.get(session.sessionId);
150022
- if (!existing) {
150023
- sessionsById.set(session.sessionId, {
150024
- sessionId: session.sessionId,
150025
- directory: session.directory
150026
- });
150027
- continue;
150028
- }
150029
- if (!existing.directory && session.directory) {
150150
+ if (!existing || !existing.directory && session.directory) {
150030
150151
  sessionsById.set(session.sessionId, {
150031
150152
  sessionId: session.sessionId,
150032
150153
  directory: session.directory
@@ -150046,22 +150167,34 @@ function getMappedSessionIds(db, harness, sessions) {
150046
150167
  FROM session_projects
150047
150168
  WHERE harness = ?
150048
150169
  AND session_id IN (${placeholders})`).all(harness, ...chunk.map((session) => session.sessionId));
150049
- for (const row of rows) {
150170
+ for (const row of rows)
150050
150171
  mapped.add(row.session_id);
150051
- }
150052
150172
  }
150053
150173
  return mapped;
150054
150174
  }
150055
- async function runSessionProjectBackfill(db, sessions, options = {}) {
150175
+ function createPageReader(source) {
150176
+ if (typeof source === "function") {
150177
+ return async (afterSessionId, limit) => source(afterSessionId, limit);
150178
+ }
150179
+ const sessions = dedupeSessions(source);
150180
+ let offset = 0;
150181
+ return async (_afterSessionId, limit) => {
150182
+ const page = sessions.slice(offset, offset + limit);
150183
+ offset += page.length;
150184
+ return page;
150185
+ };
150186
+ }
150187
+ async function runSessionProjectBackfill(db, source, options = {}) {
150056
150188
  const startedAt = performance.now();
150057
150189
  const harness = getHarness();
150058
150190
  const now = options.now ?? Date.now;
150059
150191
  const resolveIdentity = options.resolveIdentity ?? resolveProjectIdentity;
150060
150192
  const yieldFn = options.yieldFn ?? defaultYieldFn;
150061
- const dedupedSessions = dedupeSessions(sessions);
150193
+ const holderId = options.holderId ?? randomUUID();
150194
+ const readPage = createPageReader(source);
150062
150195
  const result = {
150063
150196
  status: "completed",
150064
- totalSessions: dedupedSessions.length,
150197
+ totalSessions: 0,
150065
150198
  alreadyMappedSessions: 0,
150066
150199
  unmappedSessions: 0,
150067
150200
  backfilledSessions: 0,
@@ -150069,47 +150202,95 @@ async function runSessionProjectBackfill(db, sessions, options = {}) {
150069
150202
  skippedEmptyDirectories: 0,
150070
150203
  durationMs: 0
150071
150204
  };
150072
- const leaseStatus = acquireBackfillLease(db, harness, now());
150073
- if (leaseStatus !== "completed") {
150205
+ const leaseStatus = acquireBackfillLease(db, harness, holderId, now());
150206
+ if (leaseStatus !== "acquired") {
150074
150207
  result.status = leaseStatus;
150075
150208
  result.durationMs = performance.now() - startedAt;
150076
150209
  return result;
150077
150210
  }
150078
- const mappedSessionIds = getMappedSessionIds(db, harness, dedupedSessions);
150079
- result.alreadyMappedSessions = mappedSessionIds.size;
150211
+ const finishWithLostLease = () => {
150212
+ result.status = "lost_lease";
150213
+ result.durationMs = performance.now() - startedAt;
150214
+ return result;
150215
+ };
150216
+ const yieldAndRenew = async () => {
150217
+ if (!renewBackfillLease(db, harness, holderId, now()))
150218
+ return false;
150219
+ await yieldFn();
150220
+ return renewBackfillLease(db, harness, holderId, now());
150221
+ };
150080
150222
  const existenceCache = new Map;
150081
150223
  const identityCache2 = new Map;
150082
- let newDirectoryResolutions = 0;
150083
- for (const session of dedupedSessions) {
150084
- if (mappedSessionIds.has(session.sessionId)) {
150085
- continue;
150086
- }
150087
- result.unmappedSessions += 1;
150088
- if (!session.directory) {
150089
- result.skippedEmptyDirectories += 1;
150090
- continue;
150091
- }
150092
- const stillExists = existenceCache.get(session.directory) ?? existsSync5(session.directory);
150093
- existenceCache.set(session.directory, stillExists);
150094
- if (!stillExists) {
150095
- result.skippedDeadDirectories += 1;
150096
- continue;
150097
- }
150098
- let identity = identityCache2.get(session.directory);
150099
- if (!identity) {
150100
- identity = resolveIdentity(session.directory);
150101
- identityCache2.set(session.directory, identity);
150102
- newDirectoryResolutions += 1;
150103
- if (newDirectoryResolutions % YIELD_EVERY_NEW_DIRECTORIES === 0) {
150104
- renewBackfillLease(db, harness, now());
150105
- await yieldFn();
150106
- renewBackfillLease(db, harness, now());
150224
+ const seenSessionIds = new Set;
150225
+ let processedSinceYield = 0;
150226
+ let identityResolutionsSinceYield = 0;
150227
+ let afterSessionId = null;
150228
+ for (;; ) {
150229
+ const sourcePage = await readPage(afterSessionId, SESSION_PAGE_SIZE);
150230
+ if (sourcePage.length === 0)
150231
+ break;
150232
+ afterSessionId = sourcePage.at(-1)?.sessionId ?? afterSessionId;
150233
+ const page = dedupeSessions(sourcePage).filter((session) => {
150234
+ if (seenSessionIds.has(session.sessionId))
150235
+ return false;
150236
+ seenSessionIds.add(session.sessionId);
150237
+ return true;
150238
+ });
150239
+ const mappedSessionIds = getMappedSessionIds(db, harness, page);
150240
+ for (const session of page) {
150241
+ result.totalSessions += 1;
150242
+ processedSinceYield += 1;
150243
+ if (mappedSessionIds.has(session.sessionId)) {
150244
+ result.alreadyMappedSessions += 1;
150245
+ } else {
150246
+ result.unmappedSessions += 1;
150247
+ if (!session.directory) {
150248
+ result.skippedEmptyDirectories += 1;
150249
+ } else {
150250
+ const stillExists = existenceCache.get(session.directory) ?? existsSync5(session.directory);
150251
+ existenceCache.set(session.directory, stillExists);
150252
+ if (!stillExists) {
150253
+ result.skippedDeadDirectories += 1;
150254
+ } else {
150255
+ let identity = identityCache2.get(session.directory);
150256
+ if (!identity) {
150257
+ if (identityResolutionsSinceYield >= YIELD_EVERY_IDENTITY_RESOLUTIONS) {
150258
+ if (!await yieldAndRenew())
150259
+ return finishWithLostLease();
150260
+ processedSinceYield = 0;
150261
+ identityResolutionsSinceYield = 0;
150262
+ }
150263
+ identity = await resolveIdentity(session.directory);
150264
+ identityResolutionsSinceYield += 1;
150265
+ if (!renewBackfillLease(db, harness, holderId, now())) {
150266
+ return finishWithLostLease();
150267
+ }
150268
+ identityCache2.set(session.directory, identity);
150269
+ }
150270
+ recordSessionProjectIdentity(db, session.sessionId, identity);
150271
+ result.backfilledSessions += 1;
150272
+ }
150273
+ }
150107
150274
  }
150275
+ if (processedSinceYield >= YIELD_EVERY_PROCESSED_SESSIONS) {
150276
+ if (!await yieldAndRenew())
150277
+ return finishWithLostLease();
150278
+ processedSinceYield = 0;
150279
+ identityResolutionsSinceYield = 0;
150280
+ }
150281
+ }
150282
+ if (sourcePage.length < SESSION_PAGE_SIZE)
150283
+ break;
150284
+ }
150285
+ const hasSkippedSessions = result.skippedDeadDirectories > 0 || result.skippedEmptyDirectories > 0;
150286
+ if (hasSkippedSessions) {
150287
+ if (!markBackfillRetryPending(db, harness, holderId, now())) {
150288
+ return finishWithLostLease();
150108
150289
  }
150109
- recordSessionProjectIdentity(db, session.sessionId, identity);
150110
- result.backfilledSessions += 1;
150290
+ result.status = "retry_pending";
150291
+ } else if (!markBackfillCompleted(db, harness, holderId, now())) {
150292
+ return finishWithLostLease();
150111
150293
  }
150112
- markBackfillCompleted(db, harness, now());
150113
150294
  result.durationMs = performance.now() - startedAt;
150114
150295
  log(`[session-projects] backfilled ${result.backfilledSessions} of ${result.unmappedSessions} unmapped sessions (skipped ${result.skippedDeadDirectories} dead dirs) in ${Math.round(result.durationMs)}ms`);
150115
150296
  return result;
@@ -150667,10 +150848,16 @@ function healAllNullColumns(db) {
150667
150848
  healNullIntegerColumns(db);
150668
150849
  healMissingMemoryBlockIds(db);
150669
150850
  }
150851
+ function getSessionMetaColumns(db) {
150852
+ const rows = db.prepare("PRAGMA table_info(session_meta)").all();
150853
+ return new Set(rows.flatMap((row) => typeof row.name === "string" ? [row.name] : []));
150854
+ }
150670
150855
  function healMissingMemoryBlockIds(db) {
150671
- try {
150672
- db.prepare("UPDATE session_meta SET memory_block_cache = '' WHERE memory_block_cache != '' AND (memory_block_ids IS NULL OR memory_block_ids = '') AND memory_block_count > 0").run();
150673
- } catch {}
150856
+ const columns = getSessionMetaColumns(db);
150857
+ if (!columns.has("memory_block_cache") || !columns.has("memory_block_ids") || !columns.has("memory_block_count")) {
150858
+ return;
150859
+ }
150860
+ db.prepare("UPDATE session_meta SET memory_block_cache = '' WHERE memory_block_cache != '' AND (memory_block_ids IS NULL OR memory_block_ids = '') AND memory_block_count > 0").run();
150674
150861
  }
150675
150862
  function healNullTextColumns(db) {
150676
150863
  const columns = [
@@ -150699,10 +150886,11 @@ function healNullTextColumns(db) {
150699
150886
  ["compaction_marker_state", ""],
150700
150887
  ["key_files", ""]
150701
150888
  ];
150889
+ const existingColumns = getSessionMetaColumns(db);
150702
150890
  for (const [column, fallback] of columns) {
150703
- try {
150704
- db.prepare(`UPDATE session_meta SET ${column} = ? WHERE ${column} IS NULL`).run(fallback);
150705
- } catch (_error) {}
150891
+ if (!existingColumns.has(column))
150892
+ continue;
150893
+ db.prepare(`UPDATE session_meta SET ${column} = ? WHERE ${column} IS NULL`).run(fallback);
150706
150894
  }
150707
150895
  }
150708
150896
  function healNullIntegerColumns(db) {
@@ -150733,10 +150921,11 @@ function healNullIntegerColumns(db) {
150733
150921
  ["emergency_drain_active", 0],
150734
150922
  ["historian_drain_failure_at", 0]
150735
150923
  ];
150924
+ const existingColumns = getSessionMetaColumns(db);
150736
150925
  for (const [column, fallback] of columns) {
150737
- try {
150738
- db.prepare(`UPDATE session_meta SET ${column} = ? WHERE ${column} IS NULL`).run(fallback);
150739
- } catch (_error) {}
150926
+ if (!existingColumns.has(column))
150927
+ continue;
150928
+ db.prepare(`UPDATE session_meta SET ${column} = ? WHERE ${column} IS NULL`).run(fallback);
150740
150929
  }
150741
150930
  }
150742
150931
 
@@ -152384,6 +152573,34 @@ var MIGRATIONS = [
152384
152573
  ensureColumn(db, "session_meta", "wrapup_in_progress_state", "TEXT");
152385
152574
  }
152386
152575
  }
152576
+ },
152577
+ {
152578
+ version: 51,
152579
+ description: "version tool-owner backfill state and repair legacy NULL session metadata",
152580
+ up(db) {
152581
+ db.exec(`
152582
+ CREATE TABLE IF NOT EXISTS tool_owner_backfill_state (
152583
+ session_id TEXT PRIMARY KEY,
152584
+ status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'completed', 'skipped')),
152585
+ started_at INTEGER,
152586
+ lease_expires_at INTEGER,
152587
+ completed_at INTEGER,
152588
+ last_error TEXT
152589
+ );
152590
+ CREATE INDEX IF NOT EXISTS idx_tool_owner_backfill_state_status
152591
+ ON tool_owner_backfill_state(status);
152592
+ `);
152593
+ healAllNullColumns(db);
152594
+ }
152595
+ },
152596
+ {
152597
+ version: 52,
152598
+ description: "persist emergency recovery origin",
152599
+ up(db) {
152600
+ if (tableExists2(db, "session_meta")) {
152601
+ ensureColumn(db, "session_meta", "emergency_recovery_origin", "TEXT DEFAULT ''");
152602
+ }
152603
+ }
152387
152604
  }
152388
152605
  ];
152389
152606
  var LATEST_MIGRATION_VERSION = MIGRATIONS.reduce((max, m) => Math.max(max, m.version), 0);
@@ -152413,40 +152630,45 @@ function isSiblingMigrationConflict(db, error, version) {
152413
152630
  }
152414
152631
  function runMigrations(db) {
152415
152632
  ensureMigrationsTable(db);
152416
- let currentVersion = getCurrentVersion(db);
152417
- let pendingMigrations = MIGRATIONS.filter((m) => m.version > currentVersion);
152418
- if (pendingMigrations.length === 0) {
152419
- return;
152420
- }
152421
- log(`[migrations] current schema version: ${currentVersion}, applying ${pendingMigrations.length} migration(s)`);
152422
- let migrationIndex = 0;
152423
- while (migrationIndex < pendingMigrations.length) {
152424
- const migration = pendingMigrations[migrationIndex];
152633
+ let loggedPlan = false;
152634
+ while (true) {
152635
+ let migration;
152636
+ let currentVersion = 0;
152425
152637
  try {
152426
- db.transaction(() => {
152638
+ const applied = db.transaction(() => {
152639
+ currentVersion = getCurrentVersion(db);
152640
+ migration = MIGRATIONS.find((candidate) => candidate.version > currentVersion);
152641
+ if (!migration)
152642
+ return false;
152643
+ if (!loggedPlan) {
152644
+ const pendingCount = MIGRATIONS.filter((candidate) => candidate.version > currentVersion).length;
152645
+ log(`[migrations] current schema version: ${currentVersion}, applying ${pendingCount} migration(s)`);
152646
+ loggedPlan = true;
152647
+ }
152427
152648
  migration.up(db);
152428
152649
  db.prepare("INSERT INTO schema_migrations (version, description, applied_at) VALUES (?, ?, ?)").run(migration.version, migration.description, Date.now());
152429
- })();
152650
+ return true;
152651
+ }).immediate();
152652
+ if (!applied || !migration)
152653
+ break;
152430
152654
  log(`[migrations] applied v${migration.version}: ${migration.description}`);
152431
- migrationIndex += 1;
152432
152655
  } catch (error) {
152433
- if (isSiblingMigrationConflict(db, error, migration.version)) {
152656
+ if (migration && isSiblingMigrationConflict(db, error, migration.version)) {
152434
152657
  log(`[migrations] v${migration.version} already applied by sibling instance — resuming with re-read version`);
152435
152658
  const reReadVersion = getCurrentVersion(db);
152436
- if (reReadVersion <= currentVersion) {
152437
- log(`[migrations] FAILED v${migration.version}: sibling-conflict shape but version not advanced (${reReadVersion} <= ${currentVersion}) — failing closed`);
152438
- throw new Error(`Migration v${migration.version} failed: sibling conflict reported but version did not advance. Database may need manual repair.`);
152439
- }
152440
- currentVersion = reReadVersion;
152441
- pendingMigrations = MIGRATIONS.filter((m) => m.version > currentVersion);
152442
- migrationIndex = 0;
152443
- continue;
152659
+ if (reReadVersion > currentVersion)
152660
+ continue;
152661
+ throw new Error(`Migration v${migration.version} failed: sibling conflict reported but version did not advance. Database may need manual repair.`);
152444
152662
  }
152445
- log(`[migrations] FAILED v${migration.version}: ${migration.description} — ${error instanceof Error ? error.message : String(error)}`);
152446
- throw new Error(`Migration v${migration.version} failed: ${error instanceof Error ? error.message : String(error)}. Database may need manual repair.`);
152663
+ const version = migration?.version ?? currentVersion + 1;
152664
+ const description = migration?.description ?? "acquire migration write lock";
152665
+ log(`[migrations] FAILED v${version}: ${description} — ${error instanceof Error ? error.message : String(error)}`);
152666
+ throw new Error(`Migration v${version} failed: ${error instanceof Error ? error.message : String(error)}. Database may need manual repair.`);
152447
152667
  }
152448
152668
  }
152449
- log(`[migrations] schema version now: ${MIGRATIONS[MIGRATIONS.length - 1].version}`);
152669
+ if (loggedPlan) {
152670
+ log(`[migrations] schema version now: ${MIGRATIONS[MIGRATIONS.length - 1].version}`);
152671
+ }
152450
152672
  }
152451
152673
  // ../plugin/src/features/magic-context/project-docs-hash.ts
152452
152674
  import { createHash as createHash3 } from "node:crypto";
@@ -152959,7 +153181,8 @@ function countSessionsByStatus(db, status) {
152959
153181
  function acquireSessionLease(db, sessionId, now) {
152960
153182
  const expiresAt = now + LEASE_DURATION_MS;
152961
153183
  const result = db.prepare(`INSERT INTO tool_owner_backfill_state(session_id, status, started_at, lease_expires_at)
152962
- VALUES (?, 'running', ?, ?)
153184
+ SELECT ?, 'running', ?, ?
153185
+ WHERE EXISTS (SELECT 1 FROM tags WHERE session_id = ?)
152963
153186
  ON CONFLICT(session_id) DO UPDATE SET
152964
153187
  status = 'running',
152965
153188
  started_at = excluded.started_at,
@@ -152967,7 +153190,7 @@ function acquireSessionLease(db, sessionId, now) {
152967
153190
  last_error = NULL
152968
153191
  WHERE tool_owner_backfill_state.status IN ('pending', 'skipped')
152969
153192
  OR (tool_owner_backfill_state.status = 'running'
152970
- AND tool_owner_backfill_state.lease_expires_at < ?)`).run(sessionId, now, expiresAt, now);
153193
+ AND tool_owner_backfill_state.lease_expires_at < ?)`).run(sessionId, now, expiresAt, sessionId, now);
152971
153194
  return (result.changes ?? 0) === 1;
152972
153195
  }
152973
153196
  function renewSessionLease(db, sessionId, now) {
@@ -152987,13 +153210,9 @@ function markSessionPendingRetry(db, sessionId) {
152987
153210
  WHERE session_id = ?`).run(sessionId);
152988
153211
  }
152989
153212
  function markSessionSkipped(db, sessionId, now, reason) {
152990
- db.prepare(`INSERT INTO tool_owner_backfill_state(session_id, status, completed_at, last_error)
152991
- VALUES (?, 'skipped', ?, ?)
152992
- ON CONFLICT(session_id) DO UPDATE SET
152993
- status = 'skipped',
152994
- completed_at = excluded.completed_at,
152995
- last_error = excluded.last_error,
152996
- lease_expires_at = NULL`).run(sessionId, now, reason);
153213
+ db.prepare(`UPDATE tool_owner_backfill_state
153214
+ SET status = 'skipped', completed_at = ?, last_error = ?, lease_expires_at = NULL
153215
+ WHERE session_id = ? AND status = 'running'`).run(now, reason, sessionId);
152997
153216
  }
152998
153217
  function markSessionErrored(db, sessionId, error) {
152999
153218
  const message = error instanceof Error ? error.message : String(error);
@@ -153077,7 +153296,7 @@ function applyOwnersForSession(db, sessionId, ownersByCallId) {
153077
153296
  const result = updateRowStmt.run(ownerId, orphan.id);
153078
153297
  rowsUpdated += result.changes ?? 0;
153079
153298
  }
153080
- })();
153299
+ }).immediate();
153081
153300
  const rowsLeftNull = db.prepare(`SELECT COUNT(*) AS c FROM tags
153082
153301
  WHERE session_id = ? AND type = 'tool'
153083
153302
  AND tool_owner_message_id IS NULL`).get(sessionId).c;
@@ -153127,7 +153346,7 @@ var persistenceByDatabase = new WeakMap;
153127
153346
  var persistenceErrorByDatabase = new WeakMap;
153128
153347
  var pathByDatabase = new WeakMap;
153129
153348
  var lastSchemaFenceRejection = null;
153130
- var LATEST_SUPPORTED_VERSION = 50;
153349
+ var LATEST_SUPPORTED_VERSION = 52;
153131
153350
  var PERMISSIONS_ENFORCEABLE = process.platform !== "win32";
153132
153351
  function ensureSecureStorageDir(dir) {
153133
153352
  mkdirSync4(dir, { recursive: true, mode: 448 });
@@ -153820,6 +154039,17 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
153820
154039
  pi_stable_id_scheme INTEGER
153821
154040
  );
153822
154041
 
154042
+ CREATE TABLE IF NOT EXISTS tool_owner_backfill_state (
154043
+ session_id TEXT PRIMARY KEY,
154044
+ status TEXT NOT NULL CHECK (status IN ('pending', 'running', 'completed', 'skipped')),
154045
+ started_at INTEGER,
154046
+ lease_expires_at INTEGER,
154047
+ completed_at INTEGER,
154048
+ last_error TEXT
154049
+ );
154050
+ CREATE INDEX IF NOT EXISTS idx_tool_owner_backfill_state_status
154051
+ ON tool_owner_backfill_state(status);
154052
+
153823
154053
  CREATE TABLE IF NOT EXISTS subagent_invocations (
153824
154054
  id INTEGER PRIMARY KEY AUTOINCREMENT,
153825
154055
  session_id TEXT NOT NULL,
@@ -154053,6 +154283,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
154053
154283
  ensureColumn(db, "session_meta", "detected_context_limit", "INTEGER DEFAULT 0");
154054
154284
  ensureColumn(db, "session_meta", "detected_context_limit_model_key", "TEXT");
154055
154285
  ensureColumn(db, "session_meta", "needs_emergency_recovery", "INTEGER DEFAULT 0");
154286
+ ensureColumn(db, "session_meta", "emergency_recovery_origin", "TEXT DEFAULT ''");
154056
154287
  ensureColumn(db, "session_meta", "pending_compaction_marker_state", "TEXT");
154057
154288
  ensureColumn(db, "session_meta", "pending_pi_compaction_marker_state", "TEXT");
154058
154289
  ensureColumn(db, "session_meta", "new_work_tokens", "INTEGER NOT NULL DEFAULT 0");
@@ -155120,13 +155351,17 @@ function clearHistorianFailureState(db, sessionId) {
155120
155351
  function normalizeDetectedLimitModelKey(modelKey) {
155121
155352
  return typeof modelKey === "string" && modelKey.length > 0 ? modelKey : null;
155122
155353
  }
155354
+ function normalizeEmergencyRecoveryOrigin(value) {
155355
+ return value === "provider_overflow" || value === "proactive_model_shrink" ? value : null;
155356
+ }
155123
155357
  function getOverflowState(db, sessionId, modelKey) {
155124
- const result = db.prepare("SELECT detected_context_limit, detected_context_limit_model_key, needs_emergency_recovery FROM session_meta WHERE session_id = ?").get(sessionId);
155358
+ const result = db.prepare("SELECT detected_context_limit, detected_context_limit_model_key, needs_emergency_recovery, emergency_recovery_origin FROM session_meta WHERE session_id = ?").get(sessionId);
155125
155359
  if (!result) {
155126
155360
  return {
155127
155361
  detectedContextLimit: 0,
155128
155362
  detectedContextLimitModelKey: null,
155129
- needsEmergencyRecovery: false
155363
+ needsEmergencyRecovery: false,
155364
+ emergencyRecoveryOrigin: null
155130
155365
  };
155131
155366
  }
155132
155367
  const storedModelKey = normalizeDetectedLimitModelKey(result.detected_context_limit_model_key);
@@ -155134,19 +155369,22 @@ function getOverflowState(db, sessionId, modelKey) {
155134
155369
  const limit = typeof result.detected_context_limit === "number" && result.detected_context_limit > 0 ? result.detected_context_limit : 0;
155135
155370
  const modelMatches = limit > 0 && requestedModelKey && storedModelKey ? requestedModelKey === storedModelKey : true;
155136
155371
  const needs = typeof result.needs_emergency_recovery === "number" && result.needs_emergency_recovery > 0;
155372
+ const persistedOrigin = normalizeEmergencyRecoveryOrigin(result.emergency_recovery_origin);
155373
+ const recoveryOrigin = needs ? persistedOrigin ?? (limit > 0 ? "provider_overflow" : null) : null;
155137
155374
  return {
155138
155375
  detectedContextLimit: modelMatches ? limit : 0,
155139
155376
  detectedContextLimitModelKey: storedModelKey,
155140
- needsEmergencyRecovery: needs
155377
+ needsEmergencyRecovery: needs,
155378
+ emergencyRecoveryOrigin: recoveryOrigin
155141
155379
  };
155142
155380
  }
155143
- function recordOverflowDetected(db, sessionId, reportedLimit, modelKey) {
155381
+ function recordOverflowDetected(db, sessionId, reportedLimit, modelKey, origin = "provider_overflow") {
155144
155382
  db.transaction(() => {
155145
155383
  ensureSessionMetaRow(db, sessionId);
155146
155384
  if (typeof reportedLimit === "number" && reportedLimit > 0) {
155147
- db.prepare("UPDATE session_meta SET detected_context_limit = ?, detected_context_limit_model_key = ?, needs_emergency_recovery = 1, observed_safe_input_tokens = 0, cache_alert_sent = 0 WHERE session_id = ?").run(reportedLimit, normalizeDetectedLimitModelKey(modelKey), sessionId);
155385
+ db.prepare("UPDATE session_meta SET detected_context_limit = ?, detected_context_limit_model_key = ?, needs_emergency_recovery = 1, emergency_recovery_origin = ?, observed_safe_input_tokens = 0, cache_alert_sent = 0 WHERE session_id = ?").run(reportedLimit, normalizeDetectedLimitModelKey(modelKey), origin, sessionId);
155148
155386
  } else {
155149
- db.prepare("UPDATE session_meta SET needs_emergency_recovery = 1, observed_safe_input_tokens = 0, cache_alert_sent = 0 WHERE session_id = ?").run(sessionId);
155387
+ db.prepare("UPDATE session_meta SET needs_emergency_recovery = 1, emergency_recovery_origin = ?, observed_safe_input_tokens = 0, cache_alert_sent = 0 WHERE session_id = ?").run(origin, sessionId);
155150
155388
  }
155151
155389
  })();
155152
155390
  }
@@ -155154,9 +155392,9 @@ function clearEmergencyRecovery(db, sessionId) {
155154
155392
  db.transaction(() => {
155155
155393
  ensureSessionMetaRow(db, sessionId);
155156
155394
  try {
155157
- db.prepare("UPDATE session_meta SET needs_emergency_recovery = 0, recovery_no_eligible_head_count = 0 WHERE session_id = ?").run(sessionId);
155395
+ db.prepare("UPDATE session_meta SET needs_emergency_recovery = 0, emergency_recovery_origin = '', recovery_no_eligible_head_count = 0 WHERE session_id = ?").run(sessionId);
155158
155396
  } catch {
155159
- db.prepare("UPDATE session_meta SET needs_emergency_recovery = 0 WHERE session_id = ?").run(sessionId);
155397
+ db.prepare("UPDATE session_meta SET needs_emergency_recovery = 0, emergency_recovery_origin = '' WHERE session_id = ?").run(sessionId);
155160
155398
  }
155161
155399
  })();
155162
155400
  }
@@ -156785,12 +157023,12 @@ async function runDeferredV22Backfill(db, options = {}) {
156785
157023
  };
156786
157024
  }
156787
157025
  const resolveIdentity = options.resolveIdentity ?? resolveBackfillIdentity;
156788
- const yieldToEventLoop = options.yieldToEventLoop ?? defaultYieldToEventLoop;
157026
+ const yieldToEventLoop2 = options.yieldToEventLoop ?? defaultYieldToEventLoop;
156789
157027
  let lastCursor = parseCursor(readMeta(db, BACKFILL_CURSOR_META_KEY));
156790
157028
  let processedRows = 0;
156791
157029
  let changedRows = 0;
156792
157030
  let failedRows = 0;
156793
- await yieldToEventLoop();
157031
+ await yieldToEventLoop2();
156794
157032
  while (true) {
156795
157033
  const batch = db.prepare(`SELECT id, project_path, category, normalized_hash, seen_count
156796
157034
  FROM memories
@@ -156812,7 +157050,7 @@ async function runDeferredV22Backfill(db, options = {}) {
156812
157050
  failedBatchRows.push({ ...row, ...classified });
156813
157051
  }
156814
157052
  if ((index + 1) % YIELD_EVERY_N_ROWS === 0 && index + 1 < batch.length) {
156815
- await yieldToEventLoop();
157053
+ await yieldToEventLoop2();
156816
157054
  }
156817
157055
  }
156818
157056
  await options.onBatchResolved?.(batch);
@@ -156871,10 +157109,10 @@ async function runDeferredV22Backfill(db, options = {}) {
156871
157109
  bumpProjectMemoryEpochInTransaction(db, identity, now);
156872
157110
  }
156873
157111
  writeMeta(db, BACKFILL_CURSOR_META_KEY, String(finalCursor));
156874
- })();
157112
+ }).immediate();
156875
157113
  processedRows += batch.length;
156876
157114
  lastCursor = finalCursor;
156877
- await yieldToEventLoop();
157115
+ await yieldToEventLoop2();
156878
157116
  }
156879
157117
  const status = updateFinalBackfillStatus(db);
156880
157118
  const failureCount = countFailures(db);
@@ -156916,9 +157154,15 @@ function loadPersistedApiCacheOnce() {
156916
157154
  const raw = readFileSync4(persistFilePath(), "utf-8");
156917
157155
  const obj = JSON.parse(raw);
156918
157156
  const map = new Map;
156919
- for (const [key, limit] of Object.entries(obj)) {
156920
- if (isSaneLimit(limit))
156921
- map.set(key, { limit });
157157
+ for (const [key, persisted] of Object.entries(obj)) {
157158
+ const limit = typeof persisted === "number" ? persisted : persisted.limit;
157159
+ const inputLimit = typeof persisted === "number" ? undefined : persisted.inputLimit;
157160
+ if (isSaneLimit(limit)) {
157161
+ map.set(key, {
157162
+ limit,
157163
+ inputLimit: isSaneLimit(inputLimit) ? inputLimit : undefined
157164
+ });
157165
+ }
156922
157166
  }
156923
157167
  if (map.size > 0) {
156924
157168
  apiCache = map;
@@ -173968,10 +174212,10 @@ function civilMinuteKey(date5) {
173968
174212
  const p = (n, w = 2) => String(n).padStart(w, "0");
173969
174213
  return `${p(date5.getFullYear(), 4)}-${p(date5.getMonth() + 1)}-${p(date5.getDate())} ${p(date5.getHours())}:${p(date5.getMinutes())}`;
173970
174214
  }
173971
- function nextOccurrence(cron, after, excludeCivilMinute) {
174215
+ function nextOccurrence(cron, after, excludeCivilMinute, maxSearchMs = MAX_SEARCH_MS) {
173972
174216
  const afterMs = after.getTime();
173973
174217
  let cursorMs = Math.floor(afterMs / MINUTE_MS) * MINUTE_MS + MINUTE_MS;
173974
- const capMs = afterMs + MAX_SEARCH_MS;
174218
+ const capMs = afterMs + Math.max(0, Math.min(MAX_SEARCH_MS, maxSearchMs));
173975
174219
  while (cursorMs <= capMs) {
173976
174220
  const candidate = new Date(cursorMs);
173977
174221
  if (matchesCron(cron, candidate)) {
@@ -173983,14 +174227,14 @@ function nextOccurrence(cron, after, excludeCivilMinute) {
173983
174227
  }
173984
174228
  return null;
173985
174229
  }
173986
- function nextDueAtMs(expression, afterMs, consumedScheduledAtMs) {
174230
+ function nextDueAtMs(expression, afterMs, consumedScheduledAtMs, maxSearchMs = MAX_SEARCH_MS) {
173987
174231
  if (expression.trim().length === 0)
173988
174232
  return null;
173989
174233
  const parsed = parseCron(expression);
173990
174234
  if (!parsed.ok)
173991
174235
  return null;
173992
174236
  const exclude = consumedScheduledAtMs !== undefined ? civilMinuteKey(new Date(consumedScheduledAtMs)) : undefined;
173993
- const next = nextOccurrence(parsed.cron, new Date(afterMs), exclude);
174237
+ const next = nextOccurrence(parsed.cron, new Date(afterMs), exclude, maxSearchMs);
173994
174238
  return next ? next.getTime() : null;
173995
174239
  }
173996
174240
 
@@ -174385,22 +174629,6 @@ function getInsertEmbeddingStatement(db) {
174385
174629
  }
174386
174630
  return stmt;
174387
174631
  }
174388
- function getClearProjectStatement(db) {
174389
- let stmt = clearProjectStatements.get(db);
174390
- if (!stmt) {
174391
- stmt = db.prepare("DELETE FROM compartment_chunk_embeddings WHERE project_path = ?");
174392
- clearProjectStatements.set(db, stmt);
174393
- }
174394
- return stmt;
174395
- }
174396
- function getClearProjectModelStatement(db) {
174397
- let stmt = clearProjectModelStatements.get(db);
174398
- if (!stmt) {
174399
- stmt = db.prepare("DELETE FROM compartment_chunk_embeddings WHERE project_path = ? AND model_id = ?");
174400
- clearProjectModelStatements.set(db, stmt);
174401
- }
174402
- return stmt;
174403
- }
174404
174632
  function getSearchRowsStatement(db, withModel) {
174405
174633
  const map2 = withModel ? searchRowsByModelStatements : searchRowsStatements;
174406
174634
  let stmt = map2.get(db);
@@ -174704,12 +174932,6 @@ function replaceCompartmentChunkEmbeddings(db, rows) {
174704
174932
  }
174705
174933
  })();
174706
174934
  }
174707
- function clearChunkEmbeddingsForProject(db, projectPath, modelId) {
174708
- if (modelId) {
174709
- return getClearProjectModelStatement(db).run(projectPath, modelId).changes;
174710
- }
174711
- return getClearProjectStatement(db).run(projectPath).changes;
174712
- }
174713
174935
  function loadCompartmentChunkEmbeddingsForSearch(db, sessionId, projectPath, modelId) {
174714
174936
  if (!modelId) {
174715
174937
  throw new Error("loadCompartmentChunkEmbeddingsForSearch requires a current model id");
@@ -175634,22 +175856,6 @@ function getLoadAllEmbeddingsStatement(db) {
175634
175856
  }
175635
175857
  return stmt;
175636
175858
  }
175637
- function getClearAllEmbeddingsStatement(db) {
175638
- let stmt = clearAllEmbeddingsStatements.get(db);
175639
- if (!stmt) {
175640
- stmt = db.prepare("DELETE FROM memory_embeddings WHERE memory_id IN (SELECT id FROM memories WHERE project_path = ?)");
175641
- clearAllEmbeddingsStatements.set(db, stmt);
175642
- }
175643
- return stmt;
175644
- }
175645
- function getClearModelEmbeddingsStatement(db) {
175646
- let stmt = clearModelEmbeddingsStatements.get(db);
175647
- if (!stmt) {
175648
- stmt = db.prepare("DELETE FROM memory_embeddings WHERE model_id = ? AND memory_id IN (SELECT id FROM memories WHERE project_path = ?)");
175649
- clearModelEmbeddingsStatements.set(db, stmt);
175650
- }
175651
- return stmt;
175652
- }
175653
175859
  function saveEmbedding(db, memoryId, embedding, modelId) {
175654
175860
  const blob = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
175655
175861
  getSaveEmbeddingStatement(db).run(memoryId, blob, modelId);
@@ -175669,12 +175875,6 @@ function loadAllEmbeddings(db, projectPath, modelId) {
175669
175875
  }
175670
175876
  return embeddings;
175671
175877
  }
175672
- function clearEmbeddingsForProject(db, projectPath, modelId) {
175673
- if (modelId) {
175674
- return getClearModelEmbeddingsStatement(db).run(modelId, projectPath).changes;
175675
- }
175676
- return getClearAllEmbeddingsStatement(db).run(projectPath).changes;
175677
- }
175678
175878
  function getMemoryEmbedCoverage(db, projectPath, modelId) {
175679
175879
  const row = db.prepare(`SELECT
175680
175880
  COUNT(*) AS total,
@@ -175691,7 +175891,7 @@ function getMemoryEmbedCoverage(db, projectPath, modelId) {
175691
175891
  }
175692
175892
 
175693
175893
  // ../plugin/src/features/magic-context/project-embedding-registry.ts
175694
- import { createHash as createHash8, randomUUID } from "node:crypto";
175894
+ import { createHash as createHash8, randomUUID as randomUUID2 } from "node:crypto";
175695
175895
  init_logger();
175696
175896
 
175697
175897
  // ../plugin/src/features/magic-context/git-commits/storage-git-commit-embeddings.ts
@@ -175747,24 +175947,6 @@ function getCountEmbeddedStatement(db) {
175747
175947
  }
175748
175948
  return stmt;
175749
175949
  }
175750
- function getClearProjectStatement2(db) {
175751
- let stmt = clearProjectStatements2.get(db);
175752
- if (!stmt) {
175753
- stmt = db.prepare(`DELETE FROM git_commit_embeddings
175754
- WHERE sha IN (SELECT sha FROM git_commits WHERE project_path = ?)`);
175755
- clearProjectStatements2.set(db, stmt);
175756
- }
175757
- return stmt;
175758
- }
175759
- function getClearProjectModelStatement2(db) {
175760
- let stmt = clearProjectModelStatements2.get(db);
175761
- if (!stmt) {
175762
- stmt = db.prepare(`DELETE FROM git_commit_embeddings
175763
- WHERE model_id = ? AND sha IN (SELECT sha FROM git_commits WHERE project_path = ?)`);
175764
- clearProjectModelStatements2.set(db, stmt);
175765
- }
175766
- return stmt;
175767
- }
175768
175950
  function saveCommitEmbedding(db, sha, embedding, modelId) {
175769
175951
  const bytes = new Uint8Array(embedding.buffer, embedding.byteOffset, embedding.byteLength);
175770
175952
  getSaveStatement(db).run(sha, bytes, modelId, Date.now());
@@ -175785,12 +175967,6 @@ function countEmbeddedCommits(db, projectPath, modelId) {
175785
175967
  const row = getCountEmbeddedStatement(db).get(projectPath, modelId);
175786
175968
  return row?.count ?? 0;
175787
175969
  }
175788
- function clearProjectCommitEmbeddings(db, projectPath, modelId) {
175789
- if (modelId) {
175790
- return getClearProjectModelStatement2(db).run(modelId, projectPath).changes;
175791
- }
175792
- return getClearProjectStatement2(db).run(projectPath).changes;
175793
- }
175794
175970
 
175795
175971
  // ../plugin/src/features/magic-context/git-commits/storage-git-commits.ts
175796
175972
  init_logger();
@@ -176098,6 +176274,7 @@ var COMMIT_DRAIN_BATCH_SIZE = 16;
176098
176274
  var COMMIT_DRAIN_MAX_PER_SWEEP = 500;
176099
176275
  var CHUNK_DRAIN_BATCH_SIZE = 8;
176100
176276
  var EMBEDDING_IDENTITY_GC_GRACE_MS = 14 * 24 * 60 * 60 * 1000;
176277
+ var STALE_EMBEDDING_GC_BATCH_SIZE = 250;
176101
176278
  var MAX_WINDOWS_PER_EMBED_CALL = 2;
176102
176279
  var SESSION_EMBED_LEASE_RENEWAL_MS = 60 * 1000;
176103
176280
  var EMBED_SLICE_RETRY_ATTEMPTS = 3;
@@ -176326,6 +176503,55 @@ function staleModelsForScope(db, projectIdentity, scope, currentModelId, cutoff)
176326
176503
  const rows = getStaleIdentityStatement(db, scope).all(projectIdentity, scope, currentModelId, cutoff);
176327
176504
  return rows.map((row) => row.modelId).filter((modelId) => typeof modelId === "string");
176328
176505
  }
176506
+ function deleteStaleEmbeddingBatch(db, scope, projectIdentity, modelId, limit) {
176507
+ if (scope === "memory") {
176508
+ return db.prepare(`DELETE FROM memory_embeddings
176509
+ WHERE rowid IN (
176510
+ SELECT me.rowid
176511
+ FROM memory_embeddings me
176512
+ JOIN memories m ON m.id = me.memory_id
176513
+ WHERE m.project_path = ? AND me.model_id = ?
176514
+ LIMIT ?
176515
+ )`).run(projectIdentity, modelId, limit).changes;
176516
+ }
176517
+ if (scope === "commit") {
176518
+ return db.prepare(`DELETE FROM git_commit_embeddings
176519
+ WHERE rowid IN (
176520
+ SELECT gce.rowid
176521
+ FROM git_commit_embeddings gce
176522
+ JOIN git_commits gc ON gc.sha = gce.sha
176523
+ WHERE gc.project_path = ? AND gce.model_id = ?
176524
+ LIMIT ?
176525
+ )`).run(projectIdentity, modelId, limit).changes;
176526
+ }
176527
+ return db.prepare(`DELETE FROM compartment_chunk_embeddings
176528
+ WHERE id IN (
176529
+ SELECT id
176530
+ FROM compartment_chunk_embeddings
176531
+ WHERE project_path = ? AND model_id = ?
176532
+ LIMIT ?
176533
+ )`).run(projectIdentity, modelId, limit).changes;
176534
+ }
176535
+ function hasStaleEmbeddingRows(db, scope, projectIdentity, modelId) {
176536
+ if (scope === "memory") {
176537
+ return Boolean(db.prepare(`SELECT 1
176538
+ FROM memory_embeddings me
176539
+ JOIN memories m ON m.id = me.memory_id
176540
+ WHERE m.project_path = ? AND me.model_id = ?
176541
+ LIMIT 1`).get(projectIdentity, modelId));
176542
+ }
176543
+ if (scope === "commit") {
176544
+ return Boolean(db.prepare(`SELECT 1
176545
+ FROM git_commit_embeddings gce
176546
+ JOIN git_commits gc ON gc.sha = gce.sha
176547
+ WHERE gc.project_path = ? AND gce.model_id = ?
176548
+ LIMIT 1`).get(projectIdentity, modelId));
176549
+ }
176550
+ return Boolean(db.prepare(`SELECT 1
176551
+ FROM compartment_chunk_embeddings
176552
+ WHERE project_path = ? AND model_id = ?
176553
+ LIMIT 1`).get(projectIdentity, modelId));
176554
+ }
176329
176555
  function sweepStaleEmbeddingIdentitiesForProject(db, projectIdentity, now = Date.now()) {
176330
176556
  const snapshot = getProjectEmbeddingSnapshot(projectIdentity);
176331
176557
  const result = {
@@ -176340,24 +176566,45 @@ function sweepStaleEmbeddingIdentitiesForProject(db, projectIdentity, now = Date
176340
176566
  return result;
176341
176567
  const cutoff = now - EMBEDDING_IDENTITY_GC_GRACE_MS;
176342
176568
  const deleteTracking = getDeleteActiveIdentityStatement(db);
176569
+ const scopes = [
176570
+ {
176571
+ scope: "memory",
176572
+ enabled: snapshot.enabled && snapshot.modelId !== "off",
176573
+ currentModelId: snapshot.modelId
176574
+ },
176575
+ {
176576
+ scope: "commit",
176577
+ enabled: snapshot.gitCommitEnabled && snapshot.modelId !== "off",
176578
+ currentModelId: snapshot.modelId
176579
+ },
176580
+ {
176581
+ scope: "chunk",
176582
+ enabled: snapshot.enabled && snapshot.chunkModelId !== "off",
176583
+ currentModelId: snapshot.chunkModelId
176584
+ }
176585
+ ];
176586
+ let remainingBudget = STALE_EMBEDDING_GC_BATCH_SIZE;
176343
176587
  db.exec("BEGIN IMMEDIATE");
176344
176588
  try {
176345
- if (snapshot.enabled && snapshot.modelId !== "off") {
176346
- for (const modelId of staleModelsForScope(db, projectIdentity, "memory", snapshot.modelId, cutoff)) {
176347
- result.memoryRowsDeleted += clearEmbeddingsForProject(db, projectIdentity, modelId);
176348
- result.trackingRowsDeleted += deleteTracking.run(projectIdentity, "memory", modelId).changes;
176349
- }
176350
- }
176351
- if (snapshot.gitCommitEnabled && snapshot.modelId !== "off") {
176352
- for (const modelId of staleModelsForScope(db, projectIdentity, "commit", snapshot.modelId, cutoff)) {
176353
- result.commitRowsDeleted += clearProjectCommitEmbeddings(db, projectIdentity, modelId);
176354
- result.trackingRowsDeleted += deleteTracking.run(projectIdentity, "commit", modelId).changes;
176355
- }
176356
- }
176357
- if (snapshot.enabled && snapshot.chunkModelId !== "off") {
176358
- for (const modelId of staleModelsForScope(db, projectIdentity, "chunk", snapshot.chunkModelId, cutoff)) {
176359
- result.chunkRowsDeleted += clearChunkEmbeddingsForProject(db, projectIdentity, modelId);
176360
- result.trackingRowsDeleted += deleteTracking.run(projectIdentity, "chunk", modelId).changes;
176589
+ for (const { scope, enabled, currentModelId } of scopes) {
176590
+ if (!enabled || remainingBudget === 0)
176591
+ continue;
176592
+ for (const modelId of staleModelsForScope(db, projectIdentity, scope, currentModelId, cutoff)) {
176593
+ if (remainingBudget === 0)
176594
+ break;
176595
+ const deleted = deleteStaleEmbeddingBatch(db, scope, projectIdentity, modelId, remainingBudget);
176596
+ remainingBudget -= deleted;
176597
+ if (scope === "memory")
176598
+ result.memoryRowsDeleted += deleted;
176599
+ else if (scope === "commit")
176600
+ result.commitRowsDeleted += deleted;
176601
+ else
176602
+ result.chunkRowsDeleted += deleted;
176603
+ if (!hasStaleEmbeddingRows(db, scope, projectIdentity, modelId)) {
176604
+ result.trackingRowsDeleted += deleteTracking.run(projectIdentity, scope, modelId).changes;
176605
+ } else if (deleted === 0) {
176606
+ remainingBudget = 0;
176607
+ }
176361
176608
  }
176362
176609
  }
176363
176610
  db.exec("COMMIT");
@@ -176367,9 +176614,8 @@ function sweepStaleEmbeddingIdentitiesForProject(db, projectIdentity, now = Date
176367
176614
  } catch {}
176368
176615
  throw error51;
176369
176616
  }
176370
- if (result.memoryRowsDeleted > 0) {
176617
+ if (result.memoryRowsDeleted > 0)
176371
176618
  invalidateProject(projectIdentity);
176372
- }
176373
176619
  return result;
176374
176620
  }
176375
176621
  function registerProjectEmbedding(db, projectIdentity, config2, features, sourceDirectory) {
@@ -176583,22 +176829,33 @@ async function drainCommitBacklogForProject(db, projectIdentity, deadline) {
176583
176829
  const snapshot = getProjectEmbeddingSnapshot(projectIdentity);
176584
176830
  if (!snapshot?.gitCommitEnabled)
176585
176831
  return 0;
176586
- const holderId = `embed-sweep-${randomUUID()}`;
176832
+ const holderId = `embed-sweep-${randomUUID2()}`;
176587
176833
  const lease = acquireGitSweepLease(db, projectIdentity, holderId, { ignoreCooldown: true });
176588
176834
  if (!lease.acquired) {
176589
176835
  return 0;
176590
176836
  }
176591
176837
  let total = 0;
176838
+ let leaseLost = false;
176839
+ const renewal = setInterval(() => {
176840
+ try {
176841
+ if (!renewGitSweepLease(db, projectIdentity, holderId))
176842
+ leaseLost = true;
176843
+ } catch {}
176844
+ }, SESSION_EMBED_LEASE_RENEWAL_MS);
176845
+ renewal.unref?.();
176592
176846
  try {
176593
- while (Date.now() < deadline && total < COMMIT_DRAIN_MAX_PER_SWEEP) {
176847
+ while (!leaseLost && Date.now() < deadline && total < COMMIT_DRAIN_MAX_PER_SWEEP) {
176594
176848
  const embedded = await embedCommitBatch(db, projectIdentity, COMMIT_DRAIN_BATCH_SIZE);
176595
- if (embedded === 0)
176849
+ if (!renewGitSweepLease(db, projectIdentity, holderId))
176850
+ leaseLost = true;
176851
+ if (leaseLost || embedded === 0)
176596
176852
  break;
176597
176853
  total += embedded;
176598
176854
  if (embedded < COMMIT_DRAIN_BATCH_SIZE)
176599
176855
  break;
176600
176856
  }
176601
176857
  } finally {
176858
+ clearInterval(renewal);
176602
176859
  releaseGitSweepLease(db, projectIdentity, holderId);
176603
176860
  }
176604
176861
  return total;
@@ -176705,13 +176962,23 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
176705
176962
  const total = countUnembeddedSessionCompartments(db, projectIdentity, sessionId, snapshot.chunkModelId);
176706
176963
  if (total === 0)
176707
176964
  return { status: "nothing", embedded: 0, total: 0 };
176708
- const holderId = `session-embed-${randomUUID()}`;
176965
+ const holderId = `session-embed-${randomUUID2()}`;
176709
176966
  const lease = acquireGitSweepLease(db, projectIdentity, holderId, { ignoreCooldown: true });
176710
176967
  if (!lease.acquired)
176711
176968
  return { status: "busy", embedded: 0, total };
176969
+ let leaseLost = false;
176970
+ const drainAbort = new AbortController;
176971
+ const forwardCallerAbort = () => drainAbort.abort();
176972
+ if (options?.signal?.aborted)
176973
+ drainAbort.abort();
176974
+ else
176975
+ options?.signal?.addEventListener("abort", forwardCallerAbort, { once: true });
176712
176976
  const renewal = setInterval(() => {
176713
176977
  try {
176714
- renewGitSweepLease(db, projectIdentity, holderId);
176978
+ if (!renewGitSweepLease(db, projectIdentity, holderId)) {
176979
+ leaseLost = true;
176980
+ drainAbort.abort();
176981
+ }
176715
176982
  } catch {}
176716
176983
  }, SESSION_EMBED_LEASE_RENEWAL_MS);
176717
176984
  renewal.unref?.();
@@ -176725,7 +176992,7 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
176725
176992
  try {
176726
176993
  options?.onProgress?.({ embedded, total });
176727
176994
  for (;; ) {
176728
- if (options?.signal?.aborted) {
176995
+ if (leaseLost || drainAbort.signal.aborted) {
176729
176996
  aborted2 = true;
176730
176997
  break;
176731
176998
  }
@@ -176736,7 +177003,13 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
176736
177003
  embedded: n,
176737
177004
  noWork,
176738
177005
  failed
176739
- } = await embedCandidateChunkBatch(db, projectIdentity, snapshot.chunkModelId, candidates, options?.signal);
177006
+ } = await embedCandidateChunkBatch(db, projectIdentity, snapshot.chunkModelId, candidates, drainAbort.signal);
177007
+ if (leaseLost || !renewGitSweepLease(db, projectIdentity, holderId)) {
177008
+ leaseLost = true;
177009
+ drainAbort.abort();
177010
+ aborted2 = true;
177011
+ break;
177012
+ }
176740
177013
  for (const id of noWork)
176741
177014
  skipIds.push(id);
176742
177015
  for (const id of failed)
@@ -176756,6 +177029,7 @@ async function embedSessionCompartmentChunks(db, projectIdentity, sessionId, opt
176756
177029
  }
176757
177030
  } finally {
176758
177031
  clearInterval(renewal);
177032
+ options?.signal?.removeEventListener("abort", forwardCallerAbort);
176759
177033
  try {
176760
177034
  releaseGitSweepLease(db, projectIdentity, holderId);
176761
177035
  } catch (error51) {
@@ -178661,13 +178935,7 @@ If no promotions are warranted, return empty arrays. Always consume reviewed can
178661
178935
  })).filter((u) => Boolean(u.memoryId) && u.content.length > 0);
178662
178936
  const dismissals = (parsed.dismiss_existing ?? []).filter((d) => Boolean(d.memory_id));
178663
178937
  const consumeCandidateIds = parsed.consume_candidate_ids ?? [];
178664
- let leaseLostAtCommit = false;
178665
- args.db.transaction(() => {
178666
- if (!peekLeaseHolderAndExpiry(args.db, args.holderId, leaseKey)) {
178667
- log(`[dreamer] user-memories: commit aborted — lease lost (holder ${args.holderId})`);
178668
- leaseLostAtCommit = true;
178669
- return;
178670
- }
178938
+ runLeaseGuardedWrite(args.db, args.holderId, leaseKey, () => {
178671
178939
  for (const promotion of promotions) {
178672
178940
  insertUserMemory(args.db, promotion.content, promotion.candidateIds);
178673
178941
  }
@@ -178683,10 +178951,7 @@ If no promotions are warranted, return empty arrays. Always consume reviewed can
178683
178951
  if (promotions.length > 0 || updates.length > 0 || dismissals.length > 0) {
178684
178952
  bumpProjectUserProfileVersion(args.db);
178685
178953
  }
178686
- })();
178687
- if (leaseLostAtCommit) {
178688
- throw new Error("Dream lease lost during user-memory review commit");
178689
- }
178954
+ });
178690
178955
  result.promoted = promotions.length;
178691
178956
  result.merged = updates.length;
178692
178957
  result.dismissed = dismissals.length;
@@ -179220,7 +179485,7 @@ init_logger();
179220
179485
  // ../plugin/src/features/magic-context/smart-notes/capabilities.ts
179221
179486
  import { execFile as execFile2 } from "node:child_process";
179222
179487
  import { constants as fsConstants } from "node:fs";
179223
- import { open as open2, realpath } from "node:fs/promises";
179488
+ import { lstat, open as open2, realpath } from "node:fs/promises";
179224
179489
  import path8 from "node:path";
179225
179490
  import { promisify as promisify2 } from "node:util";
179226
179491
 
@@ -179355,16 +179620,19 @@ async function resolveHostToValidatedGlobalAddresses(rawHostname, signal, resolv
179355
179620
  if (candidates.length === 0) {
179356
179621
  throw new SmartNoteSecurityError("DNS resolution returned no addresses");
179357
179622
  }
179358
- const classified = candidates.map((candidate) => {
179623
+ const ipv4Candidates = candidates.filter((candidate) => candidate.family !== 6 && !candidate.address.includes(":"));
179624
+ if (ipv4Candidates.length === 0) {
179625
+ throw new SmartNoteNetworkError("SMART_NOTE_NETWORK: IPv6 destinations are not permitted");
179626
+ }
179627
+ const classified = ipv4Candidates.map((candidate) => {
179359
179628
  const parsed = parseIpLiteral(candidate.address);
179360
- if (!parsed) {
179361
- throw new SmartNoteSecurityError(`DNS returned an unparsable address: ${candidate.address}`);
179629
+ if (parsed?.family !== 4) {
179630
+ throw new SmartNoteSecurityError(`DNS returned an unparsable IPv4 address: ${candidate.address}`);
179362
179631
  }
179363
- const global = isGlobalAddress(parsed);
179364
179632
  return {
179365
179633
  address: parsed.address,
179366
179634
  family: parsed.family,
179367
- global
179635
+ global: isGlobalAddress(parsed)
179368
179636
  };
179369
179637
  });
179370
179638
  if (classified.some((candidate) => !candidate.global)) {
@@ -179387,6 +179655,7 @@ function createPinnedLookup(candidate) {
179387
179655
  return hook;
179388
179656
  }
179389
179657
  function requestValidatedAddress(validation, candidate, options) {
179658
+ const agent = createSmartNoteRequestAgent();
179390
179659
  return new Promise((resolve, reject) => {
179391
179660
  const url2 = validation.url;
179392
179661
  const hostHeader = url2.host;
@@ -179403,6 +179672,7 @@ function requestValidatedAddress(validation, candidate, options) {
179403
179672
  Accept: "text/plain, application/json;q=0.9, */*;q=0.1"
179404
179673
  },
179405
179674
  lookup: createPinnedLookup(candidate),
179675
+ agent,
179406
179676
  timeout: options.timeoutMs
179407
179677
  }, (response) => {
179408
179678
  const chunks = [];
@@ -179447,7 +179717,10 @@ function requestValidatedAddress(validation, candidate, options) {
179447
179717
  });
179448
179718
  request2.on("close", () => options.signal.removeEventListener("abort", onAbort));
179449
179719
  request2.end();
179450
- });
179720
+ }).finally(() => agent.destroy());
179721
+ }
179722
+ function createSmartNoteRequestAgent() {
179723
+ return new https.Agent({ keepAlive: false, maxSockets: 1 });
179451
179724
  }
179452
179725
  function canonicalDnsName(hostname3) {
179453
179726
  const ascii = domainToASCII(hostname3);
@@ -179630,32 +179903,64 @@ function normalizeRepoPath(repoRelativePath) {
179630
179903
  }
179631
179904
  async function guardedReadFile(projectRoot, repoRelativePath, signal, fileLimitBytes) {
179632
179905
  throwIfAborted2(signal);
179906
+ const body = guardedReadFileBody(projectRoot, repoRelativePath, signal, fileLimitBytes);
179907
+ let onAbort;
179908
+ const abort = new Promise((_, reject) => {
179909
+ onAbort = () => reject(abortError(signal));
179910
+ if (signal.aborted)
179911
+ onAbort();
179912
+ else
179913
+ signal.addEventListener("abort", onAbort, { once: true });
179914
+ });
179915
+ try {
179916
+ return await Promise.race([body, abort]);
179917
+ } finally {
179918
+ if (onAbort)
179919
+ signal.removeEventListener("abort", onAbort);
179920
+ }
179921
+ }
179922
+ async function guardedReadFileBody(projectRoot, repoRelativePath, signal, fileLimitBytes) {
179633
179923
  const normalized = normalizeRepoPath(repoRelativePath);
179634
179924
  if (!normalized || isSecretDeniedPath(normalized))
179635
179925
  return null;
179636
179926
  const rootReal = await realpath(projectRoot).catch(() => null);
179927
+ throwIfAborted2(signal);
179637
179928
  if (!rootReal)
179638
179929
  return null;
179639
179930
  const target = path8.resolve(rootReal, normalized);
179640
179931
  if (!isPathInside(rootReal, target))
179641
179932
  return null;
179642
179933
  const parentReal = await realpath(path8.dirname(target)).catch(() => null);
179934
+ throwIfAborted2(signal);
179643
179935
  if (!parentReal || !isPathInside(rootReal, parentReal))
179644
179936
  return null;
179937
+ const canonicalTarget = path8.join(parentReal, path8.basename(target));
179938
+ const canonicalRelative = normalizeRepoPath(path8.relative(rootReal, canonicalTarget));
179939
+ if (!canonicalRelative || !isPathInside(rootReal, canonicalTarget) || isSecretDeniedPath(canonicalRelative)) {
179940
+ return null;
179941
+ }
179942
+ const targetStat = await lstat(canonicalTarget).catch((error51) => {
179943
+ if (isNoFollowOrMissing(error51))
179944
+ return null;
179945
+ throw error51;
179946
+ });
179947
+ throwIfAborted2(signal);
179948
+ if (!targetStat?.isFile() || targetStat.size > fileLimitBytes)
179949
+ return null;
179645
179950
  const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
179646
- const handle = await open2(target, fsConstants.O_RDONLY | noFollow).catch((error51) => {
179951
+ const nonBlock = typeof fsConstants.O_NONBLOCK === "number" ? fsConstants.O_NONBLOCK : 0;
179952
+ const openPromise = open2(canonicalTarget, fsConstants.O_RDONLY | noFollow | nonBlock).catch((error51) => {
179647
179953
  if (isNoFollowOrMissing(error51))
179648
179954
  return null;
179649
179955
  throw error51;
179650
179956
  });
179957
+ const handle = await closeLateOpenOnAbort(openPromise, signal);
179651
179958
  if (!handle)
179652
179959
  return null;
179653
179960
  try {
179654
179961
  throwIfAborted2(signal);
179655
179962
  const stat2 = await handle.stat();
179656
- if (!stat2.isFile())
179657
- return null;
179658
- if (stat2.size > fileLimitBytes)
179963
+ if (!stat2.isFile() || stat2.size > fileLimitBytes)
179659
179964
  return null;
179660
179965
  const buffer2 = Buffer.alloc(stat2.size);
179661
179966
  const { bytesRead } = await handle.read(buffer2, 0, stat2.size, 0);
@@ -179665,6 +179970,17 @@ async function guardedReadFile(projectRoot, repoRelativePath, signal, fileLimitB
179665
179970
  await handle.close().catch(() => {});
179666
179971
  }
179667
179972
  }
179973
+ async function closeLateOpenOnAbort(openPromise, signal) {
179974
+ const handle = await openPromise;
179975
+ if (!signal.aborted)
179976
+ return handle;
179977
+ if (handle)
179978
+ handle.close().catch(() => {});
179979
+ throw abortError(signal);
179980
+ }
179981
+ function abortError(signal) {
179982
+ return signal.reason instanceof SmartNoteNetworkError ? signal.reason : new SmartNoteNetworkError("SMART_NOTE_NETWORK: aborted");
179983
+ }
179668
179984
  function isPathInside(root, target) {
179669
179985
  const relative = path8.relative(root, target);
179670
179986
  return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
@@ -179787,6 +180103,8 @@ function withSandboxLock(fn) {
179787
180103
  var DEFAULT_TIMEOUT_MS = 2000;
179788
180104
  var DEFAULT_HEAP_LIMIT_BYTES = 8 * 1024 * 1024;
179789
180105
  var DEFAULT_STACK_LIMIT_BYTES = 512 * 1024;
180106
+ var MAX_COMPILED_CHECK_BYTES = 64 * 1024;
180107
+ var MAX_SANDBOX_ERROR_CHARS = 2 * 1024;
179790
180108
  function resolveCapabilitiesForRun(options, signal) {
179791
180109
  if (options.capabilityFactory) {
179792
180110
  return options.capabilityFactory(signal);
@@ -179802,18 +180120,29 @@ function throwIfRunAborted(signal) {
179802
180120
  }
179803
180121
  }
179804
180122
  async function runCompiledSmartNoteCheck(options) {
180123
+ if (options.signal?.aborted)
180124
+ return cancelledResult(options.signal.reason);
180125
+ if (Buffer.byteLength(options.compiledCheck, "utf8") > MAX_COMPILED_CHECK_BYTES) {
180126
+ return failureResult("compiled check exceeds 64 KiB", false);
180127
+ }
179805
180128
  return withSandboxLock(() => runCompiledSmartNoteCheckLocked(options));
179806
180129
  }
179807
180130
  async function runCompiledSmartNoteCheckLocked(options) {
180131
+ if (options.signal?.aborted)
180132
+ return cancelledResult(options.signal.reason);
179808
180133
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
179809
180134
  const controller = new AbortController;
179810
- const externalAbort = () => controller.abort(options.signal?.reason);
179811
- if (options.signal?.aborted) {
179812
- externalAbort();
179813
- } else {
179814
- options.signal?.addEventListener("abort", externalAbort, { once: true });
179815
- }
179816
- const timer = setTimeout(() => controller.abort(new Error("smart-note check timed out")), timeoutMs);
180135
+ let externallyCancelled = false;
180136
+ let executionTimedOut = false;
180137
+ const externalAbort = () => {
180138
+ externallyCancelled = true;
180139
+ controller.abort(options.signal?.reason);
180140
+ };
180141
+ options.signal?.addEventListener("abort", externalAbort, { once: true });
180142
+ const timer = setTimeout(() => {
180143
+ executionTimedOut = true;
180144
+ controller.abort(new Error("smart-note check timed out"));
180145
+ }, timeoutMs);
179817
180146
  try {
179818
180147
  throwIfRunAborted(controller.signal);
179819
180148
  const capabilities = resolveCapabilitiesForRun(options, controller.signal);
@@ -179830,23 +180159,38 @@ async function runCompiledSmartNoteCheckLocked(options) {
179830
180159
  const result = await evalCheck(context, options.compiledCheck);
179831
180160
  const checkResult = result;
179832
180161
  if (!checkResult || typeof checkResult.met !== "boolean") {
179833
- return { ok: false, error: "check() must return { met: boolean }", network: false };
180162
+ return failureResult("check() must return { met: boolean }", false);
179834
180163
  }
179835
180164
  return { ok: true, result: { met: checkResult.met } };
179836
180165
  } finally {
179837
180166
  context.dispose();
179838
180167
  }
179839
180168
  } catch (error51) {
179840
- return {
179841
- ok: false,
179842
- error: error51 instanceof Error ? `${error51.name}: ${error51.message}` : String(error51),
179843
- network: isSmartNoteNetworkError(error51)
179844
- };
180169
+ if (externallyCancelled && !executionTimedOut)
180170
+ return cancelledResult(error51);
180171
+ return failureResult(formatSandboxError(error51), isSmartNoteNetworkError(error51));
179845
180172
  } finally {
179846
180173
  clearTimeout(timer);
179847
180174
  options.signal?.removeEventListener("abort", externalAbort);
179848
180175
  }
179849
180176
  }
180177
+ function failureResult(error51, network) {
180178
+ return { ok: false, cancelled: false, error: truncate(error51), network };
180179
+ }
180180
+ function cancelledResult(reason) {
180181
+ return {
180182
+ ok: false,
180183
+ cancelled: true,
180184
+ error: truncate(reason instanceof Error ? reason.message : String(reason ?? "cancelled")),
180185
+ network: false
180186
+ };
180187
+ }
180188
+ function formatSandboxError(error51) {
180189
+ return error51 instanceof Error ? `${error51.name}: ${error51.message}` : String(error51);
180190
+ }
180191
+ function truncate(value) {
180192
+ return value.slice(0, MAX_SANDBOX_ERROR_CHARS);
180193
+ }
179850
180194
  function installCapabilityObject(context, cap) {
179851
180195
  const capObject = context.newObject();
179852
180196
  try {
@@ -179922,9 +180266,15 @@ JSON.stringify({ met: __result.met });`;
179922
180266
  }
179923
180267
 
179924
180268
  // ../plugin/src/features/magic-context/smart-notes/compiler.ts
180269
+ var MAX_COMPILER_OUTPUT_CHARS = 128 * 1024;
180270
+ var MAX_COMPILED_CHECK_BYTES2 = 64 * 1024;
180271
+ var MAX_MANIFEST_ENTRIES = 64;
180272
+ var MAX_CRON_CHARS = 256;
180273
+ var MAX_COMPILER_ERROR_CHARS = 2 * 1024;
179925
180274
  async function compileSmartNoteCheck(args) {
179926
- if (!args.note.surfaceCondition)
179927
- return { ok: false, error: "note has no surface condition" };
180275
+ if (!args.note.surfaceCondition) {
180276
+ return { ok: false, cancelled: false, error: "note has no surface condition" };
180277
+ }
179928
180278
  const prompt = `Compile this smart note condition into a sandbox check.
179929
180279
 
179930
180280
  Project identity: ${args.projectIdentity}
@@ -179998,6 +180348,7 @@ Remember: output only the JSON object described by the system prompt.`;
179998
180348
  const response = run.validated;
179999
180349
  const compiledCheck = normalizeCompiledCheck(response.compiled_check);
180000
180350
  const manifest = normalizeManifest(response.manifest);
180351
+ const checkCron = normalizeCron(response.check_cron);
180001
180352
  for (const warning of manifestAdvisoryWarnings(compiledCheck, manifest)) {
180002
180353
  log(`[dreamer] smart note #${args.note.id}: manifest advisory — ${warning}`);
180003
180354
  }
@@ -180008,11 +180359,15 @@ Remember: output only the JSON object described by the system prompt.`;
180008
180359
  timeoutMs: 2000
180009
180360
  });
180010
180361
  if (!dryRun.ok) {
180011
- recordInvocation2({ status: "failed", messages: run.output, error: dryRun.error });
180012
- return { ok: false, error: `dry-run failed: ${dryRun.error}` };
180362
+ const error51 = boundedError(`dry-run failed: ${dryRun.error}`);
180363
+ recordInvocation2({
180364
+ status: dryRun.cancelled ? "aborted" : "failed",
180365
+ messages: run.output,
180366
+ error: error51
180367
+ });
180368
+ return { ok: false, cancelled: dryRun.cancelled, error: error51 };
180013
180369
  }
180014
180370
  recordInvocation2({ status: "completed", messages: run.output });
180015
- const checkCron = normalizeCron(response.check_cron);
180016
180371
  return {
180017
180372
  ok: true,
180018
180373
  compiledCheck,
@@ -180022,8 +180377,10 @@ Remember: output only the JSON object described by the system prompt.`;
180022
180377
  dryRun: dryRun.result
180023
180378
  };
180024
180379
  } catch (error51) {
180025
- recordInvocation2({ status: "failed", error: error51 });
180026
- return { ok: false, error: error51 instanceof Error ? error51.message : String(error51) };
180380
+ const cancelled = args.signal.aborted;
180381
+ const message = boundedError(error51 instanceof Error ? error51.message : String(error51));
180382
+ recordInvocation2({ status: cancelled ? "aborted" : "failed", error: message });
180383
+ return { ok: false, cancelled, error: message };
180027
180384
  } finally {
180028
180385
  if (childSessionId) {
180029
180386
  await args.client.session.delete({ path: { id: childSessionId } }).catch(() => {});
@@ -180033,6 +180390,9 @@ Remember: output only the JSON object described by the system prompt.`;
180033
180390
  function parseCompilerOutput(output) {
180034
180391
  if (!output)
180035
180392
  throw new Error("smart-note compiler returned no output");
180393
+ if (output.length > MAX_COMPILER_OUTPUT_CHARS) {
180394
+ throw new Error("smart-note compiler output exceeds 128 KiB");
180395
+ }
180036
180396
  const json2 = extractJsonObject(output);
180037
180397
  const parsed = JSON.parse(json2);
180038
180398
  if (typeof parsed.compiled_check !== "string")
@@ -180044,6 +180404,9 @@ function parseCompilerOutput(output) {
180044
180404
  return parsed;
180045
180405
  }
180046
180406
  function normalizeCompiledCheck(source) {
180407
+ if (Buffer.byteLength(source, "utf8") > MAX_COMPILED_CHECK_BYTES2) {
180408
+ throw new Error("compiled_check exceeds 64 KiB");
180409
+ }
180047
180410
  let code = source.trim();
180048
180411
  const fence = code.match(/^```(?:javascript|js)?\s*([\s\S]*?)```$/i);
180049
180412
  if (fence)
@@ -180058,14 +180421,17 @@ function normalizeCompiledCheck(source) {
180058
180421
  if (/\b(?:import|require)\b/.test(code)) {
180059
180422
  throw new Error("compiled_check must not import modules");
180060
180423
  }
180424
+ if (Buffer.byteLength(code, "utf8") > MAX_COMPILED_CHECK_BYTES2) {
180425
+ throw new Error("compiled_check exceeds 64 KiB");
180426
+ }
180061
180427
  return code;
180062
180428
  }
180063
180429
  function normalizeManifest(manifest) {
180064
- const capabilities = Array.isArray(manifest.capabilities) ? unique(manifest.capabilities.filter((cap) => ["readFile", "gitHeadSha", "gitTag", "gitLog", "httpGet"].includes(String(cap)))) : [];
180430
+ const capabilities = Array.isArray(manifest.capabilities) ? unique(manifest.capabilities.slice(0, MAX_MANIFEST_ENTRIES).filter((cap) => ["readFile", "gitHeadSha", "gitTag", "gitLog", "httpGet"].includes(String(cap)))) : [];
180065
180431
  return {
180066
180432
  capabilities,
180067
180433
  readFiles: uniqueStrings(manifest.readFiles),
180068
- hosts: uniqueStrings(manifest.hosts?.map((h) => h.toLowerCase())),
180434
+ hosts: uniqueStrings(manifest.hosts, (host) => host.toLowerCase()),
180069
180435
  urls: uniqueStrings(manifest.urls),
180070
180436
  signals: uniqueStrings(manifest.signals),
180071
180437
  summary: typeof manifest.summary === "string" ? manifest.summary.slice(0, 160) : undefined
@@ -180131,25 +180497,36 @@ function literalCalls(code, method) {
180131
180497
  return values;
180132
180498
  }
180133
180499
  function normalizeCron(cron) {
180134
- const trimmed = cron.trim();
180135
- return trimmed.length > 0 ? trimmed : "0 * * * *";
180500
+ if (cron.length > MAX_CRON_CHARS)
180501
+ throw new Error("check_cron exceeds 256 characters");
180502
+ const normalized = cron.trim() || "0 * * * *";
180503
+ const parsed = parseCron(normalized);
180504
+ if (!parsed.ok)
180505
+ throw new Error(`invalid check_cron: ${parsed.error}`);
180506
+ const next = nextOccurrence(parsed.cron, new Date, undefined, SMART_NOTE_CHECK_CEILING_MS);
180507
+ if (!next)
180508
+ throw new Error("check_cron has no occurrence within the scheduling ceiling");
180509
+ return normalized;
180136
180510
  }
180137
180511
  function unique(items) {
180138
180512
  return [...new Set(items)];
180139
180513
  }
180140
- function uniqueStrings(items) {
180514
+ function uniqueStrings(items, normalize = (value) => value) {
180141
180515
  if (!Array.isArray(items))
180142
180516
  return;
180143
- const values = unique(items.filter((item) => typeof item === "string" && item.length > 0));
180517
+ const values = unique(items.slice(0, MAX_MANIFEST_ENTRIES).filter((item) => typeof item === "string" && item.length > 0).map(normalize));
180144
180518
  return values.length > 0 ? values : undefined;
180145
180519
  }
180520
+ function boundedError(error51) {
180521
+ return error51.slice(0, MAX_COMPILER_ERROR_CHARS);
180522
+ }
180146
180523
 
180147
180524
  // ../plugin/src/features/magic-context/smart-notes/schedule.ts
180148
180525
  function nextSmartNoteCheckDueAt(cron, options = {}) {
180149
180526
  const now = options.now ?? Date.now();
180150
180527
  const floorMs = options.floorMs ?? SMART_NOTE_CHECK_FLOOR_MS;
180151
180528
  const ceilingMs = options.ceilingMs ?? SMART_NOTE_CHECK_CEILING_MS;
180152
- const rawNext = cron?.trim() ? nextDueAtMs(cron, now) : null;
180529
+ const rawNext = cron?.trim() ? nextDueAtMs(cron, now, undefined, ceilingMs) : null;
180153
180530
  const rawDelta = rawNext ? rawNext - now : SMART_NOTE_CHECK_DEFAULT_INTERVAL_MS;
180154
180531
  const clamped = Math.min(ceilingMs, Math.max(floorMs, rawDelta));
180155
180532
  const jittered = clamped + deterministicJitterMs(clamped, options.noteId, options.hash);
@@ -180171,6 +180548,7 @@ function deterministicJitterMs(intervalMs, noteId, hash2) {
180171
180548
  }
180172
180549
 
180173
180550
  // ../plugin/src/features/magic-context/smart-notes/storage.ts
180551
+ init_logger();
180174
180552
  function toSmartNote(note) {
180175
180553
  return {
180176
180554
  ...note,
@@ -180183,11 +180561,13 @@ function toSmartNote(note) {
180183
180561
  function commitSmartNoteState(db, args) {
180184
180562
  db.exec("BEGIN IMMEDIATE");
180185
180563
  let leaseLost = false;
180564
+ let committed = false;
180186
180565
  try {
180187
180566
  if (args.leaseHeld && !args.leaseHeld()) {
180188
180567
  leaseLost = true;
180189
- } else {
180568
+ } else if (claimExpectedState(db, args.expected)) {
180190
180569
  args.write();
180570
+ committed = true;
180191
180571
  }
180192
180572
  db.exec("COMMIT");
180193
180573
  } catch (error51) {
@@ -180199,6 +180579,30 @@ function commitSmartNoteState(db, args) {
180199
180579
  if (leaseLost) {
180200
180580
  throw new Error(`Dream lease lost during smart-note ${args.phase} commit`);
180201
180581
  }
180582
+ if (!committed) {
180583
+ log(`[debug] smart note #${args.expected.noteId}: discarded stale ${args.phase} result`);
180584
+ }
180585
+ return committed;
180586
+ }
180587
+ function claimExpectedState(db, expected) {
180588
+ if (expected.kind === "compiled-check") {
180589
+ return db.prepare(`UPDATE notes SET id = id
180590
+ WHERE id = ? AND type = 'smart' AND status = 'pending'
180591
+ AND check_status = 'compiled' AND compiled_check = ?
180592
+ AND check_hash IS ? AND check_compiled_at IS ?`).run(expected.noteId, expected.compiledCheck, expected.checkHash, expected.checkCompiledAt).changes > 0;
180593
+ }
180594
+ const statusClause = expected.checkStatus ? " AND check_status = ?" : "";
180595
+ const params = [
180596
+ expected.noteId,
180597
+ expected.content,
180598
+ expected.surfaceCondition,
180599
+ expected.updatedAt
180600
+ ];
180601
+ if (expected.checkStatus)
180602
+ params.push(expected.checkStatus);
180603
+ return db.prepare(`UPDATE notes SET id = id
180604
+ WHERE id = ? AND type = 'smart' AND status = 'pending'
180605
+ AND content = ? AND surface_condition IS ? AND updated_at = ?${statusClause}`).run(...params).changes > 0;
180202
180606
  }
180203
180607
  function getDueCompiledSmartNoteChecks(db, projectPath, now, limit) {
180204
180608
  return getPendingSmartNotes(db, projectPath).map(toSmartNote).filter((note) => note.checkStatus === "compiled" && note.compiledCheck !== null && note.policyVersion === SMART_NOTE_CHECK_POLICY_VERSION && (note.checkQuarantinedUntil === null || note.checkQuarantinedUntil <= now) && (note.checkNextDueAt === null || note.checkNextDueAt <= now)).sort((a, b) => (a.checkNextDueAt ?? 0) - (b.checkNextDueAt ?? 0) || a.id - b.id).slice(0, Math.max(1, limit));
@@ -180316,13 +180720,19 @@ async function runDueCompiledSmartNoteChecks(args) {
180316
180720
  break;
180317
180721
  if (!note.compiledCheck)
180318
180722
  continue;
180723
+ const compiledCheck = note.compiledCheck;
180319
180724
  ran++;
180320
180725
  const controller = new AbortController;
180726
+ const abortFromCaller = () => controller.abort(args.signal?.reason);
180727
+ if (args.signal?.aborted)
180728
+ abortFromCaller();
180729
+ else
180730
+ args.signal?.addEventListener("abort", abortFromCaller, { once: true });
180321
180731
  const remaining = Math.max(500, (args.sweepBudgetMs ?? DEFAULT_SWEEP_BUDGET_MS) - (Date.now() - startedAt));
180322
180732
  const timer = setTimeout(() => controller.abort(new Error("smart-note sweep budget exhausted")), remaining);
180323
180733
  try {
180324
180734
  const result = await runCompiledSmartNoteCheck({
180325
- compiledCheck: note.compiledCheck,
180735
+ compiledCheck,
180326
180736
  capabilityFactory: (signal) => createSmartNoteCapabilities({
180327
180737
  projectRoot: args.projectRoot,
180328
180738
  signal
@@ -180331,48 +180741,67 @@ async function runDueCompiledSmartNoteChecks(args) {
180331
180741
  timeoutMs: Math.min(2000, remaining)
180332
180742
  });
180333
180743
  const runFinishedAt = Date.now();
180744
+ const expected = {
180745
+ kind: "compiled-check",
180746
+ noteId: note.id,
180747
+ compiledCheck,
180748
+ checkHash: note.checkHash,
180749
+ checkCompiledAt: note.checkCompiledAt
180750
+ };
180751
+ if (!result.ok && result.cancelled) {
180752
+ continue;
180753
+ }
180334
180754
  if (result.ok && result.result.met) {
180335
- commitSmartNoteState(args.db, {
180755
+ const committed = commitSmartNoteState(args.db, {
180336
180756
  phase: "due check",
180757
+ expected,
180337
180758
  leaseHeld,
180338
180759
  write: () => {
180339
180760
  markNoteReady(args.db, note.id, hostGeneratedReadyReason(note.id, note.manifestJson));
180340
180761
  }
180341
180762
  });
180342
- surfaced++;
180763
+ if (committed)
180764
+ surfaced++;
180343
180765
  } else if (result.ok) {
180766
+ const nextDueAt = nextSmartNoteCheckDueAt(note.checkCron, {
180767
+ now: runFinishedAt,
180768
+ noteId: note.id,
180769
+ hash: note.checkHash
180770
+ });
180344
180771
  commitSmartNoteState(args.db, {
180345
180772
  phase: "due check",
180773
+ expected,
180346
180774
  leaseHeld,
180347
180775
  write: () => {
180348
- markCompiledCheckFalse(args.db, note.id, nextSmartNoteCheckDueAt(note.checkCron, {
180349
- now: runFinishedAt,
180350
- noteId: note.id,
180351
- hash: note.checkHash
180352
- }), runFinishedAt);
180776
+ markCompiledCheckFalse(args.db, note.id, nextDueAt, runFinishedAt);
180353
180777
  }
180354
180778
  });
180355
180779
  } else if (result.network) {
180356
- commitSmartNoteState(args.db, {
180780
+ const committed = commitSmartNoteState(args.db, {
180357
180781
  phase: "network failure",
180782
+ expected,
180358
180783
  leaseHeld,
180359
180784
  write: () => {
180360
180785
  markCompiledCheckNetworkFailure(args.db, note.id, runFinishedAt, MAX_FAILURES_BEFORE_REAUTHOR);
180361
180786
  }
180362
180787
  });
180363
- networkFailed++;
180788
+ if (committed)
180789
+ networkFailed++;
180364
180790
  } else {
180365
- commitSmartNoteState(args.db, {
180791
+ const committed = commitSmartNoteState(args.db, {
180366
180792
  phase: "logic failure",
180793
+ expected,
180367
180794
  leaseHeld,
180368
180795
  write: () => {
180369
180796
  markCompiledCheckLogicFailure(args.db, note.id, runFinishedAt, MAX_FAILURES_BEFORE_REAUTHOR);
180370
180797
  }
180371
180798
  });
180372
- failed++;
180799
+ if (committed)
180800
+ failed++;
180373
180801
  }
180374
180802
  } finally {
180375
180803
  clearTimeout(timer);
180804
+ args.signal?.removeEventListener("abort", abortFromCaller);
180376
180805
  }
180377
180806
  }
180378
180807
  return { ran, surfaced, failed, networkFailed };
@@ -180437,7 +180866,8 @@ async function evaluateSmartNotes(args) {
180437
180866
  projectRoot,
180438
180867
  maxChecks: 10,
180439
180868
  sweepBudgetMs: 1e4,
180440
- leaseHeld
180869
+ leaseHeld,
180870
+ signal: leaseAbortController.signal
180441
180871
  });
180442
180872
  surfaced += dueRun.surfaced;
180443
180873
  didWork ||= dueRun.ran > 0;
@@ -180457,7 +180887,7 @@ async function evaluateSmartNotes(args) {
180457
180887
  break;
180458
180888
  assertLeaseHeld("liveness start");
180459
180889
  didWork = true;
180460
- const met = await runLivenessCheck(args, note, projectRoot, assertLeaseHeld, leaseHeld);
180890
+ const met = await runLivenessCheck(args, note, projectRoot, assertLeaseHeld, leaseHeld, leaseAbortController.signal);
180461
180891
  if (met)
180462
180892
  surfaced += 1;
180463
180893
  }
@@ -180470,8 +180900,9 @@ async function evaluateSmartNotes(args) {
180470
180900
  const met = await confirmReadOnly(args, note.id, note.content, note.surfaceCondition, leaseAbortController.signal);
180471
180901
  const now = Date.now();
180472
180902
  assertLeaseHeld("fallback commit");
180473
- commitSmartNoteState(args.db, {
180903
+ const committed = commitSmartNoteState(args.db, {
180474
180904
  phase: "fallback",
180905
+ expected: sourceRevisionExpectation(note, "fallback"),
180475
180906
  leaseHeld,
180476
180907
  write: () => {
180477
180908
  if (met) {
@@ -180482,7 +180913,7 @@ async function evaluateSmartNotes(args) {
180482
180913
  }
180483
180914
  }
180484
180915
  });
180485
- if (met)
180916
+ if (met && committed)
180486
180917
  surfaced += 1;
180487
180918
  }
180488
180919
  assertLeaseHeld("final commit");
@@ -180512,9 +180943,12 @@ async function compileNote(args, note, projectRoot, assertLeaseHeld, leaseHeld,
180512
180943
  const now = Date.now();
180513
180944
  assertLeaseHeld("compile commit");
180514
180945
  if (!result.ok) {
180946
+ if (result.cancelled)
180947
+ return false;
180515
180948
  log(`[dreamer] smart note #${note.id}: compile failed — ${result.error}`);
180516
180949
  commitSmartNoteState(args.db, {
180517
180950
  phase: "compile failure",
180951
+ expected: sourceRevisionExpectation(note),
180518
180952
  leaseHeld,
180519
180953
  write: () => {
180520
180954
  markSmartNoteCompilationFailure(args.db, note.id, now, MAX_COMPILATION_FAILURES);
@@ -180527,8 +180961,9 @@ async function compileNote(args, note, projectRoot, assertLeaseHeld, leaseHeld,
180527
180961
  noteId: note.id,
180528
180962
  hash: result.checkHash
180529
180963
  });
180530
- commitSmartNoteState(args.db, {
180964
+ const committed = commitSmartNoteState(args.db, {
180531
180965
  phase: "compile",
180966
+ expected: sourceRevisionExpectation(note),
180532
180967
  leaseHeld,
180533
180968
  write: () => {
180534
180969
  storeCompiledSmartNoteCheck(args.db, {
@@ -180547,47 +180982,65 @@ async function compileNote(args, note, projectRoot, assertLeaseHeld, leaseHeld,
180547
180982
  }
180548
180983
  }
180549
180984
  });
180550
- return result.dryRun.met;
180985
+ return result.dryRun.met && committed;
180551
180986
  } finally {
180552
180987
  promptSignal.cleanup();
180553
180988
  }
180554
180989
  }
180555
- async function runLivenessCheck(args, note, projectRoot, assertLeaseHeld, leaseHeld) {
180990
+ async function runLivenessCheck(args, note, projectRoot, assertLeaseHeld, leaseHeld, leaseSignal) {
180556
180991
  if (!note.compiledCheck)
180557
180992
  return false;
180558
- const controller = new AbortController;
180559
- const timer = setTimeout(() => controller.abort(new Error("smart-note liveness timeout")), 2000);
180560
- try {
180561
- const result = await runCompiledSmartNoteCheck({
180562
- compiledCheck: note.compiledCheck,
180563
- capabilityFactory: (signal) => createSmartNoteCapabilities({ projectRoot, signal }),
180564
- signal: controller.signal,
180565
- timeoutMs: 2000
180566
- });
180567
- const now = Date.now();
180568
- assertLeaseHeld("liveness commit");
180569
- commitSmartNoteState(args.db, {
180570
- phase: "liveness",
180571
- leaseHeld,
180572
- write: () => {
180573
- markSmartNoteLivenessChecked(args.db, note.id, now);
180574
- if (result.ok && result.result.met) {
180575
- markNoteReady(args.db, note.id, `Smart note #${note.id}: max-staleness liveness check returned met=true`);
180576
- } else if (result.ok) {
180577
- markCompiledCheckFalse(args.db, note.id, nextSmartNoteCheckDueAt(note.checkCron, {
180578
- now,
180579
- noteId: note.id,
180580
- hash: note.checkHash
180581
- }), now);
180582
- } else if (!result.network) {
180583
- markSmartNoteCheckStatus(args.db, note.id, "failing", now);
180584
- }
180993
+ const compiledCheck = note.compiledCheck;
180994
+ const result = await runCompiledSmartNoteCheck({
180995
+ compiledCheck,
180996
+ capabilityFactory: (signal) => createSmartNoteCapabilities({ projectRoot, signal }),
180997
+ signal: leaseSignal,
180998
+ timeoutMs: 2000
180999
+ });
181000
+ if (!result.ok && result.cancelled)
181001
+ return false;
181002
+ const now = Date.now();
181003
+ const nextDueAt = result.ok && !result.result.met ? nextSmartNoteCheckDueAt(note.checkCron, {
181004
+ now,
181005
+ noteId: note.id,
181006
+ hash: note.checkHash
181007
+ }) : null;
181008
+ assertLeaseHeld("liveness commit");
181009
+ const committed = commitSmartNoteState(args.db, {
181010
+ phase: "liveness",
181011
+ expected: compiledCheckExpectation(note, compiledCheck),
181012
+ leaseHeld,
181013
+ write: () => {
181014
+ markSmartNoteLivenessChecked(args.db, note.id, now);
181015
+ if (result.ok && result.result.met) {
181016
+ markNoteReady(args.db, note.id, `Smart note #${note.id}: max-staleness liveness check returned met=true`);
181017
+ } else if (result.ok && nextDueAt !== null) {
181018
+ markCompiledCheckFalse(args.db, note.id, nextDueAt, now);
181019
+ } else if (!result.ok && !result.network) {
181020
+ markSmartNoteCheckStatus(args.db, note.id, "failing", now);
180585
181021
  }
180586
- });
180587
- return result.ok && result.result.met;
180588
- } finally {
180589
- clearTimeout(timer);
180590
- }
181022
+ }
181023
+ });
181024
+ return result.ok && result.result.met && committed;
181025
+ }
181026
+ function sourceRevisionExpectation(note, checkStatus) {
181027
+ return {
181028
+ kind: "source-revision",
181029
+ noteId: note.id,
181030
+ content: note.content,
181031
+ surfaceCondition: note.surfaceCondition,
181032
+ updatedAt: note.updatedAt,
181033
+ ...checkStatus ? { checkStatus } : {}
181034
+ };
181035
+ }
181036
+ function compiledCheckExpectation(note, compiledCheck) {
181037
+ return {
181038
+ kind: "compiled-check",
181039
+ noteId: note.id,
181040
+ compiledCheck,
181041
+ checkHash: note.checkHash,
181042
+ checkCompiledAt: note.checkCompiledAt
181043
+ };
180591
181044
  }
180592
181045
  async function confirmReadOnly(args, noteId, content, surfaceCondition, leaseSignal) {
180593
181046
  let childSessionId = null;
@@ -186664,7 +187117,8 @@ var canonicalReasons = new Set([
186664
187117
  "pressure_refold",
186665
187118
  "upgrade_state",
186666
187119
  "cached_m1_missing",
186667
- "project_change"
187120
+ "project_change",
187121
+ "compartment_render_epoch"
186668
187122
  ]);
186669
187123
  var piReasonAliases = {
186670
187124
  project_memory_change: "project_memory_epoch",
@@ -188914,6 +189368,30 @@ class BoundedSessionMap {
188914
189368
  // ../plugin/src/hooks/magic-context/inject-compartments.ts
188915
189369
  init_logger();
188916
189370
 
189371
+ // ../plugin/src/hooks/magic-context/compartment-render-epoch.ts
189372
+ var COMPARTMENT_RENDER_EPOCH = "cre2";
189373
+ var EPOCH_COMPONENT_PREFIX = "|compartment-render:";
189374
+ function encodeCachedM0UpgradeIdentity(upgradeState, compartmentRenderEpoch = COMPARTMENT_RENDER_EPOCH) {
189375
+ if (compartmentRenderEpoch === null)
189376
+ return upgradeState;
189377
+ return `${upgradeState ?? ""}${EPOCH_COMPONENT_PREFIX}${compartmentRenderEpoch}`;
189378
+ }
189379
+ function decodeCachedM0UpgradeIdentity(value) {
189380
+ if (value === null) {
189381
+ return { upgradeState: null, compartmentRenderEpoch: null };
189382
+ }
189383
+ const componentIndex = value.lastIndexOf(EPOCH_COMPONENT_PREFIX);
189384
+ if (componentIndex < 0) {
189385
+ return { upgradeState: value, compartmentRenderEpoch: null };
189386
+ }
189387
+ const upgradeState = value.slice(0, componentIndex);
189388
+ const compartmentRenderEpoch = value.slice(componentIndex + EPOCH_COMPONENT_PREFIX.length);
189389
+ return {
189390
+ upgradeState: upgradeState.length > 0 ? upgradeState : null,
189391
+ compartmentRenderEpoch: compartmentRenderEpoch.length > 0 ? compartmentRenderEpoch : null
189392
+ };
189393
+ }
189394
+
188917
189395
  // ../plugin/src/hooks/magic-context/decay-curve.ts
188918
189396
  var H50 = 24;
188919
189397
  var D = 25;
@@ -188983,10 +189461,13 @@ function formatDateRange(startDate, endDate) {
188983
189461
  return `${startDate}→${endDate.slice(8)}`;
188984
189462
  return `${startDate}→${endDate}`;
188985
189463
  }
189464
+ function sanitizeCompartmentTitle(title) {
189465
+ return escapeXmlContent2(title.replace(/[\p{Cc}\p{Zl}\p{Zp}]+/gu, " "));
189466
+ }
188986
189467
  function compartmentHeading(c) {
188987
189468
  const dateRange = formatDateRange(c.startDate, c.endDate);
188988
189469
  const dateSegment = dateRange ? ` · ${dateRange}` : "";
188989
- return `## ${c.startMessage}-${c.endMessage}${dateSegment} · ${c.title}`;
189470
+ return `## ${c.startMessage}-${c.endMessage}${dateSegment} · ${sanitizeCompartmentTitle(c.title)}`;
188990
189471
  }
188991
189472
  function guardCompartmentBody(body) {
188992
189473
  return body.replace(/^## /gm, " ## ");
@@ -191157,7 +191638,7 @@ var MAX_FRAGMENTS = 3;
191157
191638
  var FRAGMENT_CHAR_CAP = 80;
191158
191639
  var MAX_HINT_CHARS = 800;
191159
191640
  var MS_PER_DAY2 = 24 * 60 * 60 * 1000;
191160
- function truncate(text, limit) {
191641
+ function truncate2(text, limit) {
191161
191642
  const normalized = text.replace(/\s+/g, " ").trim();
191162
191643
  if (normalized.length <= limit)
191163
191644
  return normalized;
@@ -191186,29 +191667,29 @@ function renderFragment(result, charCap) {
191186
191667
  switch (result.source) {
191187
191668
  case "memory": {
191188
191669
  const compressed = cavemanCompress(result.content, "ultra");
191189
- return truncate(compressed, charCap);
191670
+ return truncate2(compressed, charCap);
191190
191671
  }
191191
191672
  case "git_commit": {
191192
191673
  const subject = result.content.split(/\r?\n/)[0] ?? result.content;
191193
- const body = truncate(subject, Math.max(10, charCap - 20));
191674
+ const body = truncate2(subject, Math.max(10, charCap - 20));
191194
191675
  return `commit ${result.shortSha} ${formatAge(result.committedAtMs)}: ${body}`;
191195
191676
  }
191196
191677
  case "message": {
191197
191678
  const compressed = cavemanCompress(result.content, "ultra");
191198
- return truncate(compressed, charCap);
191679
+ return truncate2(compressed, charCap);
191199
191680
  }
191200
191681
  case "compartment": {
191201
191682
  const source = result.snippet ?? result.title;
191202
191683
  const compressed = cavemanCompress(source, "ultra");
191203
- return truncate(compressed, charCap);
191684
+ return truncate2(compressed, charCap);
191204
191685
  }
191205
191686
  case "primer": {
191206
191687
  const compressed = cavemanCompress(result.content, "ultra");
191207
- return truncate(compressed, charCap);
191688
+ return truncate2(compressed, charCap);
191208
191689
  }
191209
191690
  case "note": {
191210
191691
  const compressed = cavemanCompress(result.content, "ultra");
191211
- return truncate(compressed, charCap);
191692
+ return truncate2(compressed, charCap);
191212
191693
  }
191213
191694
  }
191214
191695
  }
@@ -191814,8 +192295,8 @@ function resolveToolTier(toolName) {
191814
192295
  function tagReclaimBytes(tag) {
191815
192296
  return tag.byteSize + tag.inputByteSize + tag.reasoningByteSize;
191816
192297
  }
191817
- function bytesToTokens(bytes) {
191818
- return Math.round(bytes * TOKENS_PER_BYTE);
192298
+ function estimateEmergencyDropReclaimTokens(tag) {
192299
+ return Math.round(tagReclaimBytes(tag) * TOKENS_PER_BYTE);
191819
192300
  }
191820
192301
  function planEmergencyDrop(input) {
191821
192302
  const {
@@ -191847,7 +192328,7 @@ function planEmergencyDrop(input) {
191847
192328
  for (const tag of floorTags) {
191848
192329
  if (tag.status !== "active")
191849
192330
  continue;
191850
- tailTokens += bytesToTokens(tagReclaimBytes(tag));
192331
+ tailTokens += estimateEmergencyDropReclaimTokens(tag);
191851
192332
  }
191852
192333
  const fixedFloor = Math.max(currentTotalInputTokens - tailTokens, 0);
191853
192334
  const workingSpan = Math.max(ceilingTokens - fixedFloor, 0);
@@ -191895,7 +192376,7 @@ function planEmergencyDrop(input) {
191895
192376
  group.sort((a, b) => a.tagNumber - b.tagNumber);
191896
192377
  for (const tag of group) {
191897
192378
  selected.push(tag.tagNumber);
191898
- reclaimed += bytesToTokens(tagReclaimBytes(tag));
192379
+ reclaimed += estimateEmergencyDropReclaimTokens(tag);
191899
192380
  if (reclaimed >= reclaimTokens)
191900
192381
  break outer;
191901
192382
  }
@@ -192484,6 +192965,7 @@ function getCachedMarkers(db, state, compartmentsForNormalization) {
192484
192965
  return null;
192485
192966
  }
192486
192967
  const compartments = compartmentsForNormalization ?? getCompartments(db, state.sessionId);
192968
+ const cachedUpgradeIdentity = decodeCachedM0UpgradeIdentity(meta3.cachedM0UpgradeState);
192487
192969
  const maxCompartmentSeq = normalizeCachedMaxCompartmentSeq(meta3.cachedM0MaxCompartmentSeq, compartments);
192488
192970
  const cachedBoundary = getCachedBoundary(db, state.sessionId);
192489
192971
  const liveBoundary = lastBaselineEndMessageId(compartments);
@@ -192501,7 +192983,8 @@ function getCachedMarkers(db, state, compartmentsForNormalization) {
192501
192983
  projectDocsHash: meta3.cachedM0ProjectDocsHash,
192502
192984
  sessionFactsVersion: meta3.cachedM0SessionFactsVersion,
192503
192985
  materializedAt: meta3.cachedM0MaterializedAt,
192504
- upgradeState: meta3.cachedM0UpgradeState,
192986
+ upgradeState: cachedUpgradeIdentity.upgradeState ?? "",
192987
+ compartmentRenderEpoch: cachedUpgradeIdentity.compartmentRenderEpoch,
192505
192988
  lastBaselineEndMessageId: cachedBoundary,
192506
192989
  systemHash: meta3.cachedM0SystemHash ?? "",
192507
192990
  modelKey: meta3.cachedM0ModelKey ?? "",
@@ -192533,6 +193016,7 @@ function readCurrentMarkersFromCompartments(db, state, compartments, projectDocs
192533
193016
  sessionFactsVersion: getSessionFactsVersion(db, state.sessionId),
192534
193017
  materializedAt: Date.now(),
192535
193018
  upgradeState: `${PI_M0_UPGRADE_STATE}:${compartments.some((c) => c.legacy === 1) ? "legacy" : "ready"}`,
193019
+ compartmentRenderEpoch: COMPARTMENT_RENDER_EPOCH,
192536
193020
  lastBaselineEndMessageId: lastBaselineEndMessageId(compartments),
192537
193021
  systemHash: (state.hardSignals ?? EMPTY_PI_HARD_SIGNALS).systemHash,
192538
193022
  modelKey: (state.hardSignals ?? EMPTY_PI_HARD_SIGNALS).modelKey,
@@ -192550,9 +193034,13 @@ function mustMaterializePi(state, db, currentCompartmentsOverride) {
192550
193034
  if (!decodeCachedM0(meta3.cachedM0Bytes)) {
192551
193035
  return { value: true, reason: "cache_invalid" };
192552
193036
  }
192553
- if (getCachedMarkers(db, state, currentCompartments) === null) {
193037
+ const cached2 = getCachedMarkers(db, state, currentCompartments);
193038
+ if (cached2 === null) {
192554
193039
  return { value: true, reason: "cache_invalid" };
192555
193040
  }
193041
+ if (cached2.compartmentRenderEpoch !== current.compartmentRenderEpoch) {
193042
+ return { value: true, reason: "compartment_render_epoch" };
193043
+ }
192556
193044
  const hard = state.hardSignals ?? EMPTY_PI_HARD_SIGNALS;
192557
193045
  if (hard.modelKey !== "" && hard.modelKey !== (meta3.cachedM0ModelKey ?? "")) {
192558
193046
  return { value: true, reason: "model_change" };
@@ -192566,7 +193054,7 @@ function mustMaterializePi(state, db, currentCompartmentsOverride) {
192566
193054
  if (hard.cacheExpired && hard.lastResponseTime > 0 && hard.lastResponseTime > (meta3.cachedM0MaterializedAt ?? 0)) {
192567
193055
  return { value: true, reason: "ttl_idle" };
192568
193056
  }
192569
- if (meta3.cachedM0UpgradeState !== current.upgradeState) {
193057
+ if (cached2.upgradeState !== current.upgradeState) {
192570
193058
  return { value: true, reason: "renderer_upgrade" };
192571
193059
  }
192572
193060
  if (current.workspaceFingerprint !== null || (meta3.cachedM0WorkspaceFingerprint ?? null) !== null) {
@@ -192679,6 +193167,7 @@ function readFrozenM0InputsPi(state, db, docs = readProjectDocsForPiM0(state), m
192679
193167
  sessionFactsVersion: getSessionFactsVersion(db, state.sessionId),
192680
193168
  materializedAt: memoryCutoff ?? Date.now(),
192681
193169
  upgradeState: `${PI_M0_UPGRADE_STATE}:${compartments.some((c) => c.legacy === 1) ? "legacy" : "ready"}`,
193170
+ compartmentRenderEpoch: COMPARTMENT_RENDER_EPOCH,
192682
193171
  lastBaselineEndMessageId: lastBaselineEndMessageId(compartments),
192683
193172
  systemHash: (state.hardSignals ?? EMPTY_PI_HARD_SIGNALS).systemHash,
192684
193173
  modelKey: (state.hardSignals ?? EMPTY_PI_HARD_SIGNALS).modelKey,
@@ -192760,7 +193249,7 @@ function materializeM0Pi(state, db) {
192760
193249
  projectDocsHash: snapshotMarkers.projectDocsHash,
192761
193250
  materializedAt: snapshotMarkers.materializedAt,
192762
193251
  sessionFactsVersion: snapshotMarkers.sessionFactsVersion,
192763
- upgradeState: snapshotMarkers.upgradeState,
193252
+ upgradeState: encodeCachedM0UpgradeIdentity(snapshotMarkers.upgradeState, snapshotMarkers.compartmentRenderEpoch),
192764
193253
  systemHash: snapshotMarkers.systemHash,
192765
193254
  modelKey: snapshotMarkers.modelKey,
192766
193255
  projectIdentity: snapshotMarkers.projectIdentity
@@ -192930,6 +193419,7 @@ function readCachedPiM0M1Row(db, sessionId) {
192930
193419
  function markersFromCachedPiRow(row, compartmentsForNormalization) {
192931
193420
  if (!row.cached_m0_bytes)
192932
193421
  return null;
193422
+ const cachedUpgradeIdentity = decodeCachedM0UpgradeIdentity(row.cached_m0_upgrade_state);
192933
193423
  if (row.cached_m0_project_memory_epoch === null)
192934
193424
  return null;
192935
193425
  if (row.cached_m0_project_user_profile_version === null)
@@ -192959,7 +193449,8 @@ function markersFromCachedPiRow(row, compartmentsForNormalization) {
192959
193449
  projectDocsHash: row.cached_m0_project_docs_hash ?? "",
192960
193450
  materializedAt: row.cached_m0_materialized_at,
192961
193451
  sessionFactsVersion: row.cached_m0_session_facts_version,
192962
- upgradeState: row.cached_m0_upgrade_state,
193452
+ upgradeState: cachedUpgradeIdentity.upgradeState ?? "",
193453
+ compartmentRenderEpoch: cachedUpgradeIdentity.compartmentRenderEpoch,
192963
193454
  lastBaselineEndMessageId: typeof row.cached_m0_last_baseline_end_message_id === "string" && row.cached_m0_last_baseline_end_message_id.length > 0 ? row.cached_m0_last_baseline_end_message_id : null,
192964
193455
  systemHash: row.cached_m0_system_hash ?? "",
192965
193456
  modelKey: row.cached_m0_model_key ?? "",
@@ -192970,7 +193461,7 @@ function cachedPiRowMatchesSnapshot(args) {
192970
193461
  const rowMarkers = markersFromCachedPiRow(args.row, args.compartmentsForNormalization);
192971
193462
  if (!rowMarkers)
192972
193463
  return false;
192973
- return bufferEqualsNullable(args.row.cached_m0_bytes, args.m0Bytes) && rowMarkers.projectMemoryEpoch === args.markers.projectMemoryEpoch && rowMarkers.projectUserProfileVersion === args.markers.projectUserProfileVersion && rowMarkers.maxCompartmentSeq === args.markers.maxCompartmentSeq && rowMarkers.maxMemoryId === args.markers.maxMemoryId && rowMarkers.maxMutationId === args.markers.maxMutationId && rowMarkers.maxMemoryMutationId === args.markers.maxMemoryMutationId && rowMarkers.materializedAt === args.markers.materializedAt && rowMarkers.sessionFactsVersion === args.markers.sessionFactsVersion && (rowMarkers.upgradeState ?? null) === (args.markers.upgradeState ?? null) && (rowMarkers.systemHash ?? "") === (args.markers.systemHash ?? "") && (rowMarkers.modelKey ?? "") === (args.markers.modelKey ?? "") && (rowMarkers.projectIdentity ?? null) === (args.markers.projectIdentity ?? null) && (rowMarkers.workspaceFingerprint ?? null) === (args.markers.workspaceFingerprint ?? null);
193464
+ return bufferEqualsNullable(args.row.cached_m0_bytes, args.m0Bytes) && rowMarkers.projectMemoryEpoch === args.markers.projectMemoryEpoch && rowMarkers.projectUserProfileVersion === args.markers.projectUserProfileVersion && rowMarkers.maxCompartmentSeq === args.markers.maxCompartmentSeq && rowMarkers.maxMemoryId === args.markers.maxMemoryId && rowMarkers.maxMutationId === args.markers.maxMutationId && rowMarkers.maxMemoryMutationId === args.markers.maxMemoryMutationId && rowMarkers.materializedAt === args.markers.materializedAt && rowMarkers.sessionFactsVersion === args.markers.sessionFactsVersion && (rowMarkers.upgradeState ?? null) === (args.markers.upgradeState ?? null) && rowMarkers.compartmentRenderEpoch === args.markers.compartmentRenderEpoch && (rowMarkers.systemHash ?? "") === (args.markers.systemHash ?? "") && (rowMarkers.modelKey ?? "") === (args.markers.modelKey ?? "") && (rowMarkers.projectIdentity ?? null) === (args.markers.projectIdentity ?? null) && (rowMarkers.workspaceFingerprint ?? null) === (args.markers.workspaceFingerprint ?? null);
192974
193465
  }
192975
193466
  function adoptCachedPiProjectIdentity(db, state) {
192976
193467
  db.prepare("UPDATE session_meta SET cached_m0_project_identity = ? WHERE session_id = ? AND cached_m0_project_identity IS NULL").run(state.projectIdentity, state.sessionId);
@@ -193358,7 +193849,7 @@ var COMPARTMENT_AGENT_SYSTEM_PROMPT = `# Historian
193358
193849
 
193359
193850
  You are Historian — the hippocampus of a long-running coding agent. You and the primary agent are one mind, working together: the primary agent is doing the active engineering work, and you are the part of that mind that decides what to remember and how to store it.
193360
193851
 
193361
- You do not write for some other future reader. You write for **yourself**, later — when this same agent comes back to a topic days or weeks from now, you are the one who will read what you wrote. The wording, the structure, the importance you assign — these are all for your own future self.
193852
+ You do not write for some other future reader. You write for **yourself**, later — when this same agent comes back to a topic days or weeks from now, you are the one who will read what you wrote. The wording, the structure, the importance you assign — these are all for your own future self. So write in the **first person** — as yourself remembering what you did, never as a narrator describing "the agent" from the outside.
193362
193853
 
193363
193854
  A session can run for thousands of messages. Without you, the active prompt would grow until the agent could no longer think. You compact the past so the present can keep working.
193364
193855
 
@@ -193520,6 +194011,13 @@ When in doubt about importance, use \`<compartment_examples_from_other_projects>
193520
194011
 
193521
194012
  Each compartment contains four paraphrase tiers of the same work unit, ordered from most detailed (P1) to most condensed (P4). As described in the intro, magic-context picks ONE tier per compartment per render pass based on age and importance. Each tier must be self-contained — a future render that shows only P3 must still let a reader understand what happened.
193522
194013
 
194014
+ **Voice — write every tier in the first person.** These are your own memories. Refer to your own actions as "I" ("I traced the timeout to readLoop", "I first blamed CPU load, then corrected"), and name other actors directly — the user by name or as "the user", peers/subagents/tools by their name. Never narrate yourself in the third person as "the agent". Third-person narration ("The agent investigated X and the user corrected Y") is the single most common voice failure — it reads as a detached report about someone else, when the whole point is that *you* are the one who did this and will read it back later.
194015
+
194016
+ - Wrong (third-person report): "The agent initially blamed CPU load and proposed an epoch-drop theory. The user corrected this framing, and the agent then traced the timeout to readLoop."
194017
+ - Right (first person, actors named): "I first blamed CPU load and floated an epoch-drop theory. The user rejected that (\`U: load shouldn't cause timeouts\`), so I traced the timeout to readLoop pre-computing the deadline before the header arrived."
194018
+
194019
+ The \`U:\` line convention already keeps the user's voice distinct; your prose around it is *yours*, in the first person.
194020
+
193523
194021
  Cross-tier rules:
193524
194022
  - The compartment opening tag, \`episode_type\`, \`title\`, \`importance\`, and the facts section apply to all tiers — emit them once.
193525
194023
  - Each tier covers the same work unit; do not split a tier into a different episode.
@@ -194464,7 +194962,6 @@ function mapParsedCompartmentsToChunk(compartments, chunk, sequenceOffset) {
194464
194962
  // ../plugin/src/hooks/magic-context/compartment-runner-validation.ts
194465
194963
  var MIN_RECOMP_CHUNK_TOKEN_BUDGET = 20;
194466
194964
  function healCompartmentGaps(compartments, toolOnlyRanges = []) {
194467
- const SAFETY_HEAL_GAP = 15;
194468
194965
  for (let i = 1;i < compartments.length; i++) {
194469
194966
  const prev = compartments[i - 1];
194470
194967
  const curr = compartments[i];
@@ -194474,7 +194971,7 @@ function healCompartmentGaps(compartments, toolOnlyRanges = []) {
194474
194971
  if (gapSize <= 0)
194475
194972
  continue;
194476
194973
  const fullyInsideToolOnly = toolOnlyRanges.some((range) => range.start <= gapStart && range.end >= gapEnd);
194477
- if (fullyInsideToolOnly || gapSize <= SAFETY_HEAL_GAP) {
194974
+ if (fullyInsideToolOnly) {
194478
194975
  prev.endMessage = gapEnd;
194479
194976
  }
194480
194977
  }
@@ -197666,6 +198163,7 @@ function stripPiDroppedPlaceholderMessages(args) {
197666
198163
 
197667
198164
  // src/strip-processed-images-pi.ts
197668
198165
  init_logger();
198166
+ var STRIPPED_IMAGE_MARKER = "[image stripped]";
197669
198167
  function isLargeImagePart(value) {
197670
198168
  if (!value || typeof value !== "object")
197671
198169
  return false;
@@ -197721,7 +198219,10 @@ function stripPiProcessedImages(args) {
197721
198219
  for (let partIndex = 0;partIndex < message.content.length; partIndex++) {
197722
198220
  if (!isLargeImagePart(message.content[partIndex]))
197723
198221
  continue;
197724
- message.content[partIndex] = { type: "text", text: "" };
198222
+ message.content[partIndex] = {
198223
+ type: "text",
198224
+ text: STRIPPED_IMAGE_MARKER
198225
+ };
197725
198226
  stripped++;
197726
198227
  }
197727
198228
  }
@@ -199566,7 +200067,8 @@ function registerPiContextHandler(pi, baseOptions) {
199566
200067
  const now = Date.now();
199567
200068
  if (now - lastNotifiedAt >= EMERGENCY_NOTIFICATION_COOLDOWN_MS) {
199568
200069
  lastEmergencyNotificationAtMs.set(sessionId, now);
199569
- sessionLog(sessionId, `EMERGENCY: usage=${usagePercentage.toFixed(1)}% — awaiting in-flight historian + applying drop-all-tools`);
200070
+ sendPiIgnoredNotification(ctx, "Context full — /ctx-flush or /clear to continue.");
200071
+ sessionLog(sessionId, `EMERGENCY: usage=${usagePercentage.toFixed(1)}% — notified user, awaiting in-flight historian + applying drop-all-tools`);
199570
200072
  }
199571
200073
  const histPromise = inFlightHistorian.get(sessionId);
199572
200074
  if (histPromise) {
@@ -199930,7 +200432,10 @@ function sendPiIgnoredNotification(ctx, message) {
199930
200432
  const uiNotify = ctx.ui?.notify;
199931
200433
  if (typeof uiNotify === "function") {
199932
200434
  try {
199933
- uiNotify.call(ctx.ui, message);
200435
+ const result = uiNotify.call(ctx.ui, message);
200436
+ if (result && typeof result.then === "function") {
200437
+ Promise.resolve(result).catch((error51) => sessionLog("pi", "UI notification rejected:", error51));
200438
+ }
199934
200439
  return;
199935
200440
  } catch {}
199936
200441
  }
@@ -203471,7 +203976,7 @@ function formatThresholdPercent(value) {
203471
203976
  // package.json
203472
203977
  var package_default = {
203473
203978
  name: "@cortexkit/pi-magic-context",
203474
- version: "0.32.0",
203979
+ version: "0.32.1",
203475
203980
  type: "module",
203476
203981
  description: "Pi coding agent extension for Magic Context — cross-session memory and context management",
203477
203982
  main: "dist/index.js",
@@ -204472,7 +204977,7 @@ function roleLabel(role) {
204472
204977
  return "U (user)";
204473
204978
  return role;
204474
204979
  }
204475
- function truncate2(value, max) {
204980
+ function truncate3(value, max) {
204476
204981
  const t = value.trim();
204477
204982
  return t.length <= max ? t : `${t.slice(0, max)}…`;
204478
204983
  }
@@ -204482,10 +204987,10 @@ function keyArg(input) {
204482
204987
  for (const k of ["filePath", "path", "pattern", "query", "symbol", "module", "action"]) {
204483
204988
  const v = input[k];
204484
204989
  if (typeof v === "string" && v.length > 0)
204485
- return truncate2(v, 60);
204990
+ return truncate3(v, 60);
204486
204991
  }
204487
204992
  if (typeof input.description === "string")
204488
- return truncate2(input.description, 60);
204993
+ return truncate3(input.description, 60);
204489
204994
  return "";
204490
204995
  }
204491
204996
  function asToolPart(part) {
@@ -204541,7 +205046,7 @@ function renderPartPreview(part) {
204541
205046
  return null;
204542
205047
  const text = textOf(part);
204543
205048
  if (text !== null) {
204544
- const t = truncate2(text, 200);
205049
+ const t = truncate3(text, 200);
204545
205050
  return t.length > 0 ? ` • ${t}` : null;
204546
205051
  }
204547
205052
  const tool = asToolPart(part);
@@ -204552,7 +205057,7 @@ function renderPartPreview(part) {
204552
205057
  }
204553
205058
  const reasoning = reasoningOf(part);
204554
205059
  if (reasoning !== null)
204555
- return ` • [reasoning] ${truncate2(reasoning, 120)}`;
205060
+ return ` • [reasoning] ${truncate3(reasoning, 120)}`;
204556
205061
  const type = typeof part.type === "string" ? part.type : "part";
204557
205062
  if (type === "file")
204558
205063
  return " • [file]";
@@ -210352,18 +210857,18 @@ class TodoOverlay {
210352
210857
  const headingColor = hasInProgress ? "accent" : "dim";
210353
210858
  const headingIcon = hasInProgress ? "●" : "○";
210354
210859
  const heading = `${theme.fg(headingColor, headingIcon)} ${theme.fg(headingColor, "Todos")} ${theme.fg("muted", "—")} ${theme.fg("dim", formatCounts(counts))}`;
210355
- const truncate3 = (line) => truncateToWidth2(line, width, "…");
210356
- const lines = [truncate3(heading)];
210860
+ const truncate4 = (line) => truncateToWidth2(line, width, "…");
210861
+ const lines = [truncate4(heading)];
210357
210862
  const { visible, hiddenCount } = capTodoRows(overlayTodos);
210358
210863
  for (const [index, { todo, key }] of visible.entries()) {
210359
210864
  const isLast = index === visible.length - 1 && hiddenCount === 0;
210360
210865
  const branch = theme.fg("dim", isLast ? "└─" : "├─");
210361
- lines.push(truncate3(`${branch} ${formatTodoLine(todo, theme, { showId: true })}`));
210866
+ lines.push(truncate4(`${branch} ${formatTodoLine(todo, theme, { showId: true })}`));
210362
210867
  if (todo.status === "completed")
210363
210868
  this.completedTaskIdsPendingHide.add(key);
210364
210869
  }
210365
210870
  if (hiddenCount > 0) {
210366
- lines.push(truncate3(`${theme.fg("dim", "└─")} ${theme.fg("dim", `+${hiddenCount} more`)}`));
210871
+ lines.push(truncate4(`${theme.fg("dim", "└─")} ${theme.fg("dim", `+${hiddenCount} more`)}`));
210367
210872
  }
210368
210873
  return lines;
210369
210874
  }