@cortexkit/pi-magic-context 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16202,14 +16202,6 @@ function closeQuietly(db) {
16202
16202
  }
16203
16203
 
16204
16204
  // ../plugin/src/hooks/magic-context/read-session-db.ts
16205
- var exports_read_session_db = {};
16206
- __export(exports_read_session_db, {
16207
- withReadOnlySessionDb: () => withReadOnlySessionDb,
16208
- getRawSessionMessageCountFromDb: () => getRawSessionMessageCountFromDb,
16209
- getMessageTimesFromOpenCodeDb: () => getMessageTimesFromOpenCodeDb,
16210
- findLastAssistantModelFromOpenCodeDb: () => findLastAssistantModelFromOpenCodeDb,
16211
- closeReadOnlySessionDb: () => closeReadOnlySessionDb
16212
- });
16213
16205
  import { join as join4 } from "node:path";
16214
16206
  function getOpenCodeDbPath() {
16215
16207
  return join4(getDataDir(), "opencode", "opencode.db");
@@ -16239,9 +16231,6 @@ function getReadOnlySessionDb() {
16239
16231
  function withReadOnlySessionDb(fn) {
16240
16232
  return fn(getReadOnlySessionDb());
16241
16233
  }
16242
- function closeReadOnlySessionDb() {
16243
- closeCachedReadOnlyDb();
16244
- }
16245
16234
  function getRawSessionMessageCountFromDb(db, sessionId) {
16246
16235
  const row = db.prepare(`SELECT COUNT(*) as count FROM message WHERE session_id = ?
16247
16236
  AND NOT (COALESCE(json_extract(data, '$.summary'), 0) = 1
@@ -16267,34 +16256,6 @@ function getMessageTimesFromOpenCodeDb(sessionId, messageIds) {
16267
16256
  }
16268
16257
  return result;
16269
16258
  }
16270
- function findLastAssistantModelFromOpenCodeDb(sessionId) {
16271
- try {
16272
- return withReadOnlySessionDb((db) => {
16273
- const row = db.prepare(`SELECT json_extract(data, '$.providerID') as providerID,
16274
- json_extract(data, '$.modelID') as modelID,
16275
- json_extract(data, '$.agent') as agent
16276
- FROM message
16277
- WHERE session_id = ?
16278
- AND json_extract(data, '$.role') = 'assistant'
16279
- AND json_extract(data, '$.providerID') IS NOT NULL
16280
- AND json_extract(data, '$.modelID') IS NOT NULL
16281
- ORDER BY time_created DESC
16282
- LIMIT 1`).get(sessionId);
16283
- if (!row || typeof row.providerID !== "string" || typeof row.modelID !== "string") {
16284
- return null;
16285
- }
16286
- const agent = typeof row.agent === "string" && row.agent.length > 0 ? row.agent : undefined;
16287
- return {
16288
- providerID: row.providerID,
16289
- modelID: row.modelID,
16290
- ...agent ? { agent } : {}
16291
- };
16292
- });
16293
- } catch (error51) {
16294
- log("[magic-context] failed to recover live model from OpenCode DB:", error51);
16295
- return null;
16296
- }
16297
- }
16298
16259
  var cachedReadOnlyDb = null;
16299
16260
  var init_read_session_db = __esm(async () => {
16300
16261
  init_data_path();
@@ -16302,34 +16263,6 @@ var init_read_session_db = __esm(async () => {
16302
16263
  await init_sqlite();
16303
16264
  });
16304
16265
 
16305
- // ../plugin/src/shared/rpc-notifications.ts
16306
- var exports_rpc_notifications = {};
16307
- __export(exports_rpc_notifications, {
16308
- pushNotification: () => pushNotification,
16309
- isTuiConnected: () => isTuiConnected,
16310
- drainNotifications: () => drainNotifications
16311
- });
16312
- function pushNotification(type, payload, sessionId) {
16313
- queue2.push({ id: nextNotificationId++, type, payload, sessionId });
16314
- if (queue2.length > 100) {
16315
- queue2 = queue2.slice(-50);
16316
- }
16317
- }
16318
- function drainNotifications(lastReceivedId = 0) {
16319
- lastDrainAt = Date.now();
16320
- if (lastReceivedId > 0) {
16321
- queue2 = queue2.filter((notification) => notification.id > lastReceivedId);
16322
- }
16323
- return [...queue2];
16324
- }
16325
- function isTuiConnected() {
16326
- return lastDrainAt > 0 && Date.now() - lastDrainAt < TUI_CONNECTED_WINDOW_MS;
16327
- }
16328
- var queue2, nextNotificationId = 1, lastDrainAt = 0, TUI_CONNECTED_WINDOW_MS = 3000;
16329
- var init_rpc_notifications = __esm(() => {
16330
- queue2 = [];
16331
- });
16332
-
16333
16266
  // ../../node_modules/.bun/esprima@4.0.1/node_modules/esprima/dist/esprima.js
16334
16267
  var require_esprima = __commonJS((exports, module) => {
16335
16268
  (function webpackUniversalModuleDefinition(root, factory) {
@@ -23937,10 +23870,10 @@ var require_stringify = __commonJS((exports, module) => {
23937
23870
  replacer = null;
23938
23871
  indent = EMPTY;
23939
23872
  };
23940
- var join12 = (one, two, gap) => one ? two ? one + two.trim() + LF + gap : one.trimRight() + repeat_line_breaks(Math.max(1, count_trailing_line_breaks(one, gap)), gap) : two ? two.trimRight() + repeat_line_breaks(Math.max(1, count_trailing_line_breaks(two, gap)), gap) : EMPTY;
23873
+ var join9 = (one, two, gap) => one ? two ? one + two.trim() + LF + gap : one.trimRight() + repeat_line_breaks(Math.max(1, count_trailing_line_breaks(one, gap)), gap) : two ? two.trimRight() + repeat_line_breaks(Math.max(1, count_trailing_line_breaks(two, gap)), gap) : EMPTY;
23941
23874
  var join_content = (inside, value, gap) => {
23942
23875
  const comment = process_comments(value, PREFIX_BEFORE, gap + indent, true);
23943
- return join12(comment, inside, gap);
23876
+ return join9(comment, inside, gap);
23944
23877
  };
23945
23878
  var stringify_string = (holder, key, value) => {
23946
23879
  const raw = get_raw_string_literal(holder, key);
@@ -23962,13 +23895,13 @@ var require_stringify = __commonJS((exports, module) => {
23962
23895
  if (i !== 0) {
23963
23896
  inside += COMMA;
23964
23897
  }
23965
- const before = join12(after_comma, process_comments(value, BEFORE(i), deeper_gap), deeper_gap);
23898
+ const before = join9(after_comma, process_comments(value, BEFORE(i), deeper_gap), deeper_gap);
23966
23899
  inside += before || LF + deeper_gap;
23967
23900
  inside += stringify(i, value, deeper_gap) || STR_NULL;
23968
23901
  inside += process_comments(value, AFTER_VALUE(i), deeper_gap);
23969
23902
  after_comma = process_comments(value, AFTER(i), deeper_gap);
23970
23903
  }
23971
- inside += join12(after_comma, process_comments(value, PREFIX_AFTER, deeper_gap), deeper_gap);
23904
+ inside += join9(after_comma, process_comments(value, PREFIX_AFTER, deeper_gap), deeper_gap);
23972
23905
  return BRACKET_OPEN + join_content(inside, value, gap) + BRACKET_CLOSE;
23973
23906
  };
23974
23907
  var object_stringify = (value, gap) => {
@@ -23989,13 +23922,13 @@ var require_stringify = __commonJS((exports, module) => {
23989
23922
  inside += COMMA;
23990
23923
  }
23991
23924
  first = false;
23992
- const before = join12(after_comma, process_comments(value, BEFORE(key), deeper_gap), deeper_gap);
23925
+ const before = join9(after_comma, process_comments(value, BEFORE(key), deeper_gap), deeper_gap);
23993
23926
  inside += before || LF + deeper_gap;
23994
23927
  inside += quote(key) + process_comments(value, AFTER_PROP(key), deeper_gap) + COLON + process_comments(value, AFTER_COLON(key), deeper_gap) + SPACE + sv + process_comments(value, AFTER_VALUE(key), deeper_gap);
23995
23928
  after_comma = process_comments(value, AFTER(key), deeper_gap);
23996
23929
  };
23997
23930
  keys.forEach(iteratee);
23998
- inside += join12(after_comma, process_comments(value, PREFIX_AFTER, deeper_gap), deeper_gap);
23931
+ inside += join9(after_comma, process_comments(value, PREFIX_AFTER, deeper_gap), deeper_gap);
23999
23932
  return CURLY_BRACKET_OPEN + join_content(inside, value, gap) + CURLY_BRACKET_CLOSE;
24000
23933
  };
24001
23934
  function stringify(key, holder, gap) {
@@ -24087,10 +24020,38 @@ var require_src2 = __commonJS((exports, module) => {
24087
24020
  };
24088
24021
  });
24089
24022
 
24023
+ // ../plugin/src/shared/rpc-notifications.ts
24024
+ var exports_rpc_notifications = {};
24025
+ __export(exports_rpc_notifications, {
24026
+ pushNotification: () => pushNotification,
24027
+ isTuiConnected: () => isTuiConnected,
24028
+ drainNotifications: () => drainNotifications
24029
+ });
24030
+ function pushNotification(type, payload, sessionId) {
24031
+ queue2.push({ id: nextNotificationId++, type, payload, sessionId });
24032
+ if (queue2.length > 100) {
24033
+ queue2 = queue2.slice(-50);
24034
+ }
24035
+ }
24036
+ function drainNotifications(lastReceivedId = 0) {
24037
+ lastDrainAt = Date.now();
24038
+ if (lastReceivedId > 0) {
24039
+ queue2 = queue2.filter((notification) => notification.id > lastReceivedId);
24040
+ }
24041
+ return [...queue2];
24042
+ }
24043
+ function isTuiConnected() {
24044
+ return lastDrainAt > 0 && Date.now() - lastDrainAt < TUI_CONNECTED_WINDOW_MS;
24045
+ }
24046
+ var queue2, nextNotificationId = 1, lastDrainAt = 0, TUI_CONNECTED_WINDOW_MS = 3000;
24047
+ var init_rpc_notifications = __esm(() => {
24048
+ queue2 = [];
24049
+ });
24050
+
24090
24051
  // src/index.ts
24091
24052
  init_embedding();
24092
24053
  import { createRequire as createRequire2 } from "node:module";
24093
- import { join as join14 } from "node:path";
24054
+ import { join as join18 } from "node:path";
24094
24055
 
24095
24056
  // ../plugin/src/features/magic-context/memory/project-identity.ts
24096
24057
  import { execSync } from "node:child_process";
@@ -157818,8 +157779,8 @@ function escapeXmlContent(s) {
157818
157779
  }
157819
157780
  // ../plugin/src/features/magic-context/storage-db.ts
157820
157781
  init_data_path();
157821
- import { copyFileSync, cpSync, existsSync as existsSync3, mkdirSync as mkdirSync4 } from "node:fs";
157822
- import { join as join6 } from "node:path";
157782
+ import { copyFileSync, cpSync, existsSync as existsSync4, mkdirSync as mkdirSync4 } from "node:fs";
157783
+ import { join as join7 } from "node:path";
157823
157784
 
157824
157785
  // ../plugin/src/shared/error-message.ts
157825
157786
  function getErrorMessage(error51) {
@@ -157901,6 +157862,119 @@ function safeString(value) {
157901
157862
  init_logger();
157902
157863
  await init_sqlite();
157903
157864
 
157865
+ // ../plugin/src/features/magic-context/key-files/project-key-files.ts
157866
+ init_logger();
157867
+ import { createHash as createHash3 } from "node:crypto";
157868
+ import { existsSync as existsSync2, readFileSync, realpathSync } from "node:fs";
157869
+ import { join as join5, resolve, sep } from "node:path";
157870
+ var MISSING_CONTENT_HASH = "<missing>";
157871
+ var ORPHAN_GRACE_MS = 7 * 24 * 60 * 60 * 1000;
157872
+ function sha256(input) {
157873
+ return createHash3("sha256").update(input).digest("hex");
157874
+ }
157875
+ function resolveProjectPath(directory) {
157876
+ if (!directory?.trim())
157877
+ return null;
157878
+ try {
157879
+ return realpathSync(directory);
157880
+ } catch {
157881
+ return resolve(directory);
157882
+ }
157883
+ }
157884
+ function rowToProjectKeyFile(row) {
157885
+ return {
157886
+ projectPath: String(row.project_path),
157887
+ path: String(row.path),
157888
+ content: String(row.content),
157889
+ contentHash: String(row.content_hash),
157890
+ localTokenEstimate: Number(row.local_token_estimate),
157891
+ generatedAt: Number(row.generated_at),
157892
+ generatedByModel: row.generated_by_model == null ? null : String(row.generated_by_model),
157893
+ generationConfigHash: String(row.generation_config_hash),
157894
+ staleReason: row.stale_reason == null ? null : String(row.stale_reason)
157895
+ };
157896
+ }
157897
+ function readCurrentKeyFiles(db, projectPath) {
157898
+ const resolvedProjectPath = resolveProjectPath(projectPath) ?? projectPath;
157899
+ const rows = db.prepare(`SELECT project_path, path, content, content_hash, local_token_estimate,
157900
+ generated_at, generated_by_model, generation_config_hash, stale_reason
157901
+ FROM project_key_files
157902
+ WHERE project_path = ?
157903
+ ORDER BY generated_at DESC, path ASC`).all(resolvedProjectPath);
157904
+ return rows.map(rowToProjectKeyFile);
157905
+ }
157906
+ function getKeyFilesVersion(db, projectPath) {
157907
+ const resolvedProjectPath = resolveProjectPath(projectPath) ?? projectPath;
157908
+ const row = db.prepare("SELECT version FROM project_key_files_version WHERE project_path = ?").get(resolvedProjectPath);
157909
+ return Number(row?.version ?? 0);
157910
+ }
157911
+ function bumpKeyFilesVersion(db, projectPath) {
157912
+ const resolvedProjectPath = resolveProjectPath(projectPath) ?? projectPath;
157913
+ db.prepare(`INSERT INTO project_key_files_version (project_path, version)
157914
+ VALUES (?, 1)
157915
+ ON CONFLICT(project_path) DO UPDATE SET version = version + 1`).run(resolvedProjectPath);
157916
+ return getKeyFilesVersion(db, resolvedProjectPath);
157917
+ }
157918
+ function resolveCommitFiles(projectPath, files) {
157919
+ return files.map((file2) => {
157920
+ try {
157921
+ const disk = readFileSync(join5(projectPath, file2.path));
157922
+ return {
157923
+ ...file2,
157924
+ contentHash: sha256(disk),
157925
+ staleReason: null
157926
+ };
157927
+ } catch {
157928
+ return {
157929
+ ...file2,
157930
+ contentHash: MISSING_CONTENT_HASH,
157931
+ staleReason: "missing"
157932
+ };
157933
+ }
157934
+ });
157935
+ }
157936
+ function insertResolvedKeyFiles(db, projectPath, files, generatedAt, generatedByModel, generationConfigHash) {
157937
+ const insert = db.prepare(`INSERT INTO project_key_files
157938
+ (project_path, path, content, content_hash, local_token_estimate,
157939
+ generated_at, generated_by_model, generation_config_hash, stale_reason)
157940
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
157941
+ for (const file2 of files) {
157942
+ insert.run(projectPath, file2.path, file2.content, file2.contentHash, file2.localTokenEstimate, generatedAt, generatedByModel, generationConfigHash, file2.staleReason);
157943
+ }
157944
+ }
157945
+ function deleteOrphanProjectKeyFiles(db, now = Date.now()) {
157946
+ const cutoff = now - ORPHAN_GRACE_MS;
157947
+ const rows = db.prepare(`SELECT project_path AS projectPath, MAX(generated_at) AS lastGen
157948
+ FROM project_key_files
157949
+ GROUP BY project_path
157950
+ HAVING lastGen < ?`).all(cutoff);
157951
+ let deletedProjects = 0;
157952
+ for (const row of rows) {
157953
+ if (existsSync2(row.projectPath))
157954
+ continue;
157955
+ try {
157956
+ db.prepare("DELETE FROM project_key_files WHERE project_path = ?").run(row.projectPath);
157957
+ db.prepare("DELETE FROM project_key_files_version WHERE project_path = ?").run(row.projectPath);
157958
+ deletedProjects++;
157959
+ } catch (error51) {
157960
+ log(`[key-files] orphan GC failed for ${row.projectPath}:`, error51);
157961
+ }
157962
+ }
157963
+ return deletedProjects;
157964
+ }
157965
+ function isRelativeProjectFile(projectPath, relativePath) {
157966
+ if (!relativePath || relativePath.startsWith("/") || relativePath.includes(".."))
157967
+ return false;
157968
+ try {
157969
+ const root = realpathSync(projectPath);
157970
+ const absPath = resolve(projectPath, relativePath);
157971
+ const real = realpathSync(absPath);
157972
+ return real.startsWith(root + sep) || real === root;
157973
+ } catch {
157974
+ return false;
157975
+ }
157976
+ }
157977
+
157904
157978
  // ../plugin/src/features/magic-context/migrations.ts
157905
157979
  init_logger();
157906
157980
  var MIGRATIONS = [
@@ -158201,6 +158275,46 @@ var MIGRATIONS = [
158201
158275
  db.exec("ALTER TABLE session_meta ADD COLUMN pending_compaction_marker_state TEXT");
158202
158276
  }
158203
158277
  }
158278
+ },
158279
+ {
158280
+ version: 14,
158281
+ description: "Add project-scoped key files and version counter",
158282
+ up: (db) => {
158283
+ db.exec(`
158284
+ CREATE TABLE IF NOT EXISTS project_key_files (
158285
+ project_path TEXT NOT NULL,
158286
+ path TEXT NOT NULL,
158287
+ content TEXT NOT NULL,
158288
+ content_hash TEXT NOT NULL,
158289
+ local_token_estimate INTEGER NOT NULL,
158290
+ generated_at INTEGER NOT NULL,
158291
+ generated_by_model TEXT,
158292
+ generation_config_hash TEXT NOT NULL,
158293
+ stale_reason TEXT,
158294
+ PRIMARY KEY (project_path, path)
158295
+ );
158296
+
158297
+ CREATE INDEX IF NOT EXISTS idx_project_key_files_project
158298
+ ON project_key_files(project_path);
158299
+ CREATE INDEX IF NOT EXISTS idx_project_key_files_generated_at
158300
+ ON project_key_files(project_path, generated_at);
158301
+
158302
+ CREATE TABLE IF NOT EXISTS project_key_files_version (
158303
+ project_path TEXT PRIMARY KEY,
158304
+ version INTEGER NOT NULL DEFAULT 0
158305
+ );
158306
+ `);
158307
+ }
158308
+ },
158309
+ {
158310
+ version: 15,
158311
+ description: "Add deferred_execute_state column for boundary execution drain",
158312
+ up: (db) => {
158313
+ const cols = db.prepare("PRAGMA table_info(session_meta)").all();
158314
+ if (!cols.some((c) => c.name === "deferred_execute_state")) {
158315
+ db.exec("ALTER TABLE session_meta ADD COLUMN deferred_execute_state TEXT");
158316
+ }
158317
+ }
158204
158318
  }
158205
158319
  ];
158206
158320
  function ensureMigrationsTable(db) {
@@ -158324,12 +158438,12 @@ function loadToolDefinitionMeasurements(db) {
158324
158438
  // ../plugin/src/features/magic-context/tool-owner-backfill.ts
158325
158439
  init_data_path();
158326
158440
  init_logger();
158327
- import { existsSync as existsSync2 } from "node:fs";
158328
- import { join as join5 } from "node:path";
158441
+ import { existsSync as existsSync3 } from "node:fs";
158442
+ import { join as join6 } from "node:path";
158329
158443
  var LEASE_DURATION_MS = 5 * 60 * 1000;
158330
158444
  var LEASE_RENEWAL_MS = 60 * 1000;
158331
158445
  function resolveOpencodeDbPath() {
158332
- return join5(getDataDir(), "opencode", "opencode.db");
158446
+ return join6(getDataDir(), "opencode", "opencode.db");
158333
158447
  }
158334
158448
  function ensureBackfillStateTable(db) {
158335
158449
  db.exec(`
@@ -158364,7 +158478,7 @@ function runToolOwnerBackfill(db) {
158364
158478
  return result;
158365
158479
  }
158366
158480
  const opencodeDbPath = resolveOpencodeDbPath();
158367
- if (!existsSync2(opencodeDbPath)) {
158481
+ if (!existsSync3(opencodeDbPath)) {
158368
158482
  log(`[backfill] OpenCode DB not found at ${opencodeDbPath} — marking all unbackfilled sessions as skipped. Lazy adoption (defense-in-depth) handles legacy rows at runtime.`);
158369
158483
  markAllUnbackfilledSessionsSkipped(db);
158370
158484
  result.sessionsSkippedNoOcDb = countSessionsByStatus(db, "skipped");
@@ -158572,21 +158686,21 @@ var persistenceByDatabase = new WeakMap;
158572
158686
  var persistenceErrorByDatabase = new WeakMap;
158573
158687
  function resolveDatabasePath() {
158574
158688
  const dbDir = getMagicContextStorageDir();
158575
- return { dbDir, dbPath: join6(dbDir, "context.db") };
158689
+ return { dbDir, dbPath: join7(dbDir, "context.db") };
158576
158690
  }
158577
158691
  function migrateLegacyStorageIfNeeded(targetDbPath, targetDbDir) {
158578
- if (existsSync3(targetDbPath))
158692
+ if (existsSync4(targetDbPath))
158579
158693
  return;
158580
158694
  const legacyDir = getLegacyOpenCodeMagicContextStorageDir();
158581
- const legacyDbPath = join6(legacyDir, "context.db");
158582
- if (!existsSync3(legacyDbPath))
158695
+ const legacyDbPath = join7(legacyDir, "context.db");
158696
+ if (!existsSync4(legacyDbPath))
158583
158697
  return;
158584
158698
  log(`[magic-context] migrating legacy plugin storage: ${legacyDir} -> ${targetDbDir} (legacy left in place as backup)`);
158585
158699
  mkdirSync4(targetDbDir, { recursive: true });
158586
158700
  for (const suffix of ["", "-wal", "-shm"]) {
158587
158701
  const src = `${legacyDbPath}${suffix}`;
158588
- const dst = join6(targetDbDir, `context.db${suffix}`);
158589
- if (existsSync3(src)) {
158702
+ const dst = join7(targetDbDir, `context.db${suffix}`);
158703
+ if (existsSync4(src)) {
158590
158704
  try {
158591
158705
  copyFileSync(src, dst);
158592
158706
  } catch (error51) {
@@ -158594,9 +158708,9 @@ function migrateLegacyStorageIfNeeded(targetDbPath, targetDbDir) {
158594
158708
  }
158595
158709
  }
158596
158710
  }
158597
- const legacyModelsDir = join6(legacyDir, "models");
158598
- const targetModelsDir = join6(targetDbDir, "models");
158599
- if (existsSync3(legacyModelsDir) && !existsSync3(targetModelsDir)) {
158711
+ const legacyModelsDir = join7(legacyDir, "models");
158712
+ const targetModelsDir = join7(targetDbDir, "models");
158713
+ if (existsSync4(legacyModelsDir) && !existsSync4(targetModelsDir)) {
158600
158714
  try {
158601
158715
  cpSync(legacyModelsDir, targetModelsDir, { recursive: true });
158602
158716
  } catch (error51) {
@@ -158744,6 +158858,26 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
158744
158858
  );
158745
158859
  CREATE INDEX IF NOT EXISTS idx_dream_runs_project ON dream_runs(project_path, finished_at DESC);
158746
158860
 
158861
+ CREATE TABLE IF NOT EXISTS project_key_files (
158862
+ project_path TEXT NOT NULL,
158863
+ path TEXT NOT NULL,
158864
+ content TEXT NOT NULL,
158865
+ content_hash TEXT NOT NULL,
158866
+ local_token_estimate INTEGER NOT NULL,
158867
+ generated_at INTEGER NOT NULL,
158868
+ generated_by_model TEXT,
158869
+ generation_config_hash TEXT NOT NULL,
158870
+ stale_reason TEXT,
158871
+ PRIMARY KEY (project_path, path)
158872
+ );
158873
+ CREATE INDEX IF NOT EXISTS idx_project_key_files_project ON project_key_files(project_path);
158874
+ CREATE INDEX IF NOT EXISTS idx_project_key_files_generated_at ON project_key_files(project_path, generated_at);
158875
+
158876
+ CREATE TABLE IF NOT EXISTS project_key_files_version (
158877
+ project_path TEXT PRIMARY KEY,
158878
+ version INTEGER NOT NULL DEFAULT 0
158879
+ );
158880
+
158747
158881
  -- (smart_notes: see note above; merged into unified notes table by migration v1)
158748
158882
 
158749
158883
  CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
@@ -158821,7 +158955,11 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
158821
158955
  -- valid JSON blob written via setPendingCompactionMarkerState.
158822
158956
  -- Excluded from healNullTextColumns. Readers filter IS NOT NULL AND
158823
158957
  -- != empty-string defensively. Plan v6 section 3.
158824
- pending_compaction_marker_state TEXT
158958
+ pending_compaction_marker_state TEXT,
158959
+ -- deferred_execute_state: intentionally NULLABLE without a default.
158960
+ -- Absence is SQL NULL; presence is a JSON blob written via
158961
+ -- setDeferredExecutePendingIfAbsent. Excluded from healNullTextColumns.
158962
+ deferred_execute_state TEXT
158825
158963
  );
158826
158964
 
158827
158965
  CREATE INDEX IF NOT EXISTS idx_tags_session_tag_number ON tags(session_id, tag_number);
@@ -158910,6 +159048,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
158910
159048
  ensureColumn(db, "session_meta", "detected_context_limit", "INTEGER DEFAULT 0");
158911
159049
  ensureColumn(db, "session_meta", "needs_emergency_recovery", "INTEGER DEFAULT 0");
158912
159050
  ensureColumn(db, "session_meta", "pending_compaction_marker_state", "TEXT");
159051
+ ensureColumn(db, "session_meta", "deferred_execute_state", "TEXT");
158913
159052
  ensureColumn(db, "tags", "harness", "TEXT NOT NULL DEFAULT 'opencode'");
158914
159053
  ensureColumn(db, "pending_ops", "harness", "TEXT NOT NULL DEFAULT 'opencode'");
158915
159054
  ensureColumn(db, "source_contents", "harness", "TEXT NOT NULL DEFAULT 'opencode'");
@@ -158986,7 +159125,15 @@ function ensureColumn(db, table, column, definition) {
158986
159125
  if (rows.some((row) => row.name === column)) {
158987
159126
  return;
158988
159127
  }
158989
- db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
159128
+ try {
159129
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
159130
+ } catch (err) {
159131
+ const recheck = db.prepare(`PRAGMA table_info(${table})`).all();
159132
+ if (recheck.some((row) => row.name === column)) {
159133
+ return;
159134
+ }
159135
+ throw err;
159136
+ }
158990
159137
  }
158991
159138
  function openDatabase() {
158992
159139
  const { dbDir, dbPath } = resolveDatabasePath();
@@ -159003,6 +159150,11 @@ function openDatabase() {
159003
159150
  const db = new Database(dbPath);
159004
159151
  initializeDatabase(db);
159005
159152
  runMigrations(db);
159153
+ try {
159154
+ deleteOrphanProjectKeyFiles(db);
159155
+ } catch (error51) {
159156
+ log(`[magic-context] key-files orphan GC failed: ${getErrorMessage(error51)}`);
159157
+ }
159006
159158
  try {
159007
159159
  runToolOwnerBackfill(db);
159008
159160
  } catch (error51) {
@@ -159124,6 +159276,12 @@ function isPersistedNoteNudgeRow(row) {
159124
159276
  const r = row;
159125
159277
  return typeof r.note_nudge_trigger_pending === "number" && typeof r.note_nudge_trigger_message_id === "string" && typeof r.note_nudge_sticky_text === "string" && typeof r.note_nudge_sticky_message_id === "string";
159126
159278
  }
159279
+ function isPersistedTodoSyntheticAnchorRow(row) {
159280
+ if (row === null || typeof row !== "object")
159281
+ return false;
159282
+ const r = row;
159283
+ return typeof r.todo_synthetic_call_id === "string" && typeof r.todo_synthetic_anchor_message_id === "string" && typeof r.todo_synthetic_state_json === "string";
159284
+ }
159127
159285
  function isPersistedHistorianFailureRow(row) {
159128
159286
  if (row === null || typeof row !== "object")
159129
159287
  return false;
@@ -159200,6 +159358,29 @@ function setPersistedDeliveredNoteNudge(db, sessionId, text, messageId = "") {
159200
159358
  function clearPersistedNoteNudge(db, sessionId) {
159201
159359
  db.prepare("UPDATE session_meta SET note_nudge_trigger_pending = 0, note_nudge_trigger_message_id = '', note_nudge_sticky_text = '', note_nudge_sticky_message_id = '' WHERE session_id = ?").run(sessionId);
159202
159360
  }
159361
+ function getPersistedTodoSyntheticAnchor(db, sessionId) {
159362
+ const result = db.prepare("SELECT todo_synthetic_call_id, todo_synthetic_anchor_message_id, todo_synthetic_state_json FROM session_meta WHERE session_id = ?").get(sessionId);
159363
+ if (!isPersistedTodoSyntheticAnchorRow(result)) {
159364
+ return null;
159365
+ }
159366
+ if (result.todo_synthetic_call_id.length === 0 || result.todo_synthetic_anchor_message_id.length === 0) {
159367
+ return null;
159368
+ }
159369
+ return {
159370
+ callId: result.todo_synthetic_call_id,
159371
+ messageId: result.todo_synthetic_anchor_message_id,
159372
+ stateJson: result.todo_synthetic_state_json
159373
+ };
159374
+ }
159375
+ function setPersistedTodoSyntheticAnchor(db, sessionId, callId, messageId, stateJson) {
159376
+ db.transaction(() => {
159377
+ ensureSessionMetaRow(db, sessionId);
159378
+ db.prepare("UPDATE session_meta SET todo_synthetic_call_id = ?, todo_synthetic_anchor_message_id = ?, todo_synthetic_state_json = ? WHERE session_id = ?").run(callId, messageId, stateJson, sessionId);
159379
+ })();
159380
+ }
159381
+ function clearPersistedTodoSyntheticAnchor(db, sessionId) {
159382
+ db.prepare("UPDATE session_meta SET todo_synthetic_call_id = '', todo_synthetic_anchor_message_id = '', todo_synthetic_state_json = '' WHERE session_id = ?").run(sessionId);
159383
+ }
159203
159384
  function getNoteLastReadAt(db, sessionId) {
159204
159385
  try {
159205
159386
  const result = db.prepare("SELECT note_last_read_at FROM session_meta WHERE session_id = ?").get(sessionId);
@@ -159326,6 +159507,30 @@ function clearPendingCompactionMarkerStateIf(db, sessionId, expected) {
159326
159507
  WHERE session_id = ? AND pending_compaction_marker_state = ?`).run(sessionId, expectedBlob);
159327
159508
  return result.changes > 0;
159328
159509
  }
159510
+ function peekDeferredExecutePending(db, sessionId) {
159511
+ const row = db.prepare("SELECT deferred_execute_state FROM session_meta WHERE session_id = ?").get(sessionId);
159512
+ const raw = row?.deferred_execute_state;
159513
+ if (raw === null || raw === undefined || raw === "")
159514
+ return null;
159515
+ try {
159516
+ return JSON.parse(raw);
159517
+ } catch {
159518
+ return null;
159519
+ }
159520
+ }
159521
+ function setDeferredExecutePendingIfAbsent(db, sessionId, payload) {
159522
+ ensureSessionMetaRow(db, sessionId);
159523
+ const payloadBlob = stableStringify(payload);
159524
+ const result = db.prepare(`UPDATE session_meta SET deferred_execute_state = ?
159525
+ WHERE session_id = ? AND deferred_execute_state IS NULL`).run(payloadBlob, sessionId);
159526
+ return result.changes > 0;
159527
+ }
159528
+ function clearDeferredExecutePendingIfMatches(db, sessionId, expected) {
159529
+ const expectedBlob = stableStringify(expected);
159530
+ const result = db.prepare(`UPDATE session_meta SET deferred_execute_state = NULL
159531
+ WHERE session_id = ? AND deferred_execute_state = ?`).run(sessionId, expectedBlob);
159532
+ return result.changes > 0;
159533
+ }
159329
159534
  // ../plugin/src/features/magic-context/resolve-subagent-fallback.ts
159330
159535
  init_logger();
159331
159536
  await init_read_session_db();
@@ -159920,10 +160125,10 @@ function getAgentFallbackModels(agent) {
159920
160125
 
159921
160126
  // ../plugin/src/shared/models-dev-cache.ts
159922
160127
  init_data_path();
159923
- import { createHash as createHash3 } from "node:crypto";
159924
- import { existsSync as existsSync4, readFileSync } from "node:fs";
160128
+ import { createHash as createHash4 } from "node:crypto";
160129
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
159925
160130
  import { homedir as homedir2, platform } from "node:os";
159926
- import { join as join7 } from "node:path";
160131
+ import { join as join8 } from "node:path";
159927
160132
 
159928
160133
  // ../plugin/src/shared/jsonc-parser.ts
159929
160134
  function stripJsonComments(content) {
@@ -160029,7 +160234,7 @@ var recentlySeenApiSizes = new Set;
160029
160234
  var fileCache = null;
160030
160235
  var fileLastAttempt = 0;
160031
160236
  function hashFast(input) {
160032
- return createHash3("sha1").update(input).digest("hex");
160237
+ return createHash4("sha1").update(input).digest("hex");
160033
160238
  }
160034
160239
  function getModelsJsonPath() {
160035
160240
  const explicit = process.env.OPENCODE_MODELS_PATH?.trim();
@@ -160038,16 +160243,16 @@ function getModelsJsonPath() {
160038
160243
  const cacheBase = getCacheDir();
160039
160244
  const source = process.env.OPENCODE_MODELS_URL?.trim();
160040
160245
  const filename = source && source !== "https://models.dev" ? `models-${hashFast(source)}.json` : "models.json";
160041
- return join7(cacheBase, "opencode", filename);
160246
+ return join8(cacheBase, "opencode", filename);
160042
160247
  }
160043
160248
  function getOpencodeConfigPath() {
160044
160249
  const envDir = process.env.OPENCODE_CONFIG_DIR?.trim();
160045
- const configDir = envDir ? envDir : platform() === "win32" ? join7(homedir2(), ".config", "opencode") : join7(process.env.XDG_CONFIG_HOME || join7(homedir2(), ".config"), "opencode");
160046
- const jsonc = join7(configDir, "opencode.jsonc");
160047
- if (existsSync4(jsonc))
160250
+ const configDir = envDir ? envDir : platform() === "win32" ? join8(homedir2(), ".config", "opencode") : join8(process.env.XDG_CONFIG_HOME || join8(homedir2(), ".config"), "opencode");
160251
+ const jsonc = join8(configDir, "opencode.jsonc");
160252
+ if (existsSync5(jsonc))
160048
160253
  return jsonc;
160049
- const json2 = join7(configDir, "opencode.json");
160050
- if (existsSync4(json2))
160254
+ const json2 = join8(configDir, "opencode.json");
160255
+ if (existsSync5(json2))
160051
160256
  return json2;
160052
160257
  return null;
160053
160258
  }
@@ -160079,9 +160284,9 @@ function loadModelsDevMetadataFromFile() {
160079
160284
  const modelsJsonPath = getModelsJsonPath();
160080
160285
  let fileFound = false;
160081
160286
  try {
160082
- if (existsSync4(modelsJsonPath)) {
160287
+ if (existsSync5(modelsJsonPath)) {
160083
160288
  fileFound = true;
160084
- const raw = readFileSync(modelsJsonPath, "utf-8");
160289
+ const raw = readFileSync2(modelsJsonPath, "utf-8");
160085
160290
  const data = JSON.parse(raw);
160086
160291
  for (const [providerId, provider2] of Object.entries(data)) {
160087
160292
  if (!provider2?.models || typeof provider2.models !== "object")
@@ -160096,8 +160301,8 @@ function loadModelsDevMetadataFromFile() {
160096
160301
  }
160097
160302
  try {
160098
160303
  const configPath = getOpencodeConfigPath();
160099
- if (configPath && existsSync4(configPath)) {
160100
- const config2 = parseJsonc(readFileSync(configPath, "utf-8"));
160304
+ if (configPath && existsSync5(configPath)) {
160305
+ const config2 = parseJsonc(readFileSync2(configPath, "utf-8"));
160101
160306
  if (config2.provider && typeof config2.provider === "object") {
160102
160307
  for (const [providerId, provider2] of Object.entries(config2.provider)) {
160103
160308
  if (!provider2?.models || typeof provider2.models !== "object")
@@ -160392,6 +160597,84 @@ function clearNoteNudgeState(db, sessionId, options) {
160392
160597
  lastDeliveredAt.delete(sessionId);
160393
160598
  }
160394
160599
 
160600
+ // ../plugin/src/hooks/magic-context/todo-view.ts
160601
+ import { createHash as createHash5 } from "node:crypto";
160602
+ var TERMINAL_STATUSES = new Set(["completed", "cancelled"]);
160603
+ var TITLE_DONE_STATUSES = new Set(["completed"]);
160604
+ var SYNTHETIC_CALL_ID_PREFIX = "mc_synthetic_todo_";
160605
+ function normalizeTodoStateJson(todos) {
160606
+ if (!Array.isArray(todos))
160607
+ return null;
160608
+ const normalized = [];
160609
+ for (const todo of todos) {
160610
+ if (!isTodoItem(todo))
160611
+ return null;
160612
+ normalized.push({
160613
+ content: todo.content,
160614
+ status: todo.status,
160615
+ priority: todo.priority ?? "medium"
160616
+ });
160617
+ }
160618
+ return JSON.stringify(normalized);
160619
+ }
160620
+ function buildSyntheticTodoPart(stateJson) {
160621
+ const todos = parseTodoState(stateJson);
160622
+ if (todos === null || todos.length === 0)
160623
+ return null;
160624
+ if (todos.every((t) => TERMINAL_STATUSES.has(t.status)))
160625
+ return null;
160626
+ const callID = computeSyntheticCallId(stateJson);
160627
+ const activeCount = todos.filter((t) => !TITLE_DONE_STATUSES.has(t.status)).length;
160628
+ const output = JSON.stringify(todos, null, 2);
160629
+ const ts = 0;
160630
+ return {
160631
+ type: "tool",
160632
+ callID,
160633
+ tool: "todowrite",
160634
+ state: {
160635
+ status: "completed",
160636
+ input: { todos },
160637
+ output,
160638
+ title: `${activeCount} todos`,
160639
+ metadata: { todos, truncated: false },
160640
+ time: { start: ts, end: ts }
160641
+ },
160642
+ syntheticTodoMarker: true
160643
+ };
160644
+ }
160645
+ function computeSyntheticCallId(stateJson) {
160646
+ const hash2 = createHash5("sha256").update(stateJson).digest("hex").slice(0, 16);
160647
+ return `${SYNTHETIC_CALL_ID_PREFIX}${hash2}`;
160648
+ }
160649
+ function parseTodoState(stateJson) {
160650
+ if (stateJson.length === 0)
160651
+ return null;
160652
+ try {
160653
+ const parsed = JSON.parse(stateJson);
160654
+ if (!Array.isArray(parsed))
160655
+ return null;
160656
+ const result = [];
160657
+ for (const item of parsed) {
160658
+ if (!isTodoItem(item))
160659
+ continue;
160660
+ result.push({
160661
+ content: item.content,
160662
+ status: item.status,
160663
+ priority: item.priority ?? "medium"
160664
+ });
160665
+ }
160666
+ return result;
160667
+ } catch {
160668
+ return null;
160669
+ }
160670
+ }
160671
+ function isTodoItem(value) {
160672
+ if (value === null || typeof value !== "object")
160673
+ return false;
160674
+ const todo = value;
160675
+ return typeof todo.content === "string" && typeof todo.status === "string" && (todo.priority === undefined || typeof todo.priority === "string");
160676
+ }
160677
+
160395
160678
  // src/index.ts
160396
160679
  init_data_path();
160397
160680
  init_logger();
@@ -160421,7 +160704,7 @@ init_logger();
160421
160704
 
160422
160705
  // src/subagent-runner.ts
160423
160706
  import * as childProcess from "node:child_process";
160424
- import { existsSync as existsSync5 } from "node:fs";
160707
+ import { existsSync as existsSync6 } from "node:fs";
160425
160708
  import { dirname as dirname3, resolve as resolvePath } from "node:path";
160426
160709
  import { createInterface } from "node:readline";
160427
160710
  import { fileURLToPath } from "node:url";
@@ -160429,7 +160712,7 @@ function resolveSubagentEntryPath() {
160429
160712
  try {
160430
160713
  const here = dirname3(fileURLToPath(import.meta.url));
160431
160714
  const candidate = resolvePath(here, "subagent-entry.js");
160432
- if (existsSync5(candidate))
160715
+ if (existsSync6(candidate))
160433
160716
  return candidate;
160434
160717
  return;
160435
160718
  } catch {
@@ -160454,13 +160737,13 @@ class PiSubagentRunner {
160454
160737
  async run(options) {
160455
160738
  const startTime = Date.now();
160456
160739
  const args = buildArgs(options);
160457
- return new Promise((resolve) => {
160740
+ return new Promise((resolve2) => {
160458
160741
  let settled = false;
160459
160742
  const settle = (result) => {
160460
160743
  if (settled)
160461
160744
  return;
160462
160745
  settled = true;
160463
- resolve(result);
160746
+ resolve2(result);
160464
160747
  };
160465
160748
  const emitProgress = (event) => {
160466
160749
  if (!options.onProgress)
@@ -160928,6 +161211,16 @@ function isLeaseActive(db) {
160928
161211
  function getLeaseHolder(db) {
160929
161212
  return getDreamState(db, LEASE_HOLDER_KEY);
160930
161213
  }
161214
+ function peekLeaseHolderAndExpiry(db, expectedHolder) {
161215
+ const holder = getDreamState(db, LEASE_HOLDER_KEY);
161216
+ if (holder !== expectedHolder)
161217
+ return false;
161218
+ const expiryStr = getDreamState(db, LEASE_EXPIRY_KEY);
161219
+ if (!expiryStr)
161220
+ return false;
161221
+ const expiry = Number(expiryStr);
161222
+ return Number.isFinite(expiry) && expiry >= Date.now();
161223
+ }
160931
161224
  function acquireLease(db, holderId) {
160932
161225
  return db.transaction(() => {
160933
161226
  if (isLeaseActive(db)) {
@@ -161040,8 +161333,8 @@ function clearStaleEntries(db, maxAgeMs) {
161040
161333
  // src/commands/ctx-dream.ts
161041
161334
  init_logger();
161042
161335
  // ../plugin/src/features/magic-context/dreamer/runner.ts
161043
- import { existsSync as existsSync6 } from "node:fs";
161044
- import { join as join8 } from "node:path";
161336
+ import { existsSync as existsSync8 } from "node:fs";
161337
+ import { join as join11 } from "node:path";
161045
161338
 
161046
161339
  // ../plugin/src/shared/index.ts
161047
161340
  init_logger();
@@ -161335,187 +161628,519 @@ init_logger();
161335
161628
  await init_sqlite();
161336
161629
 
161337
161630
  // ../plugin/src/features/magic-context/key-files/identify-key-files.ts
161631
+ import { readFileSync as readFileSync4 } from "node:fs";
161632
+ import { join as join10 } from "node:path";
161338
161633
  init_logger();
161339
161634
 
161340
- // ../plugin/src/features/magic-context/key-files/read-stats.ts
161341
- function getSessionReadStats(openCodeDb, sessionId, minReads) {
161342
- const fullReads = openCodeDb.prepare(`
161343
- WITH full_reads AS (
161344
- SELECT
161345
- json_extract(json_extract(data, '$.state'), '$.input.filePath') as file_path,
161346
- LENGTH(json_extract(json_extract(data, '$.state'), '$.output')) as output_bytes,
161347
- p.time_created,
161348
- ROW_NUMBER() OVER (
161349
- PARTITION BY json_extract(json_extract(data, '$.state'), '$.input.filePath')
161350
- ORDER BY p.time_created DESC
161351
- ) as rn
161352
- FROM part p
161353
- WHERE p.session_id = ?
161354
- AND json_extract(data, '$.type') = 'tool'
161355
- AND json_extract(data, '$.tool') = 'read'
161356
- AND json_extract(json_extract(data, '$.state'), '$.input.filePath') IS NOT NULL
161357
- AND json_extract(json_extract(data, '$.state'), '$.input.startLine') IS NULL
161358
- AND json_extract(json_extract(data, '$.state'), '$.input.start_line') IS NULL
161359
- AND json_extract(json_extract(data, '$.state'), '$.input.endLine') IS NULL
161360
- AND json_extract(json_extract(data, '$.state'), '$.input.end_line') IS NULL
161361
- AND json_extract(json_extract(data, '$.state'), '$.input.offset') IS NULL
161362
- AND json_extract(json_extract(data, '$.state'), '$.input.limit') IS NULL
161363
- ),
161364
- file_counts AS (
161365
- SELECT file_path, COUNT(*) as full_read_count
161366
- FROM full_reads
161367
- GROUP BY file_path
161368
- HAVING full_read_count >= ?
161369
- )
161370
- SELECT
161371
- r.file_path,
161372
- fc.full_read_count,
161373
- r.output_bytes as latest_read_bytes
161374
- FROM full_reads r
161375
- JOIN file_counts fc ON r.file_path = fc.file_path
161376
- WHERE r.rn = 1
161377
- ORDER BY fc.full_read_count DESC
161378
- `).all(sessionId, minReads);
161379
- if (fullReads.length === 0)
161380
- return [];
161381
- const editCounts = new Map;
161382
- const editRows = openCodeDb.prepare(`
161383
- SELECT
161384
- json_extract(json_extract(data, '$.state'), '$.input.filePath') as file_path,
161385
- COUNT(*) as edit_count
161386
- FROM part p
161387
- WHERE p.session_id = ?
161388
- AND json_extract(data, '$.type') = 'tool'
161389
- AND json_extract(data, '$.tool') IN ('edit', 'write', 'mcp_edit', 'mcp_write')
161390
- AND json_extract(json_extract(data, '$.state'), '$.input.filePath') IS NOT NULL
161391
- GROUP BY file_path
161392
- `).all(sessionId);
161393
- for (const row of editRows) {
161394
- editCounts.set(row.file_path, row.edit_count);
161395
- }
161396
- return fullReads.map((row) => ({
161397
- filePath: row.file_path,
161398
- fullReadCount: row.full_read_count,
161399
- spreadAcrossCompartments: 0,
161400
- editCount: editCounts.get(row.file_path) ?? 0,
161401
- latestReadBytes: row.latest_read_bytes ?? 0,
161402
- latestReadTokens: Math.ceil((row.latest_read_bytes ?? 0) / 3.5)
161403
- }));
161635
+ // ../plugin/src/features/magic-context/key-files/aft-availability.ts
161636
+ var import_comment_json = __toESM(require_src2(), 1);
161637
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "node:fs";
161638
+ import { homedir as homedir3 } from "node:os";
161639
+ import { join as join9 } from "node:path";
161640
+ var overrideAvailability = null;
161641
+ function parseConfig(path5) {
161642
+ if (!existsSync7(path5))
161643
+ return null;
161644
+ return import_comment_json.parse(readFileSync3(path5, "utf-8"));
161645
+ }
161646
+ function entryMatchesAft(entry) {
161647
+ const value = Array.isArray(entry) ? entry[0] : entry;
161648
+ return typeof value === "string" && (value.includes("@cortexkit/aft") || value.includes("aft-opencode") || value.includes("aft-pi"));
161649
+ }
161650
+ function hasAftInArray(value) {
161651
+ return Array.isArray(value) && value.some(entryMatchesAft);
161652
+ }
161653
+ function hasAftAtKeys(value, keys) {
161654
+ if (!value || typeof value !== "object")
161655
+ return false;
161656
+ const record2 = value;
161657
+ for (const key of keys) {
161658
+ if (hasAftInArray(record2[key]))
161659
+ return true;
161660
+ }
161661
+ return false;
161662
+ }
161663
+ function getAftAvailability() {
161664
+ const home = process.env.HOME || homedir3();
161665
+ const opencodePaths = [
161666
+ join9(home, ".config", "opencode", "opencode.jsonc"),
161667
+ join9(home, ".config", "opencode", "opencode.json")
161668
+ ];
161669
+ const piPaths = [join9(home, ".pi", "agent", "settings.json")];
161670
+ const checkedPaths = [...opencodePaths, ...piPaths];
161671
+ let opencode = false;
161672
+ for (const path5 of opencodePaths) {
161673
+ try {
161674
+ const config2 = parseConfig(path5);
161675
+ if (hasAftAtKeys(config2, ["plugin", "plugins", "mcp", "mcp_servers"])) {
161676
+ opencode = true;
161677
+ break;
161678
+ }
161679
+ } catch {}
161680
+ }
161681
+ let pi = false;
161682
+ for (const path5 of piPaths) {
161683
+ try {
161684
+ const config2 = parseConfig(path5);
161685
+ if (hasAftAtKeys(config2, ["packages", "extensions"])) {
161686
+ pi = true;
161687
+ break;
161688
+ }
161689
+ const agent = config2?.agent;
161690
+ if (hasAftAtKeys(agent, ["packages", "extensions"])) {
161691
+ pi = true;
161692
+ break;
161693
+ }
161694
+ } catch {}
161695
+ }
161696
+ const detected = opencode || pi;
161697
+ return {
161698
+ available: overrideAvailability ?? detected,
161699
+ opencode,
161700
+ pi,
161701
+ checkedPaths
161702
+ };
161703
+ }
161704
+ function isAftAvailable() {
161705
+ return getAftAvailability().available;
161404
161706
  }
161405
161707
 
161406
- // ../plugin/src/features/magic-context/key-files/storage-key-files.ts
161407
- init_logger();
161408
- function getKeyFiles(db, sessionId) {
161409
- try {
161410
- const row = db.prepare("SELECT key_files FROM session_meta WHERE session_id = ?").get(sessionId);
161411
- if (!row?.key_files)
161412
- return [];
161413
- const parsed = JSON.parse(row.key_files);
161414
- if (!Array.isArray(parsed))
161415
- return [];
161416
- return parsed.filter((e) => typeof e === "object" && e !== null && typeof e.filePath === "string" && typeof e.tokens === "number");
161417
- } catch {
161708
+ // ../plugin/src/features/magic-context/key-files/read-history.ts
161709
+ import { realpathSync as realpathSync2 } from "node:fs";
161710
+ import { relative, resolve as resolve2 } from "node:path";
161711
+ function toMs(value) {
161712
+ if (typeof value === "number" && Number.isFinite(value))
161713
+ return value;
161714
+ if (typeof value === "string") {
161715
+ const n = Number(value);
161716
+ if (Number.isFinite(n))
161717
+ return n;
161718
+ const parsed = Date.parse(value);
161719
+ if (Number.isFinite(parsed))
161720
+ return parsed;
161721
+ }
161722
+ return 0;
161723
+ }
161724
+ function toPositiveInt(value) {
161725
+ const n = Number(value);
161726
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
161727
+ }
161728
+ function coalesceRanges(ranges) {
161729
+ if (ranges.length === 0)
161418
161730
  return [];
161731
+ const sorted = [...ranges].sort((a, b) => a.start - b.start || a.end - b.end);
161732
+ const merged = [];
161733
+ for (const range of sorted) {
161734
+ const last = merged.at(-1);
161735
+ if (last && range.start <= last.end + 10) {
161736
+ last.end = Math.max(last.end, range.end);
161737
+ last.count += range.count;
161738
+ last.lastReadAt = Math.max(last.lastReadAt, range.lastReadAt);
161739
+ } else {
161740
+ merged.push({ ...range });
161741
+ }
161419
161742
  }
161743
+ return merged.sort((a, b) => b.lastReadAt - a.lastReadAt || b.count - a.count).slice(0, 3);
161420
161744
  }
161421
- function setKeyFiles(db, sessionId, files) {
161745
+ function normalizeProjectRelativePath(projectPath, filePath) {
161746
+ const root = realpathSync2(projectPath);
161747
+ const abs = filePath.startsWith("/") ? resolve2(filePath) : resolve2(root, filePath);
161748
+ let real = abs;
161422
161749
  try {
161423
- db.prepare("INSERT OR IGNORE INTO session_meta (session_id) VALUES (?)").run(sessionId);
161424
- db.prepare("UPDATE session_meta SET key_files = ? WHERE session_id = ?").run(JSON.stringify(files), sessionId);
161425
- } catch (error51) {
161426
- sessionLog(sessionId, "failed to persist key files:", error51);
161750
+ real = realpathSync2(abs);
161751
+ } catch {}
161752
+ if (!real.startsWith(`${root}/`) && real !== root)
161753
+ return null;
161754
+ const rel = relative(root, real).replaceAll("\\", "/");
161755
+ if (!rel || rel.startsWith(".."))
161756
+ return null;
161757
+ return rel;
161758
+ }
161759
+ function primarySessionIds(db) {
161760
+ try {
161761
+ const rows = db.prepare("SELECT session_id AS sessionId FROM session_meta WHERE is_subagent = 0").all();
161762
+ return new Set(rows.map((row) => row.sessionId));
161763
+ } catch {
161764
+ return new Set;
161427
161765
  }
161428
161766
  }
161429
- function greedyFitFiles(rankedFiles, tokenBudget) {
161430
- const selected = [];
161431
- let remainingBudget = tokenBudget;
161432
- for (const file2 of rankedFiles) {
161433
- if (file2.tokens <= 0)
161767
+ function collectKeyFileCandidates(args) {
161768
+ const primaryIds = primarySessionIds(args.magicDb);
161769
+ if (primaryIds.size === 0)
161770
+ return [];
161771
+ const reads = args.openCodeDb.prepare(`SELECT p.session_id,
161772
+ json_extract(json_extract(p.data, '$.state'), '$.input.filePath') AS file_path,
161773
+ json_extract(json_extract(p.data, '$.state'), '$.input.startLine') AS start_line,
161774
+ json_extract(json_extract(p.data, '$.state'), '$.input.endLine') AS end_line,
161775
+ json_extract(json_extract(p.data, '$.state'), '$.input.offset') AS offset_value,
161776
+ json_extract(json_extract(p.data, '$.state'), '$.input.limit') AS limit_value,
161777
+ LENGTH(json_extract(json_extract(p.data, '$.state'), '$.output')) AS output_bytes,
161778
+ p.time_created
161779
+ FROM part p
161780
+ WHERE json_extract(p.data, '$.type') = 'tool'
161781
+ AND json_extract(p.data, '$.tool') = 'read'
161782
+ AND json_extract(json_extract(p.data, '$.state'), '$.input.filePath') IS NOT NULL`).all();
161783
+ const byPath = new Map;
161784
+ for (const row of reads) {
161785
+ if (!primaryIds.has(row.session_id) || !row.file_path)
161434
161786
  continue;
161435
- if (file2.tokens > remainingBudget)
161787
+ const rel = normalizeProjectRelativePath(args.projectPath, row.file_path);
161788
+ if (!rel)
161436
161789
  continue;
161437
- selected.push({ filePath: file2.filePath, tokens: file2.tokens });
161438
- remainingBudget -= file2.tokens;
161439
- if (remainingBudget <= 0)
161440
- break;
161790
+ const timestamp = toMs(row.time_created);
161791
+ const candidate = byPath.get(rel) ?? {
161792
+ path: rel,
161793
+ totalReads: 0,
161794
+ rangedReads: 0,
161795
+ fullReads: 0,
161796
+ editCount: 0,
161797
+ latestReadBytes: 0,
161798
+ firstReadAt: timestamp || Date.now(),
161799
+ lastReadAt: 0,
161800
+ ranges: [],
161801
+ rangeMap: new Map
161802
+ };
161803
+ candidate.totalReads++;
161804
+ candidate.firstReadAt = Math.min(candidate.firstReadAt, timestamp || candidate.firstReadAt);
161805
+ candidate.lastReadAt = Math.max(candidate.lastReadAt, timestamp);
161806
+ if (timestamp >= candidate.lastReadAt)
161807
+ candidate.latestReadBytes = Number(row.output_bytes ?? 0);
161808
+ const start = toPositiveInt(row.start_line) ?? toPositiveInt(row.offset_value);
161809
+ const explicitEnd = toPositiveInt(row.end_line);
161810
+ const limit = toPositiveInt(row.limit_value);
161811
+ const end = explicitEnd ?? (start && limit ? start + limit - 1 : null);
161812
+ if (start && end) {
161813
+ candidate.rangedReads++;
161814
+ const key = `${start}:${end}`;
161815
+ const existing = candidate.rangeMap.get(key);
161816
+ if (existing) {
161817
+ existing.count++;
161818
+ existing.lastReadAt = Math.max(existing.lastReadAt, timestamp);
161819
+ } else {
161820
+ candidate.rangeMap.set(key, { start, end, count: 1, lastReadAt: timestamp });
161821
+ }
161822
+ } else {
161823
+ candidate.fullReads++;
161824
+ }
161825
+ byPath.set(rel, candidate);
161826
+ }
161827
+ const edits = args.openCodeDb.prepare(`SELECT p.session_id,
161828
+ json_extract(json_extract(p.data, '$.state'), '$.input.filePath') AS file_path,
161829
+ COUNT(*) AS edit_count
161830
+ FROM part p
161831
+ WHERE json_extract(p.data, '$.type') = 'tool'
161832
+ AND json_extract(p.data, '$.tool') IN ('edit', 'write', 'mcp_edit', 'mcp_write')
161833
+ AND json_extract(json_extract(p.data, '$.state'), '$.input.filePath') IS NOT NULL
161834
+ GROUP BY p.session_id, file_path`).all();
161835
+ for (const row of edits) {
161836
+ if (!primaryIds.has(row.session_id) || !row.file_path)
161837
+ continue;
161838
+ const rel = normalizeProjectRelativePath(args.projectPath, row.file_path);
161839
+ if (!rel)
161840
+ continue;
161841
+ const candidate = byPath.get(rel);
161842
+ if (candidate)
161843
+ candidate.editCount += Number(row.edit_count ?? 0);
161441
161844
  }
161442
- return selected;
161845
+ return [...byPath.values()].filter((candidate) => candidate.totalReads >= args.minReads).map(({ rangeMap, ...candidate }) => ({
161846
+ ...candidate,
161847
+ ranges: coalesceRanges([...rangeMap.values()])
161848
+ })).sort((a, b) => b.totalReads - a.totalReads || b.lastReadAt - a.lastReadAt).slice(0, 200);
161443
161849
  }
161444
161850
 
161851
+ // ../plugin/src/features/magic-context/key-files/storage-key-files.ts
161852
+ init_logger();
161853
+
161445
161854
  // ../plugin/src/features/magic-context/key-files/identify-key-files.ts
161446
161855
  var KEY_FILES_SYSTEM_PROMPT = "You are a file importance evaluator. Given read statistics about files in a coding session, identify which are core orientation files worth pinning in context. Return a JSON array.";
161447
- function buildKeyFilesPrompt(candidates, tokenBudget, minReads) {
161448
- const statsText = candidates.map((s) => `- **${s.filePath}** — ${s.fullReadCount} full reads, ${s.editCount} edits, ~${s.latestReadTokens} tokens`).join(`
161856
+ class KeyFilesValidationError extends Error {
161857
+ constructor(message) {
161858
+ super(message);
161859
+ this.name = "KeyFilesValidationError";
161860
+ }
161861
+ }
161862
+ function computeGenerationConfigHash(config2) {
161863
+ return sha256(JSON.stringify({ budget: config2.token_budget, min_reads: config2.min_reads }));
161864
+ }
161865
+ function extractJsonObject(text) {
161866
+ const fenced = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
161867
+ if (fenced?.[1])
161868
+ return fenced[1];
161869
+ const object2 = text.match(/\{[\s\S]*\}/);
161870
+ if (object2?.[0])
161871
+ return object2[0];
161872
+ throw new KeyFilesValidationError("missing JSON object");
161873
+ }
161874
+ function formatCandidates(candidates) {
161875
+ return candidates.map((candidate) => {
161876
+ const ranges = candidate.ranges.map((range) => `${range.start}-${range.end} (${range.count}x)`).join(", ");
161877
+ return `| ${candidate.path} | ${candidate.totalReads} | ${new Date(candidate.lastReadAt).toISOString()} | ${ranges || "full reads"} |`;
161878
+ }).join(`
161879
+ `);
161880
+ }
161881
+ function buildV6KeyFilesPrompt(args) {
161882
+ const current = args.currentRows.map((row) => `- ${row.path}: ${row.localTokenEstimate} tokens, stale=${row.staleReason ?? "fresh"}`).join(`
161449
161883
  `);
161450
- return `## Identify Key Files for Pinning
161884
+ return `## Task: Identify project key files
161451
161885
 
161452
- The following files were fully read ${minReads}+ times during a coding session.
161453
- Identify which ones are **core orientation files** worth keeping permanently in context.
161886
+ You are deciding which files this project's primary agent should always see as orientation context. The injection budget is **${args.config.token_budget} tokens** for all files combined.
161454
161887
 
161455
- ### Signals of a core orientation file:
161456
- - Read many times across different phases of work (not clustered in one task)
161457
- - Read without editing — consulted for understanding, not modification
161458
- - Contains architecture, configuration, types, or key abstractions
161888
+ ## Read history (primary sessions only, project-scoped)
161459
161889
 
161460
- ### Signals of a NON-core file (exclude):
161461
- - Read many times but always edited — actively working on it
161462
- - Very large (>5000 tokens) — too expensive to pin
161463
- - Test files, scripts, or generated files
161890
+ | path | total reads | last read | common line ranges |
161891
+ | --- | ---: | --- | --- |
161892
+ ${formatCandidates(args.candidates)}
161464
161893
 
161465
- ### Token budget: ${tokenBudget} tokens total
161894
+ ## Current key files (if any)
161466
161895
 
161467
- ### Files:
161468
- ${statsText}
161896
+ ${current || "(none)"}
161897
+
161898
+ ## Tools available
161899
+
161900
+ - \`aft_outline(target=<path>)\` — get symbol outline for a file
161901
+ - \`aft_zoom(filePath=<path>, symbol=<name>)\` — get full source of one symbol
161902
+
161903
+ ## Output (strict JSON)
161469
161904
 
161470
- ### Output Format
161471
- Return a JSON array ranked by importance (most important first):
161472
161905
  \`\`\`json
161473
- [
161474
- {"filePath": "src/path/to/file.ts", "tokens": 2500, "reason": "brief reason"}
161475
- ]
161906
+ {
161907
+ "no_change": false,
161908
+ "files": [
161909
+ {
161910
+ "path": "src/example.ts",
161911
+ "content": "...outline + symbol bodies stitched from src/example.ts only...",
161912
+ "approx_token_estimate": 1200
161913
+ }
161914
+ ]
161915
+ }
161476
161916
  \`\`\`
161477
161917
 
161478
- Only include files you're confident are true orientation files. Return empty array if none qualify.`;
161918
+ If current key files are still optimal, return \`{ "no_change": true, "files": [] }\`.
161919
+
161920
+ Rules:
161921
+ - Each row's content MUST come from ONLY the one file at path. If two files matter, emit two rows.
161922
+ - Do NOT emit source_files. One row = one file = one hash.
161923
+ - Total approx_token_estimate must be < ${args.config.token_budget}.
161924
+ - path must be relative, no .., no absolute paths, no symlinks escaping project root.
161925
+ - path must be unique case-sensitively and case-insensitively.
161926
+ - content must be plain text; no XML tags inside.`;
161927
+ }
161928
+ function validateLlmOutput(raw, config2, projectPath) {
161929
+ let obj;
161930
+ try {
161931
+ obj = JSON.parse(extractJsonObject(raw));
161932
+ } catch (error51) {
161933
+ if (error51 instanceof KeyFilesValidationError)
161934
+ throw error51;
161935
+ throw new KeyFilesValidationError(`invalid JSON: ${getErrorMessage(error51)}`);
161936
+ }
161937
+ if (!obj || typeof obj !== "object")
161938
+ throw new KeyFilesValidationError("output must be object");
161939
+ const record2 = obj;
161940
+ if (typeof record2.no_change !== "boolean")
161941
+ throw new KeyFilesValidationError("missing no_change");
161942
+ if (!Array.isArray(record2.files))
161943
+ throw new KeyFilesValidationError("missing files array");
161944
+ if (record2.no_change && record2.files.length > 0)
161945
+ throw new KeyFilesValidationError("no_change=true with files");
161946
+ if (!record2.no_change && record2.files.length === 0)
161947
+ throw new KeyFilesValidationError("no_change=false with empty files");
161948
+ const seen = new Set;
161949
+ const seenLower = new Set;
161950
+ const files = [];
161951
+ for (const item of record2.files) {
161952
+ if (!item || typeof item !== "object")
161953
+ throw new KeyFilesValidationError("bad file entry");
161954
+ const file2 = item;
161955
+ if ("source_files" in file2) {
161956
+ throw new KeyFilesValidationError(`source_files field not allowed (one file per row): ${String(file2.path)}`);
161957
+ }
161958
+ if (typeof file2.path !== "string" || file2.path.length === 0)
161959
+ throw new KeyFilesValidationError("bad path");
161960
+ if (file2.path.startsWith("/") || file2.path.includes(".."))
161961
+ throw new KeyFilesValidationError(`escape: ${file2.path}`);
161962
+ if (seen.has(file2.path))
161963
+ throw new KeyFilesValidationError(`dup path: ${file2.path}`);
161964
+ if (seenLower.has(file2.path.toLowerCase()))
161965
+ throw new KeyFilesValidationError(`case-dup: ${file2.path}`);
161966
+ seen.add(file2.path);
161967
+ seenLower.add(file2.path.toLowerCase());
161968
+ if (!isRelativeProjectFile(projectPath, file2.path))
161969
+ throw new KeyFilesValidationError(`unreadable: ${file2.path}`);
161970
+ if (typeof file2.content !== "string")
161971
+ throw new KeyFilesValidationError(`bad content: ${file2.path}`);
161972
+ if (file2.content.length > 1e5)
161973
+ throw new KeyFilesValidationError(`content >100KB: ${file2.path}`);
161974
+ if (typeof file2.approx_token_estimate !== "number" || file2.approx_token_estimate < 0) {
161975
+ throw new KeyFilesValidationError(`bad token estimate: ${file2.path}`);
161976
+ }
161977
+ const local = estimateTokens(file2.content);
161978
+ if (file2.approx_token_estimate > 0 && (local / file2.approx_token_estimate > 1.5 || local / file2.approx_token_estimate < 0.5)) {
161979
+ log(`key-files: token estimate divergence for ${file2.path}: claimed=${file2.approx_token_estimate}, plugin=${local}`);
161980
+ }
161981
+ files.push({
161982
+ path: file2.path,
161983
+ content: file2.content,
161984
+ approx_token_estimate: file2.approx_token_estimate,
161985
+ local_token_estimate: local
161986
+ });
161987
+ }
161988
+ const total = files.reduce((sum, file2) => sum + file2.local_token_estimate, 0);
161989
+ if (total > config2.token_budget)
161990
+ throw new KeyFilesValidationError(`total ${total} > budget ${config2.token_budget}`);
161991
+ return { no_change: record2.no_change, files };
161479
161992
  }
161480
- function parseKeyFilesOutput(text) {
161481
- const jsonMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/) ?? text.match(/\[[\s\S]*\]/);
161482
- if (!jsonMatch)
161483
- return [];
161993
+ function commitKeyFiles(args) {
161994
+ if (args.validated.no_change)
161995
+ return null;
161996
+ const projectPath = resolveProjectPath(args.projectPath) ?? args.projectPath;
161997
+ const resolved = resolveCommitFiles(projectPath, args.validated.files.map((file2) => ({
161998
+ path: file2.path,
161999
+ content: file2.content,
162000
+ localTokenEstimate: file2.local_token_estimate
162001
+ })));
162002
+ const generatedAt = Date.now();
162003
+ const bump = args.bumpVersion ?? bumpKeyFilesVersion;
162004
+ args.db.exec("BEGIN IMMEDIATE");
162005
+ let committed = false;
161484
162006
  try {
161485
- const raw = jsonMatch[1] ?? jsonMatch[0];
161486
- const parsed = JSON.parse(raw);
161487
- if (!Array.isArray(parsed))
161488
- return [];
161489
- return parsed.filter((item) => typeof item === "object" && item !== null && typeof item.filePath === "string" && typeof item.tokens === "number").map((item) => ({ filePath: item.filePath, tokens: item.tokens }));
161490
- } catch {
161491
- return [];
162007
+ if (!peekLeaseHolderAndExpiry(args.db, args.leaseHolderId)) {
162008
+ log(`key-files commit aborted: lease lost (holder ${args.leaseHolderId})`);
162009
+ return null;
162010
+ }
162011
+ args.db.prepare("DELETE FROM project_key_files WHERE project_path = ?").run(projectPath);
162012
+ insertResolvedKeyFiles(args.db, projectPath, resolved, generatedAt, args.modelId, args.configHash);
162013
+ const version2 = bump(args.db, projectPath);
162014
+ args.db.exec("COMMIT");
162015
+ committed = true;
162016
+ log(`key-files committed: ${resolved.length} files, version=${version2}, ${resolved.filter((r) => r.staleReason).length} pre-stale`);
162017
+ return version2;
162018
+ } finally {
162019
+ if (!committed) {
162020
+ try {
162021
+ args.db.exec("ROLLBACK");
162022
+ } catch {}
162023
+ }
161492
162024
  }
161493
162025
  }
161494
- function getKeyFileCandidates(openCodeDb, sessionId, minReads, tokenBudget, projectDirectory) {
161495
- const stats = getSessionReadStats(openCodeDb, sessionId, minReads);
161496
- const maxPerFileTokens = Math.min(tokenBudget / 2, 5000);
161497
- const projectPrefix = projectDirectory ? `${projectDirectory.replace(/\/$/, "")}/` : undefined;
161498
- return stats.filter((s) => s.latestReadTokens > 0 && s.latestReadTokens <= maxPerFileTokens && (!projectPrefix || s.filePath.startsWith(projectPrefix)));
161499
- }
161500
- function applyKeyFileResults(db, sessionId, llmRanked, tokenBudget, candidatePaths) {
161501
- const filtered = candidatePaths ? llmRanked.filter((f) => candidatePaths.has(f.filePath)) : llmRanked;
161502
- const selected = greedyFitFiles(filtered, tokenBudget);
161503
- setKeyFiles(db, sessionId, selected);
161504
- const totalTokens = selected.reduce((sum, f) => sum + f.tokens, 0);
161505
- log(`[key-files][${sessionId}] pinned ${selected.length} files (${totalTokens} tokens): ${selected.map((f) => f.filePath).join(", ")}`);
161506
- return { filesIdentified: selected.length, totalTokens };
161507
- }
161508
- function heuristicKeyFileSelection(db, sessionId, candidates, tokenBudget) {
161509
- const scored = candidates.map((c) => ({
161510
- filePath: c.filePath,
161511
- tokens: c.latestReadTokens,
161512
- score: c.fullReadCount * 2 - c.editCount * 3
161513
- })).filter((c) => c.score > 0).sort((a, b) => b.score - a.score);
161514
- const selected = greedyFitFiles(scored, tokenBudget);
161515
- setKeyFiles(db, sessionId, selected);
161516
- const totalTokens = selected.reduce((sum, f) => sum + f.tokens, 0);
161517
- log(`[key-files][${sessionId}] heuristic pinned ${selected.length} files (${totalTokens} tokens)`);
161518
- return { filesIdentified: selected.length, totalTokens };
162026
+ async function runKeyFilesLlm(args) {
162027
+ const createResponse = await args.client.session.create({
162028
+ body: {
162029
+ ...args.parentSessionId ? { parentID: args.parentSessionId } : {},
162030
+ title: "magic-context-dream-key-files-v6"
162031
+ },
162032
+ query: { directory: args.projectPath }
162033
+ });
162034
+ const created = normalizeSDKResponse(createResponse, null, {
162035
+ preferResponseOnMissingData: true
162036
+ });
162037
+ const agentSessionId = typeof created?.id === "string" ? created.id : null;
162038
+ if (!agentSessionId)
162039
+ throw new Error("Could not create key-file identification session.");
162040
+ try {
162041
+ await promptSyncWithModelSuggestionRetry(args.client, {
162042
+ path: { id: agentSessionId },
162043
+ query: { directory: args.projectPath },
162044
+ body: {
162045
+ agent: DREAMER_AGENT,
162046
+ system: KEY_FILES_SYSTEM_PROMPT,
162047
+ parts: [{ type: "text", text: args.prompt, synthetic: true }]
162048
+ }
162049
+ }, {
162050
+ timeoutMs: Math.min(Math.max(0, args.deadline - Date.now()), 5 * 60 * 1000),
162051
+ fallbackModels: args.fallbackModels,
162052
+ callContext: "dreamer:key-files-v6"
162053
+ });
162054
+ const messagesResponse = await args.client.session.messages({
162055
+ path: { id: agentSessionId },
162056
+ query: { directory: args.projectPath }
162057
+ });
162058
+ const messages = normalizeSDKResponse(messagesResponse, [], {
162059
+ preferResponseOnMissingData: true
162060
+ });
162061
+ const text = extractLatestAssistantText(messages);
162062
+ if (!text)
162063
+ throw new Error("Dreamer returned no key-files output.");
162064
+ return text;
162065
+ } finally {
162066
+ await args.client.session.delete({ path: { id: agentSessionId } }).catch(() => {
162067
+ return;
162068
+ });
162069
+ }
162070
+ }
162071
+ async function runKeyFilesTask(args) {
162072
+ if (!args.config.enabled)
162073
+ return { committedVersion: null, candidates: 0, noChange: false };
162074
+ if (!isAftAvailable()) {
162075
+ log("[key-files] AFT not available, skipping key-files task");
162076
+ return { committedVersion: null, candidates: 0, noChange: false };
162077
+ }
162078
+ const projectPath = resolveProjectPath(args.projectPath) ?? args.projectPath;
162079
+ const candidates = collectKeyFileCandidates({
162080
+ openCodeDb: args.openCodeDb,
162081
+ magicDb: args.db,
162082
+ projectPath,
162083
+ minReads: args.config.min_reads
162084
+ });
162085
+ if (candidates.length === 0)
162086
+ return { committedVersion: null, candidates: 0, noChange: false };
162087
+ const currentRows = readCurrentKeyFiles(args.db, projectPath);
162088
+ const configHash = computeGenerationConfigHash(args.config);
162089
+ const allRowsFreshAndCurrent = currentRows.length > 0 && currentRows.every((row) => {
162090
+ if (row.staleReason !== null || row.generationConfigHash !== configHash)
162091
+ return false;
162092
+ try {
162093
+ return sha256(readFileSync4(join10(projectPath, row.path))) === row.contentHash;
162094
+ } catch {
162095
+ return false;
162096
+ }
162097
+ });
162098
+ const currentPaths = new Set(currentRows.map((row) => row.path));
162099
+ if (allRowsFreshAndCurrent && candidates.every((candidate) => currentPaths.has(candidate.path))) {
162100
+ log(`key-files: no_change short-circuit (${currentRows.length} rows fresh)`);
162101
+ return { committedVersion: null, candidates: candidates.length, noChange: true };
162102
+ }
162103
+ const prompt = buildV6KeyFilesPrompt({ candidates, currentRows, config: args.config });
162104
+ let validated;
162105
+ const leaseInterval = setInterval(() => {
162106
+ try {
162107
+ if (!renewLease(args.db, args.holderId)) {
162108
+ log("[key-files] lease renewal failed during LLM call");
162109
+ }
162110
+ } catch (error51) {
162111
+ log(`[key-files] lease renewal threw: ${getErrorMessage(error51)}`);
162112
+ }
162113
+ }, 60000);
162114
+ try {
162115
+ try {
162116
+ const raw = await runKeyFilesLlm({
162117
+ client: args.client,
162118
+ parentSessionId: args.parentSessionId,
162119
+ projectPath,
162120
+ prompt,
162121
+ deadline: args.deadline,
162122
+ fallbackModels: args.fallbackModels
162123
+ });
162124
+ validated = validateLlmOutput(raw, args.config, projectPath);
162125
+ } catch (error51) {
162126
+ log(`[key-files] LLM validation failed: ${getErrorMessage(error51)}`);
162127
+ throw error51;
162128
+ }
162129
+ if (validated.no_change)
162130
+ return { committedVersion: null, candidates: candidates.length, noChange: true };
162131
+ const committedVersion = commitKeyFiles({
162132
+ db: args.db,
162133
+ projectPath,
162134
+ validated,
162135
+ configHash,
162136
+ modelId: args.fallbackModels?.[0] ?? "dreamer",
162137
+ leaseHolderId: args.holderId
162138
+ });
162139
+ renewLease(args.db, args.holderId);
162140
+ return { committedVersion, candidates: candidates.length, noChange: false };
162141
+ } finally {
162142
+ clearInterval(leaseInterval);
162143
+ }
161519
162144
  }
161520
162145
 
161521
162146
  // ../plugin/src/features/magic-context/memory/embedding-cache.ts
@@ -162579,11 +163204,11 @@ function logWithStackHead(message, stackHead) {
162579
163204
  log(message, stackHead ? { stackHead } : undefined);
162580
163205
  }
162581
163206
  function getOpenCodeDbPath2() {
162582
- return join8(getDataDir(), "opencode", "opencode.db");
163207
+ return join11(getDataDir(), "opencode", "opencode.db");
162583
163208
  }
162584
163209
  function openOpenCodeDb() {
162585
163210
  const dbPath = getOpenCodeDbPath2();
162586
- if (!existsSync6(dbPath)) {
163211
+ if (!existsSync8(dbPath)) {
162587
163212
  log(`[key-files] OpenCode DB not found at ${dbPath} — skipping`);
162588
163213
  return null;
162589
163214
  }
@@ -162596,190 +163221,6 @@ function openOpenCodeDb() {
162596
163221
  return null;
162597
163222
  }
162598
163223
  }
162599
- function isSessionIdRow(row) {
162600
- if (row === null || typeof row !== "object") {
162601
- return false;
162602
- }
162603
- return typeof row.sessionId === "string";
162604
- }
162605
- function hasExplicitEmptyKeyFilesOutput(text) {
162606
- return /```(?:json)?\s*\[\s*\]\s*```/s.test(text) || /^\s*\[\s*\]\s*$/s.test(text);
162607
- }
162608
- async function getActiveProjectSessionIds(args) {
162609
- try {
162610
- const { withReadOnlySessionDb: withReadOnlySessionDb2 } = await init_read_session_db().then(() => exports_read_session_db);
162611
- const projectSessionIds = withReadOnlySessionDb2((openCodeDb) => {
162612
- const bareIdentity = args.projectIdentity.replace(/^git:/, "");
162613
- const rows = openCodeDb.prepare("SELECT id FROM session WHERE project_id = ? AND parent_id IS NULL ORDER BY time_updated DESC").all(bareIdentity);
162614
- return new Set(rows.map((r) => r.id));
162615
- });
162616
- if (projectSessionIds.size === 0) {
162617
- return [];
162618
- }
162619
- return args.db.prepare("SELECT session_id AS sessionId FROM session_meta WHERE is_subagent = 0 ORDER BY session_id ASC").all().filter(isSessionIdRow).map((row) => row.sessionId).filter((sessionId) => projectSessionIds.has(sessionId));
162620
- } catch (error51) {
162621
- sessionLog(args.projectIdentity, `key-files: OpenCode DB lookup failed, falling back to SDK list: ${getErrorMessage(error51)}`);
162622
- const listResponse = await args.client.session.list({
162623
- query: { directory: args.sessionDirectory ?? args.projectIdentity }
162624
- });
162625
- const sessions = normalizeSDKResponse(listResponse, [], {
162626
- preferResponseOnMissingData: true
162627
- });
162628
- const projectSessionIds = new Set(sessions.map((session) => typeof session?.id === "string" ? session.id : null).filter((sessionId) => Boolean(sessionId)));
162629
- if (projectSessionIds.size === 0) {
162630
- return [];
162631
- }
162632
- return args.db.prepare("SELECT session_id AS sessionId FROM session_meta WHERE is_subagent = 0 ORDER BY session_id ASC").all().filter(isSessionIdRow).map((row) => row.sessionId).filter((sessionId) => projectSessionIds.has(sessionId));
162633
- }
162634
- }
162635
- async function identifyKeyFilesForSession(args) {
162636
- let openCodeDb = null;
162637
- try {
162638
- openCodeDb = openOpenCodeDb();
162639
- if (!openCodeDb) {
162640
- return;
162641
- }
162642
- const candidates = getKeyFileCandidates(openCodeDb, args.sessionId, args.config.min_reads, args.config.token_budget, args.sessionDirectory);
162643
- if (candidates.length === 0) {
162644
- log(`[key-files][${args.sessionId}] no candidates found — skipping`);
162645
- return;
162646
- }
162647
- const prompt = buildKeyFilesPrompt(candidates, args.config.token_budget, args.config.min_reads);
162648
- const applyHeuristicFallback = () => {
162649
- heuristicKeyFileSelection(args.db, args.sessionId, candidates, args.config.token_budget);
162650
- };
162651
- let agentSessionId = null;
162652
- const abortController = new AbortController;
162653
- const leaseInterval = setInterval(() => {
162654
- try {
162655
- if (!renewLease(args.db, args.holderId)) {
162656
- log(`[key-files][${args.sessionId}] lease renewal failed — aborting`);
162657
- args.onLeaseLost?.(`key-files:${args.sessionId}`);
162658
- abortController.abort();
162659
- }
162660
- } catch (error51) {
162661
- args.onLeaseLost?.(`key-files:${args.sessionId}`, error51);
162662
- abortController.abort();
162663
- }
162664
- }, 60000);
162665
- try {
162666
- const createResponse = await args.client.session.create({
162667
- body: {
162668
- ...args.parentSessionId ? { parentID: args.parentSessionId } : {},
162669
- title: `magic-context-dream-key-files-${args.sessionId.slice(0, 12)}`
162670
- },
162671
- query: { directory: args.sessionDirectory }
162672
- });
162673
- const created = normalizeSDKResponse(createResponse, null, { preferResponseOnMissingData: true });
162674
- agentSessionId = typeof created?.id === "string" ? created.id : null;
162675
- if (!agentSessionId) {
162676
- throw new Error("Could not create key-file identification session.");
162677
- }
162678
- log(`[key-files][${args.sessionId}] child session created ${agentSessionId}`);
162679
- const remainingMs = Math.max(0, args.deadline - Date.now());
162680
- await promptSyncWithModelSuggestionRetry(args.client, {
162681
- path: { id: agentSessionId },
162682
- query: { directory: args.sessionDirectory },
162683
- body: {
162684
- agent: DREAMER_AGENT,
162685
- system: KEY_FILES_SYSTEM_PROMPT,
162686
- parts: [{ type: "text", text: prompt, synthetic: true }]
162687
- }
162688
- }, {
162689
- timeoutMs: Math.min(remainingMs, 300000),
162690
- signal: abortController.signal,
162691
- fallbackModels: args.fallbackModels,
162692
- callContext: `dreamer:key-files:${args.sessionId.slice(0, 12)}`
162693
- });
162694
- const messagesResponse = await args.client.session.messages({
162695
- path: { id: agentSessionId },
162696
- query: { directory: args.sessionDirectory }
162697
- });
162698
- const messages = normalizeSDKResponse(messagesResponse, [], {
162699
- preferResponseOnMissingData: true
162700
- });
162701
- const responseText = extractLatestAssistantText(messages);
162702
- if (!responseText) {
162703
- log(`[key-files][${args.sessionId}] no response from agent — using heuristic fallback`);
162704
- applyHeuristicFallback();
162705
- return;
162706
- }
162707
- const parsed = parseKeyFilesOutput(responseText);
162708
- if (parsed.length > 0 || hasExplicitEmptyKeyFilesOutput(responseText)) {
162709
- const candidatePaths = new Set(candidates.map((c) => c.filePath));
162710
- applyKeyFileResults(args.db, args.sessionId, parsed, args.config.token_budget, candidatePaths);
162711
- return;
162712
- }
162713
- log(`[key-files][${args.sessionId}] could not parse agent output — using heuristic fallback`);
162714
- applyHeuristicFallback();
162715
- } catch (error51) {
162716
- if (args.isLeaseLost?.() || abortController.signal.aborted) {
162717
- log(`[key-files][${args.sessionId}] lease lost during identification — skipping heuristic fallback`);
162718
- throw error51;
162719
- }
162720
- log(`[key-files][${args.sessionId}] identification failed: ${getErrorMessage(error51)} — using heuristic fallback`);
162721
- try {
162722
- applyHeuristicFallback();
162723
- } catch (fallbackError) {
162724
- log(`[key-files][${args.sessionId}] heuristic fallback failed: ${getErrorMessage(fallbackError)}`);
162725
- }
162726
- } finally {
162727
- clearInterval(leaseInterval);
162728
- if (agentSessionId) {
162729
- await args.client.session.delete({
162730
- path: { id: agentSessionId },
162731
- query: { directory: args.sessionDirectory }
162732
- }).catch((error51) => {
162733
- log(`[key-files][${args.sessionId}] session cleanup failed: ${getErrorMessage(error51)}`);
162734
- });
162735
- }
162736
- }
162737
- } finally {
162738
- if (openCodeDb) {
162739
- try {
162740
- closeQuietly(openCodeDb);
162741
- } catch (error51) {
162742
- log(`[key-files][${args.sessionId}] failed to close OpenCode DB: ${getErrorMessage(error51)}`);
162743
- }
162744
- }
162745
- }
162746
- }
162747
- async function identifyKeyFiles(args) {
162748
- const sessionIds = await getActiveProjectSessionIds({
162749
- db: args.db,
162750
- client: args.client,
162751
- projectIdentity: args.projectIdentity,
162752
- sessionDirectory: args.sessionDirectory
162753
- });
162754
- if (sessionIds.length === 0) {
162755
- log(`[key-files] no active sessions found for ${args.projectIdentity}`);
162756
- return;
162757
- }
162758
- log(`[key-files] evaluating ${sessionIds.length} active session(s) for ${args.projectIdentity}`);
162759
- for (const sessionId of sessionIds) {
162760
- if (args.isLeaseLost?.()) {
162761
- log("[key-files] lease lost — stopping key-file identification");
162762
- break;
162763
- }
162764
- if (Date.now() > args.deadline) {
162765
- log("[key-files] deadline reached — stopping key-file identification");
162766
- break;
162767
- }
162768
- await identifyKeyFilesForSession({
162769
- db: args.db,
162770
- client: args.client,
162771
- parentSessionId: args.parentSessionId,
162772
- sessionDirectory: args.sessionDirectory,
162773
- holderId: args.holderId,
162774
- fallbackModels: args.fallbackModels,
162775
- onLeaseLost: args.onLeaseLost,
162776
- isLeaseLost: args.isLeaseLost,
162777
- deadline: args.deadline,
162778
- sessionId,
162779
- config: args.config
162780
- });
162781
- }
162782
- }
162783
163224
  async function runDream(args) {
162784
163225
  const holderId = crypto.randomUUID();
162785
163226
  const startedAt = Date.now();
@@ -162898,8 +163339,8 @@ async function runDream(args) {
162898
163339
  try {
162899
163340
  const docsDir = args.sessionDirectory ?? args.projectIdentity;
162900
163341
  const existingDocs = taskName === "maintain-docs" ? {
162901
- architecture: existsSync6(join8(docsDir, "ARCHITECTURE.md")),
162902
- structure: existsSync6(join8(docsDir, "STRUCTURE.md"))
163342
+ architecture: existsSync8(join11(docsDir, "ARCHITECTURE.md")),
163343
+ structure: existsSync8(join11(docsDir, "STRUCTURE.md"))
162903
163344
  } : undefined;
162904
163345
  const userMemories = taskName === "archive-stale" ? getActiveUserMemories(args.db).map((um) => ({
162905
163346
  id: um.id,
@@ -163000,8 +163441,7 @@ async function runDream(args) {
163000
163441
  clearInterval(leaseRenewalInterval);
163001
163442
  if (agentSessionId) {
163002
163443
  await args.client.session.delete({
163003
- path: { id: agentSessionId },
163004
- query: { directory: args.sessionDirectory ?? args.projectIdentity }
163444
+ path: { id: agentSessionId }
163005
163445
  }).catch((error51) => {
163006
163446
  log("[dreamer] failed to delete child session:", error51);
163007
163447
  });
@@ -163102,19 +163542,24 @@ async function runDream(args) {
163102
163542
  if (!verifyLeaseStillHeld("before key-file identification")) {
163103
163543
  throw new Error(lostLeaseReason ?? "Dream lease lost before key-file identification");
163104
163544
  }
163105
- await identifyKeyFiles({
163106
- db: args.db,
163107
- client: args.client,
163108
- projectIdentity: args.projectIdentity,
163109
- parentSessionId,
163110
- sessionDirectory: args.sessionDirectory ?? args.projectIdentity,
163111
- holderId,
163112
- deadline,
163113
- config: args.experimentalPinKeyFiles,
163114
- fallbackModels: args.fallbackModels,
163115
- onLeaseLost: markLeaseLost,
163116
- isLeaseLost: () => lostLease
163117
- });
163545
+ const openCodeDb = openOpenCodeDb();
163546
+ if (openCodeDb) {
163547
+ try {
163548
+ await runKeyFilesTask({
163549
+ db: args.db,
163550
+ openCodeDb,
163551
+ client: args.client,
163552
+ projectPath: args.sessionDirectory ?? args.projectIdentity,
163553
+ holderId,
163554
+ deadline,
163555
+ parentSessionId,
163556
+ config: args.experimentalPinKeyFiles,
163557
+ fallbackModels: args.fallbackModels
163558
+ });
163559
+ } finally {
163560
+ closeQuietly(openCodeDb);
163561
+ }
163562
+ }
163118
163563
  if (!verifyLeaseStillHeld("after key-file identification")) {
163119
163564
  throw new Error(lostLeaseReason ?? "Dream lease lost after key-file identification");
163120
163565
  }
@@ -163332,8 +163777,7 @@ Only include notes whose conditions you could definitively evaluate. Skip notes
163332
163777
  clearInterval(leaseInterval);
163333
163778
  if (agentSessionId) {
163334
163779
  await args.client.session.delete({
163335
- path: { id: agentSessionId },
163336
- query: { directory: args.sessionDirectory ?? args.projectIdentity }
163780
+ path: { id: agentSessionId }
163337
163781
  }).catch(() => {});
163338
163782
  }
163339
163783
  }
@@ -164451,6 +164895,9 @@ function executeFlush(db, sessionId) {
164451
164895
  }
164452
164896
  }
164453
164897
 
164898
+ // src/context-handler.ts
164899
+ import * as crypto2 from "node:crypto";
164900
+
164454
164901
  // ../plugin/src/features/magic-context/scheduler.ts
164455
164902
  init_logger();
164456
164903
  var TTL_PATTERN = /^(\d+)([smh])$/;
@@ -164848,6 +165295,30 @@ function applyFlushedStatuses(sessionId, db, targets, preloadedTags) {
164848
165295
  return didMutateMessage;
164849
165296
  }
164850
165297
 
165298
+ // ../plugin/src/hooks/magic-context/boundary-execution.ts
165299
+ var FORCE_MATERIALIZE_PERCENTAGE = 85;
165300
+ function detectMidTurnBypassReason(input) {
165301
+ if (input.contextUsage.percentage >= FORCE_MATERIALIZE_PERCENTAGE)
165302
+ return "force-materialize";
165303
+ if (input.historyRefreshSessions.has(input.sessionId))
165304
+ return "explicit-bust";
165305
+ if (input.sessionMeta.isSubagent)
165306
+ return "subagent";
165307
+ return "none";
165308
+ }
165309
+ function applyMidTurnDeferral(input) {
165310
+ if (input.base === "defer") {
165311
+ return { midTurnAdjustedSchedulerDecision: "defer", sideEffect: "none" };
165312
+ }
165313
+ if (input.bypassReason !== "none") {
165314
+ return { midTurnAdjustedSchedulerDecision: "execute", sideEffect: "none" };
165315
+ }
165316
+ if (input.midTurn) {
165317
+ return { midTurnAdjustedSchedulerDecision: "defer", sideEffect: "set-flag" };
165318
+ }
165319
+ return { midTurnAdjustedSchedulerDecision: "execute", sideEffect: "none" };
165320
+ }
165321
+
164851
165322
  // ../plugin/src/hooks/magic-context/caveman.ts
164852
165323
  var PRESERVATION_PATTERNS = [
164853
165324
  /```[\s\S]*?```/g,
@@ -166619,10 +167090,10 @@ var DEFAULT_MIN_PROMPT_CHARS = 20;
166619
167090
  async function unifiedSearchWithTimeout(db, sessionId, projectPath, prompt, options, timeoutMs) {
166620
167091
  const controller = new AbortController;
166621
167092
  let timer;
166622
- const timeoutPromise = new Promise((resolve) => {
167093
+ const timeoutPromise = new Promise((resolve3) => {
166623
167094
  timer = setTimeout(() => {
166624
167095
  controller.abort();
166625
- resolve(null);
167096
+ resolve3(null);
166626
167097
  }, timeoutMs);
166627
167098
  });
166628
167099
  try {
@@ -168272,7 +168743,7 @@ function getReducedRecompTokenBudget(currentBudget) {
168272
168743
  // ../plugin/src/hooks/magic-context/historian-state-file.ts
168273
168744
  init_data_path();
168274
168745
  import { mkdirSync as mkdirSync5, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
168275
- import { join as join9 } from "node:path";
168746
+ import { join as join12 } from "node:path";
168276
168747
  var HISTORIAN_STATE_INLINE_THRESHOLD = 30000;
168277
168748
  function maybeWriteHistorianStateFile(sessionId, existingState, directory) {
168278
168749
  if (existingState.length <= HISTORIAN_STATE_INLINE_THRESHOLD)
@@ -168280,7 +168751,7 @@ function maybeWriteHistorianStateFile(sessionId, existingState, directory) {
168280
168751
  try {
168281
168752
  const dir = getProjectMagicContextHistorianDir(directory);
168282
168753
  mkdirSync5(dir, { recursive: true });
168283
- const path5 = join9(dir, `state-${sessionId}-${Date.now()}.xml`);
168754
+ const path5 = join12(dir, `state-${sessionId}-${Date.now()}.xml`);
168284
168755
  writeFileSync2(path5, existingState, "utf8");
168285
168756
  return path5;
168286
168757
  } catch {
@@ -168297,6 +168768,238 @@ function cleanupHistorianStateFile(path5) {
168297
168768
 
168298
168769
  // src/pi-historian-runner.ts
168299
168770
  init_logger();
168771
+
168772
+ // src/read-session-pi.ts
168773
+ function isMidTurnPi(event, _sessionId) {
168774
+ const messages = event?.messages;
168775
+ if (!Array.isArray(messages))
168776
+ return false;
168777
+ let latestAssistantIndex = -1;
168778
+ let latestAssistant = null;
168779
+ for (let i = messages.length - 1;i >= 0; i--) {
168780
+ const msg = messages[i];
168781
+ if (msg !== null && typeof msg === "object") {
168782
+ const record2 = msg;
168783
+ if (record2.role === "assistant") {
168784
+ latestAssistantIndex = i;
168785
+ latestAssistant = record2;
168786
+ break;
168787
+ }
168788
+ }
168789
+ }
168790
+ if (latestAssistant === null)
168791
+ return false;
168792
+ if (latestAssistant.stopReason === "toolUse")
168793
+ return true;
168794
+ const toolCallIds = getToolCallIds(latestAssistant.content);
168795
+ if (toolCallIds.size === 0)
168796
+ return false;
168797
+ const pairedToolResultIds = new Set;
168798
+ for (const msg of messages.slice(latestAssistantIndex + 1)) {
168799
+ if (msg === null || typeof msg !== "object")
168800
+ continue;
168801
+ const record2 = msg;
168802
+ if (record2.role !== "toolResult")
168803
+ continue;
168804
+ if (typeof record2.toolCallId === "string") {
168805
+ pairedToolResultIds.add(record2.toolCallId);
168806
+ }
168807
+ }
168808
+ for (const id of toolCallIds) {
168809
+ if (!pairedToolResultIds.has(id))
168810
+ return true;
168811
+ }
168812
+ return false;
168813
+ }
168814
+ function getToolCallIds(content) {
168815
+ const ids = new Set;
168816
+ if (!Array.isArray(content))
168817
+ return ids;
168818
+ for (const item of content) {
168819
+ if (item === null || typeof item !== "object")
168820
+ continue;
168821
+ const record2 = item;
168822
+ if (record2.type === "toolCall" && typeof record2.id === "string") {
168823
+ ids.add(record2.id);
168824
+ }
168825
+ }
168826
+ return ids;
168827
+ }
168828
+ function readPiSessionMessages(ctx) {
168829
+ const sm = ctx.sessionManager;
168830
+ if (sm === undefined)
168831
+ return [];
168832
+ const getBranch = sm.getBranch;
168833
+ if (typeof getBranch !== "function")
168834
+ return [];
168835
+ let entries;
168836
+ try {
168837
+ entries = getBranch.call(sm);
168838
+ } catch {
168839
+ return [];
168840
+ }
168841
+ if (!Array.isArray(entries))
168842
+ return [];
168843
+ return convertEntriesToRawMessages(entries);
168844
+ }
168845
+ function convertEntriesToRawMessages(entries) {
168846
+ const result = [];
168847
+ let nextOrdinal = 1;
168848
+ let pendingToolParts = [];
168849
+ let pendingFirstRealId = "";
168850
+ for (const entry of entries) {
168851
+ if (!isMessageEntry(entry)) {
168852
+ continue;
168853
+ }
168854
+ const msg = entry.message;
168855
+ const role = msg.role;
168856
+ if (role === "toolResult") {
168857
+ pendingToolParts.push(...synthesizeToolResultParts(msg));
168858
+ if (pendingFirstRealId === "") {
168859
+ pendingFirstRealId = entry.id;
168860
+ }
168861
+ continue;
168862
+ }
168863
+ if (role === "user") {
168864
+ const parts = [
168865
+ ...pendingToolParts,
168866
+ ...synthesizeUserParts(msg)
168867
+ ];
168868
+ pendingToolParts = [];
168869
+ pendingFirstRealId = "";
168870
+ result.push({
168871
+ ordinal: nextOrdinal++,
168872
+ id: entry.id,
168873
+ role: "user",
168874
+ parts
168875
+ });
168876
+ continue;
168877
+ }
168878
+ if (role === "assistant") {
168879
+ if (pendingToolParts.length > 0) {
168880
+ result.push({
168881
+ ordinal: nextOrdinal++,
168882
+ id: pendingFirstRealId,
168883
+ role: "user",
168884
+ parts: pendingToolParts
168885
+ });
168886
+ pendingToolParts = [];
168887
+ pendingFirstRealId = "";
168888
+ }
168889
+ result.push({
168890
+ ordinal: nextOrdinal++,
168891
+ id: entry.id,
168892
+ role: "assistant",
168893
+ parts: synthesizeAssistantParts(msg)
168894
+ });
168895
+ continue;
168896
+ }
168897
+ result.push({
168898
+ ordinal: nextOrdinal++,
168899
+ id: entry.id,
168900
+ role: typeof role === "string" ? role : "unknown",
168901
+ parts: []
168902
+ });
168903
+ }
168904
+ if (pendingToolParts.length > 0) {
168905
+ result.push({
168906
+ ordinal: nextOrdinal,
168907
+ id: pendingFirstRealId,
168908
+ role: "user",
168909
+ parts: pendingToolParts
168910
+ });
168911
+ }
168912
+ return result;
168913
+ }
168914
+ function isMessageEntry(value) {
168915
+ if (value === null || typeof value !== "object")
168916
+ return false;
168917
+ const v = value;
168918
+ if (v.type !== "message")
168919
+ return false;
168920
+ if (typeof v.id !== "string")
168921
+ return false;
168922
+ if (v.message === null || typeof v.message !== "object")
168923
+ return false;
168924
+ return true;
168925
+ }
168926
+ function synthesizeUserParts(msg) {
168927
+ const m = msg;
168928
+ if (typeof m.content === "string") {
168929
+ if (m.content.trim().length === 0)
168930
+ return [];
168931
+ return [{ type: "text", text: m.content }];
168932
+ }
168933
+ if (!Array.isArray(m.content))
168934
+ return [];
168935
+ const parts = [];
168936
+ for (const c of m.content) {
168937
+ if (c === null || typeof c !== "object")
168938
+ continue;
168939
+ const cc = c;
168940
+ if (cc.type === "text" && typeof cc.text === "string") {
168941
+ parts.push({ type: "text", text: cc.text });
168942
+ }
168943
+ }
168944
+ return parts;
168945
+ }
168946
+ function synthesizeAssistantParts(msg) {
168947
+ const m = msg;
168948
+ if (!Array.isArray(m.content))
168949
+ return [];
168950
+ const parts = [];
168951
+ for (const c of m.content) {
168952
+ if (c === null || typeof c !== "object")
168953
+ continue;
168954
+ const cc = c;
168955
+ if (cc.type === "text" && typeof cc.text === "string") {
168956
+ parts.push({ type: "text", text: cc.text });
168957
+ } else if (cc.type === "toolCall" && typeof cc.id === "string") {
168958
+ parts.push({
168959
+ type: "tool",
168960
+ tool: typeof cc.name === "string" ? cc.name : "unknown",
168961
+ callID: cc.id,
168962
+ state: {
168963
+ input: cc.arguments ?? {}
168964
+ }
168965
+ });
168966
+ }
168967
+ }
168968
+ return parts;
168969
+ }
168970
+ function synthesizeToolResultParts(msg) {
168971
+ const m = msg;
168972
+ const callID = typeof m.toolCallId === "string" ? m.toolCallId : "";
168973
+ const tool = typeof m.toolName === "string" ? m.toolName : "unknown";
168974
+ if (!callID)
168975
+ return [];
168976
+ let output = "";
168977
+ if (Array.isArray(m.content)) {
168978
+ const fragments = [];
168979
+ for (const c of m.content) {
168980
+ if (c === null || typeof c !== "object")
168981
+ continue;
168982
+ const cc = c;
168983
+ if (cc.type === "text" && typeof cc.text === "string") {
168984
+ fragments.push(cc.text);
168985
+ }
168986
+ }
168987
+ output = fragments.join(`
168988
+ `);
168989
+ }
168990
+ return [
168991
+ {
168992
+ type: "tool",
168993
+ tool,
168994
+ callID,
168995
+ state: {
168996
+ output
168997
+ }
168998
+ }
168999
+ ];
169000
+ }
169001
+
169002
+ // src/pi-historian-runner.ts
168300
169003
  var HISTORIAN_AGENT_NAME = "magic-context-historian";
168301
169004
  var DEFAULT_HISTORIAN_TIMEOUT_MS2 = 120000;
168302
169005
  var HISTORIAN_ALERT_COOLDOWN_MS = 60 * 1000;
@@ -168591,191 +169294,160 @@ function appendPiNativeCompaction(args) {
168591
169294
  }
168592
169295
  }
168593
169296
  function findFirstKeptEntryId(entries, lastCompactedOrdinal) {
168594
- let ordinal = 0;
168595
- for (const entry of entries) {
168596
- if (!entry || typeof entry !== "object")
168597
- continue;
168598
- const e = entry;
168599
- if (e.type !== "message")
168600
- continue;
168601
- const role = e.message?.role;
168602
- if (role !== "user" && role !== "assistant")
168603
- continue;
168604
- ordinal++;
168605
- if (ordinal === lastCompactedOrdinal + 1) {
168606
- return typeof e.id === "string" && e.id.length > 0 ? e.id : null;
168607
- }
168608
- }
168609
- return null;
169297
+ const rawMessages = convertEntriesToRawMessages(entries);
169298
+ const target = lastCompactedOrdinal + 1;
169299
+ const match = rawMessages.find((m) => m.ordinal === target);
169300
+ if (!match)
169301
+ return null;
169302
+ return match.id.length > 0 ? match.id : null;
168610
169303
  }
168611
169304
 
168612
- // src/read-session-pi.ts
168613
- function readPiSessionMessages(ctx) {
168614
- const sm = ctx.sessionManager;
168615
- if (sm === undefined)
168616
- return [];
168617
- const getBranch = sm.getBranch;
168618
- if (typeof getBranch !== "function")
168619
- return [];
168620
- let entries;
168621
- try {
168622
- entries = getBranch.call(sm);
168623
- } catch {
168624
- return [];
169305
+ // src/pi-todo-inject.ts
169306
+ function getMessageId(message) {
169307
+ if (typeof message.responseId === "string" && message.responseId.length > 0) {
169308
+ return message.responseId;
168625
169309
  }
168626
- if (!Array.isArray(entries))
168627
- return [];
168628
- return convertEntriesToRawMessages(entries);
169310
+ if (typeof message.timestamp === "number") {
169311
+ return `pi-ts-${message.timestamp}`;
169312
+ }
169313
+ return "";
168629
169314
  }
168630
- function convertEntriesToRawMessages(entries) {
168631
- const result = [];
168632
- let nextOrdinal = 1;
168633
- let pendingToolParts = [];
168634
- for (const entry of entries) {
168635
- if (!isMessageEntry(entry)) {
168636
- continue;
168637
- }
168638
- const msg = entry.message;
168639
- const role = msg.role;
168640
- if (role === "toolResult") {
168641
- pendingToolParts.push(...synthesizeToolResultParts(msg));
168642
- continue;
168643
- }
168644
- if (role === "user") {
168645
- const parts = [
168646
- ...pendingToolParts,
168647
- ...synthesizeUserParts(msg)
168648
- ];
168649
- pendingToolParts = [];
168650
- result.push({
168651
- ordinal: nextOrdinal++,
168652
- id: entry.id,
168653
- role: "user",
168654
- parts
168655
- });
168656
- continue;
168657
- }
168658
- if (role === "assistant") {
168659
- if (pendingToolParts.length > 0) {
168660
- result.push({
168661
- ordinal: nextOrdinal++,
168662
- id: "",
168663
- role: "user",
168664
- parts: pendingToolParts
168665
- });
168666
- pendingToolParts = [];
168667
- }
168668
- result.push({
168669
- ordinal: nextOrdinal++,
168670
- id: entry.id,
168671
- role: "assistant",
168672
- parts: synthesizeAssistantParts(msg)
168673
- });
169315
+ function hasToolCallWithId(message, callId) {
169316
+ if (!Array.isArray(message.content))
169317
+ return false;
169318
+ for (const block of message.content) {
169319
+ if (!block || typeof block !== "object")
168674
169320
  continue;
168675
- }
168676
- result.push({
168677
- ordinal: nextOrdinal++,
168678
- id: entry.id,
168679
- role: typeof role === "string" ? role : "unknown",
168680
- parts: []
168681
- });
169321
+ const t = block.type;
169322
+ const id = block.id;
169323
+ if (t === "toolCall" && id === callId)
169324
+ return true;
168682
169325
  }
168683
- if (pendingToolParts.length > 0) {
168684
- result.push({
168685
- ordinal: nextOrdinal,
168686
- id: "",
168687
- role: "user",
168688
- parts: pendingToolParts
168689
- });
169326
+ return false;
169327
+ }
169328
+ function findToolResultAfter(messages, assistantIndex, callId) {
169329
+ for (let i = assistantIndex + 1;i < messages.length; i += 1) {
169330
+ const m = messages[i];
169331
+ if (!m || typeof m !== "object")
169332
+ continue;
169333
+ const role = m.role;
169334
+ const tcId = m.toolCallId;
169335
+ if (role === "toolResult" && tcId === callId)
169336
+ return i;
169337
+ if (role === "assistant")
169338
+ return -1;
168690
169339
  }
168691
- return result;
169340
+ return -1;
168692
169341
  }
168693
- function isMessageEntry(value) {
168694
- if (value === null || typeof value !== "object")
168695
- return false;
168696
- const v = value;
168697
- if (v.type !== "message")
168698
- return false;
168699
- if (typeof v.id !== "string")
168700
- return false;
168701
- if (v.message === null || typeof v.message !== "object")
168702
- return false;
168703
- return true;
169342
+ function piBlocksFromSynthetic(part) {
169343
+ return {
169344
+ call: {
169345
+ type: "toolCall",
169346
+ id: part.callID,
169347
+ name: part.tool,
169348
+ arguments: { todos: part.state.input.todos },
169349
+ syntheticTodoMarker: true
169350
+ },
169351
+ result: {
169352
+ role: "toolResult",
169353
+ toolCallId: part.callID,
169354
+ toolName: part.tool,
169355
+ content: [{ type: "text", text: part.state.output }],
169356
+ timestamp: 0,
169357
+ syntheticTodoMarker: true
169358
+ }
169359
+ };
168704
169360
  }
168705
- function synthesizeUserParts(msg) {
168706
- const m = msg;
168707
- if (typeof m.content === "string") {
168708
- if (m.content.trim().length === 0)
168709
- return [];
168710
- return [{ type: "text", text: m.content }];
168711
- }
168712
- if (!Array.isArray(m.content))
168713
- return [];
168714
- const parts = [];
168715
- for (const c of m.content) {
168716
- if (c === null || typeof c !== "object")
169361
+ function injectByAssistantId(messages, messageId, part) {
169362
+ for (let i = 0;i < messages.length; i += 1) {
169363
+ const m = messages[i];
169364
+ if (!m || typeof m !== "object")
168717
169365
  continue;
168718
- const cc = c;
168719
- if (cc.type === "text" && typeof cc.text === "string") {
168720
- parts.push({ type: "text", text: cc.text });
169366
+ if (m.role !== "assistant")
169367
+ continue;
169368
+ const assistant = m;
169369
+ if (getMessageId(assistant) !== messageId)
169370
+ continue;
169371
+ if (hasToolCallWithId(assistant, part.callID)) {
169372
+ if (findToolResultAfter(messages, i, part.callID) >= 0)
169373
+ return true;
169374
+ }
169375
+ const { call, result } = piBlocksFromSynthetic(part);
169376
+ if (!Array.isArray(assistant.content))
169377
+ assistant.content = [];
169378
+ if (!hasToolCallWithId(assistant, part.callID)) {
169379
+ assistant.content.push(call);
169380
+ }
169381
+ if (findToolResultAfter(messages, i, part.callID) < 0) {
169382
+ messages.splice(i + 1, 0, result);
168721
169383
  }
169384
+ return true;
168722
169385
  }
168723
- return parts;
169386
+ return false;
168724
169387
  }
168725
- function synthesizeAssistantParts(msg) {
168726
- const m = msg;
168727
- if (!Array.isArray(m.content))
168728
- return [];
168729
- const parts = [];
168730
- for (const c of m.content) {
168731
- if (c === null || typeof c !== "object")
169388
+ function injectIntoLatestAssistant(messages, part) {
169389
+ for (let i = messages.length - 1;i >= 0; i -= 1) {
169390
+ const m = messages[i];
169391
+ if (!m || typeof m !== "object")
168732
169392
  continue;
168733
- const cc = c;
168734
- if (cc.type === "text" && typeof cc.text === "string") {
168735
- parts.push({ type: "text", text: cc.text });
168736
- } else if (cc.type === "toolCall" && typeof cc.id === "string") {
168737
- parts.push({
168738
- type: "tool",
168739
- tool: typeof cc.name === "string" ? cc.name : "unknown",
168740
- callID: cc.id,
168741
- state: {
168742
- input: cc.arguments ?? {}
168743
- }
168744
- });
169393
+ if (m.role !== "assistant")
169394
+ continue;
169395
+ const assistant = m;
169396
+ const id = getMessageId(assistant);
169397
+ if (id.length === 0)
169398
+ continue;
169399
+ if (hasToolCallWithId(assistant, part.callID)) {
169400
+ if (findToolResultAfter(messages, i, part.callID) >= 0)
169401
+ return id;
169402
+ }
169403
+ const { call, result } = piBlocksFromSynthetic(part);
169404
+ if (!Array.isArray(assistant.content))
169405
+ assistant.content = [];
169406
+ if (!hasToolCallWithId(assistant, part.callID)) {
169407
+ assistant.content.push(call);
169408
+ }
169409
+ if (findToolResultAfter(messages, i, part.callID) < 0) {
169410
+ messages.splice(i + 1, 0, result);
168745
169411
  }
169412
+ return id;
168746
169413
  }
168747
- return parts;
169414
+ return null;
168748
169415
  }
168749
- function synthesizeToolResultParts(msg) {
168750
- const m = msg;
168751
- const callID = typeof m.toolCallId === "string" ? m.toolCallId : "";
168752
- const tool = typeof m.toolName === "string" ? m.toolName : "unknown";
168753
- if (!callID)
168754
- return [];
168755
- let output = "";
168756
- if (Array.isArray(m.content)) {
168757
- const fragments = [];
168758
- for (const c of m.content) {
168759
- if (c === null || typeof c !== "object")
168760
- continue;
168761
- const cc = c;
168762
- if (cc.type === "text" && typeof cc.text === "string") {
168763
- fragments.push(cc.text);
169416
+ function injectSyntheticTodowriteForPi(args) {
169417
+ if (args.isSubagent)
169418
+ return args.messages;
169419
+ const persistedAnchor = getPersistedTodoSyntheticAnchor(args.db, args.sessionId);
169420
+ if (args.isCacheBusting) {
169421
+ const part2 = buildSyntheticTodoPart(args.lastTodoState);
169422
+ if (part2 === null) {
169423
+ if (persistedAnchor) {
169424
+ clearPersistedTodoSyntheticAnchor(args.db, args.sessionId);
168764
169425
  }
169426
+ return args.messages;
168765
169427
  }
168766
- output = fragments.join(`
168767
- `);
168768
- }
168769
- return [
168770
- {
168771
- type: "tool",
168772
- tool,
168773
- callID,
168774
- state: {
168775
- output
169428
+ if (persistedAnchor && persistedAnchor.callId === part2.callID && injectByAssistantId(args.messages, persistedAnchor.messageId, part2)) {
169429
+ if (persistedAnchor.stateJson.length === 0) {
169430
+ setPersistedTodoSyntheticAnchor(args.db, args.sessionId, persistedAnchor.callId, persistedAnchor.messageId, args.lastTodoState);
168776
169431
  }
169432
+ return args.messages;
168777
169433
  }
168778
- ];
169434
+ const anchoredMessageId = injectIntoLatestAssistant(args.messages, part2);
169435
+ if (anchoredMessageId) {
169436
+ setPersistedTodoSyntheticAnchor(args.db, args.sessionId, part2.callID, anchoredMessageId, args.lastTodoState);
169437
+ } else if (persistedAnchor) {
169438
+ clearPersistedTodoSyntheticAnchor(args.db, args.sessionId);
169439
+ }
169440
+ return args.messages;
169441
+ }
169442
+ if (!persistedAnchor || persistedAnchor.stateJson.length === 0) {
169443
+ return args.messages;
169444
+ }
169445
+ const part = buildSyntheticTodoPart(persistedAnchor.stateJson);
169446
+ if (part === null || part.callID !== persistedAnchor.callId) {
169447
+ return args.messages;
169448
+ }
169449
+ injectByAssistantId(args.messages, persistedAnchor.messageId, part);
169450
+ return args.messages;
168779
169451
  }
168780
169452
 
168781
169453
  // src/reasoning-replay-pi.ts
@@ -169546,6 +170218,17 @@ var recentReduceBySession = new Map;
169546
170218
  var liveModelBySession = new Map;
169547
170219
  var toolUsageSinceUserTurn = new Map;
169548
170220
  var latestUserMessageBySession = new Map;
170221
+ function resolvePiContextModelKey(ctx) {
170222
+ const model = ctx.model;
170223
+ if (!model)
170224
+ return;
170225
+ if (typeof model.provider !== "string" || model.provider.length === 0) {
170226
+ return;
170227
+ }
170228
+ if (typeof model.id !== "string" || model.id.length === 0)
170229
+ return;
170230
+ return `${model.provider}/${model.id}`;
170231
+ }
169549
170232
  function readPiSessionMessageById(ctx, messageId) {
169550
170233
  return readPiSessionMessages(ctx).find((message) => message.id === messageId) ?? null;
169551
170234
  }
@@ -169768,8 +170451,32 @@ function registerPiContextHandler(pi, options) {
169768
170451
  const isFirstContextPassForSession = !firstContextPassSeenBySession.has(sessionId);
169769
170452
  firstContextPassSeenBySession.add(sessionId);
169770
170453
  const piUsage = ctx.getContextUsage?.();
169771
- let usagePercentage = typeof piUsage?.percent === "number" ? piUsage.percent : 0;
169772
- const usageInputTokens = typeof piUsage?.tokens === "number" ? piUsage.tokens : 0;
170454
+ const previousModelKey = liveModelBySession.get(sessionId);
170455
+ const currentModelKey = resolvePiContextModelKey(ctx);
170456
+ const modelChanged = previousModelKey !== undefined && currentModelKey !== undefined && previousModelKey !== currentModelKey;
170457
+ if (currentModelKey !== undefined) {
170458
+ liveModelBySession.set(sessionId, currentModelKey);
170459
+ }
170460
+ const sessionMetaForUsage = getOrCreateSessionMeta(options.db, sessionId);
170461
+ if ((isFirstContextPassForSession || modelChanged) && (sessionMetaForUsage.lastContextPercentage > 0 || sessionMetaForUsage.lastInputTokens > 0)) {
170462
+ const reason = isFirstContextPassForSession ? "first pass" : `model switch ${previousModelKey} -> ${currentModelKey}`;
170463
+ sessionLog(sessionId, `pi transform: ${reason} reset — percentage=${sessionMetaForUsage.lastContextPercentage.toFixed(1)}% tokens=${sessionMetaForUsage.lastInputTokens} — clearing stale usage state`);
170464
+ updateSessionMeta(options.db, sessionId, {
170465
+ lastContextPercentage: 0,
170466
+ lastInputTokens: 0
170467
+ });
170468
+ sessionMetaForUsage.lastContextPercentage = 0;
170469
+ sessionMetaForUsage.lastInputTokens = 0;
170470
+ }
170471
+ let usagePercentage = 0;
170472
+ let usageInputTokens = 0;
170473
+ if (sessionMetaForUsage.lastContextPercentage > 0 && sessionMetaForUsage.lastInputTokens > 0) {
170474
+ usagePercentage = sessionMetaForUsage.lastContextPercentage;
170475
+ usageInputTokens = sessionMetaForUsage.lastInputTokens;
170476
+ } else {
170477
+ usagePercentage = typeof piUsage?.percent === "number" ? piUsage.percent : 0;
170478
+ usageInputTokens = typeof piUsage?.tokens === "number" ? piUsage.tokens : 0;
170479
+ }
169773
170480
  let usageContextLimit = typeof piUsage?.contextWindow === "number" && piUsage.contextWindow > 0 ? piUsage.contextWindow : undefined;
169774
170481
  try {
169775
170482
  const overflowState = getOverflowState(options.db, sessionId);
@@ -169783,7 +170490,7 @@ function registerPiContextHandler(pi, options) {
169783
170490
  } catch (err) {
169784
170491
  sessionLog(sessionId, `pi transform: overflow state read failed: ${err instanceof Error ? err.message : String(err)}`);
169785
170492
  }
169786
- const sessionMeta = getOrCreateSessionMeta(options.db, sessionId);
170493
+ const sessionMeta = sessionMetaForUsage;
169787
170494
  const modelKey = liveModelBySession.get(sessionId);
169788
170495
  let schedulerDecision;
169789
170496
  try {
@@ -169798,6 +170505,29 @@ function registerPiContextHandler(pi, options) {
169798
170505
  schedulerDecision = "execute";
169799
170506
  sessionLog(sessionId, `pi transform: large imported session detected (${piMessageCount} messages, no usage baseline) — forcing execute on first pass`);
169800
170507
  }
170508
+ const schedulerDecisionEarly = schedulerDecision;
170509
+ const midTurn = isMidTurnPi(event, sessionId);
170510
+ const bypassReason = detectMidTurnBypassReason({
170511
+ contextUsage: { percentage: usagePercentage },
170512
+ sessionMeta,
170513
+ historyRefreshSessions,
170514
+ sessionId
170515
+ });
170516
+ const { midTurnAdjustedSchedulerDecision, sideEffect } = applyMidTurnDeferral({
170517
+ base: schedulerDecisionEarly,
170518
+ bypassReason,
170519
+ midTurn
170520
+ });
170521
+ if (sideEffect === "set-flag") {
170522
+ const flagPayload = {
170523
+ id: crypto2.randomUUID(),
170524
+ reason: `${schedulerDecisionEarly}-${bypassReason}`,
170525
+ recordedAt: Date.now()
170526
+ };
170527
+ setDeferredExecutePendingIfAbsent(options.db, sessionId, flagPayload);
170528
+ }
170529
+ schedulerDecision = midTurnAdjustedSchedulerDecision;
170530
+ sessionLog(sessionId, `pi [boundary-exec] base=${schedulerDecisionEarly} bypass=${bypassReason} midTurn=${midTurn} effective=${midTurnAdjustedSchedulerDecision} sideEffect=${sideEffect}`);
169801
170531
  const forceMaterialization = usagePercentage >= FORCE_MATERIALIZATION_PERCENTAGE;
169802
170532
  const isEmergency = usagePercentage >= EMERGENCY_BLOCK_PERCENTAGE;
169803
170533
  if (isEmergency) {
@@ -169812,7 +170542,7 @@ function registerPiContextHandler(pi, options) {
169812
170542
  try {
169813
170543
  await Promise.race([
169814
170544
  histPromise,
169815
- new Promise((resolve) => setTimeout(resolve, 30000))
170545
+ new Promise((resolve3) => setTimeout(resolve3, 30000))
169816
170546
  ]);
169817
170547
  sessionLog(sessionId, "pi EMERGENCY: historian wait completed (or timed out)");
169818
170548
  } catch {}
@@ -169904,6 +170634,22 @@ function registerPiContextHandler(pi, options) {
169904
170634
  sessionLog(sessionId, `pi auto-search failed: ${err instanceof Error ? err.message : String(err)}`);
169905
170635
  }
169906
170636
  }
170637
+ try {
170638
+ const sessionMetaForTodo = getOrCreateSessionMeta(options.db, sessionId);
170639
+ if (!sessionMetaForTodo.isSubagent && sessionMetaForTodo.lastTodoState !== "") {
170640
+ const isCacheBustingForTodo = isCacheBusting || result.executedWorkThisPass;
170641
+ outputMessages = injectSyntheticTodowriteForPi({
170642
+ db: options.db,
170643
+ sessionId,
170644
+ isSubagent: sessionMetaForTodo.isSubagent,
170645
+ isCacheBusting: isCacheBustingForTodo,
170646
+ lastTodoState: sessionMetaForTodo.lastTodoState,
170647
+ messages: outputMessages
170648
+ });
170649
+ }
170650
+ } catch (err) {
170651
+ sessionLog(sessionId, `pi synthetic todowrite injection failed: ${err instanceof Error ? err.message : String(err)}`);
170652
+ }
169907
170653
  clearLastTransformErrorIfSet(options.db, sessionId);
169908
170654
  return { messages: outputMessages };
169909
170655
  } catch (err) {
@@ -170085,16 +170831,25 @@ function maybeFireHistorian(args) {
170085
170831
  }
170086
170832
  let usage;
170087
170833
  try {
170088
- const piUsage = ctx.getContextUsage?.();
170089
- if (!piUsage || piUsage.tokens === null || piUsage.percent === null || piUsage.contextWindow === 0) {
170090
- sessionLog(sessionId, `pi-historian trigger eval: no usage info yet (tokens=${piUsage?.tokens ?? "<no piUsage>"}, percent=${piUsage?.percent ?? "<no piUsage>"}, contextWindow=${piUsage?.contextWindow ?? "<no piUsage>"})`);
170091
- return;
170834
+ const sessionMetaForUsage = getOrCreateSessionMeta(db, sessionId);
170835
+ if (sessionMetaForUsage.lastContextPercentage > 0 && sessionMetaForUsage.lastInputTokens > 0) {
170836
+ usage = {
170837
+ percentage: sessionMetaForUsage.lastContextPercentage,
170838
+ inputTokens: sessionMetaForUsage.lastInputTokens
170839
+ };
170840
+ sessionLog(sessionId, `pi-historian trigger eval: usage=${usage.percentage.toFixed(1)}% (${usage.inputTokens} tokens) [from session_meta], checking trigger...`);
170841
+ } else {
170842
+ const piUsage = ctx.getContextUsage?.();
170843
+ if (!piUsage || piUsage.tokens === null || piUsage.percent === null || piUsage.contextWindow === 0) {
170844
+ sessionLog(sessionId, `pi-historian trigger eval: no usage info yet (tokens=${piUsage?.tokens ?? "<no piUsage>"}, percent=${piUsage?.percent ?? "<no piUsage>"}, contextWindow=${piUsage?.contextWindow ?? "<no piUsage>"})`);
170845
+ return;
170846
+ }
170847
+ usage = {
170848
+ percentage: piUsage.percent,
170849
+ inputTokens: piUsage.tokens
170850
+ };
170851
+ sessionLog(sessionId, `pi-historian trigger eval: usage=${usage.percentage.toFixed(1)}% (${usage.inputTokens} tokens) [piUsage fallback], checking trigger...`);
170092
170852
  }
170093
- usage = {
170094
- percentage: piUsage.percent,
170095
- inputTokens: piUsage.tokens
170096
- };
170097
- sessionLog(sessionId, `pi-historian trigger eval: usage=${usage.percentage.toFixed(1)}% (${usage.inputTokens} tokens), checking trigger...`);
170098
170853
  } catch (err) {
170099
170854
  sessionLog(sessionId, `pi-historian trigger eval: getContextUsage threw: ${err instanceof Error ? err.message : String(err)}`);
170100
170855
  return;
@@ -170106,6 +170861,11 @@ function maybeFireHistorian(args) {
170106
170861
  let triggered = false;
170107
170862
  try {
170108
170863
  if (isFirstContextPassForSession) {
170864
+ const sessionMeta2 = getOrCreateSessionMeta(db, sessionId);
170865
+ if (sessionMeta2.compartmentInProgress && !inFlightHistorian.has(sessionId)) {
170866
+ updateSessionMeta(db, sessionId, { compartmentInProgress: false });
170867
+ sessionLog(sessionId, "pi-historian: cleared stale compartmentInProgress flag on first context pass after restart");
170868
+ }
170109
170869
  const failureState = getHistorianFailureState(db, sessionId);
170110
170870
  const shouldRecoverOnFirstPass = failureState.failureCount > 0 && hasEligiblePiCompartmentHistory(db, sessionId);
170111
170871
  if (shouldRecoverOnFirstPass) {
@@ -170149,6 +170909,7 @@ Historian previously failed ${failureState.failureCount} time(s), so magic-conte
170149
170909
  }
170150
170910
  }
170151
170911
  async function runPipeline(args) {
170912
+ let executedWorkThisPass = false;
170152
170913
  if (args.temporalAwareness) {
170153
170914
  try {
170154
170915
  const injected = injectPiTemporalMarkers(args.messages);
@@ -170179,9 +170940,15 @@ async function runPipeline(args) {
170179
170940
  }
170180
170941
  const hasPendingMaterializeSignal = hasPendingMaterialization(args.sessionId);
170181
170942
  if (args.schedulerDecision === "execute" || args.forceMaterialization || hasPendingMaterializeSignal) {
170182
- applyPendingOperations(args.sessionId, args.db, targets, args.protectedTags);
170183
- if (hasPendingMaterializeSignal) {
170184
- consumePendingMaterialization(args.sessionId);
170943
+ try {
170944
+ applyPendingOperations(args.sessionId, args.db, targets, args.protectedTags);
170945
+ executedWorkThisPass = true;
170946
+ if (hasPendingMaterializeSignal) {
170947
+ consumePendingMaterialization(args.sessionId);
170948
+ }
170949
+ } catch (err) {
170950
+ sessionLog(args.sessionId, `pi pending operations failed: ${err instanceof Error ? err.message : String(err)}`);
170951
+ throw err;
170185
170952
  }
170186
170953
  }
170187
170954
  const flushedSliceTags = getTagsByNumbers(args.db, args.sessionId, [
@@ -170235,6 +171002,7 @@ async function runPipeline(args) {
170235
171002
  caveman: args.heuristics.caveman
170236
171003
  });
170237
171004
  heuristicsExecuted = true;
171005
+ executedWorkThisPass = true;
170238
171006
  } catch (err) {
170239
171007
  sessionLog(args.sessionId, `pi heuristic cleanup failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
170240
171008
  }
@@ -170257,6 +171025,7 @@ async function runPipeline(args) {
170257
171025
  sessionLog(args.sessionId, `pi reasoning cleared on execute: cleared=${clearOutcome.cleared} watermark=${prevWatermark}->${clearOutcome.newWatermark}`);
170258
171026
  }
170259
171027
  }
171028
+ executedWorkThisPass = true;
170260
171029
  } catch (err) {
170261
171030
  sessionLog(args.sessionId, `pi reasoning clearing failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
170262
171031
  }
@@ -170279,6 +171048,16 @@ async function runPipeline(args) {
170279
171048
  messages: args.messages,
170280
171049
  isCacheBusting: args.isCacheBusting
170281
171050
  });
171051
+ if (executedWorkThisPass) {
171052
+ try {
171053
+ const currentFlag = peekDeferredExecutePending(args.db, args.sessionId);
171054
+ if (currentFlag !== null) {
171055
+ clearDeferredExecutePendingIfMatches(args.db, args.sessionId, currentFlag);
171056
+ }
171057
+ } catch (err) {
171058
+ sessionLog(args.sessionId, `pi [boundary-exec] drain failed (continuing): ${err}`);
171059
+ }
171060
+ }
170282
171061
  const outputMessages = transcript.getOutputMessages();
170283
171062
  try {
170284
171063
  const counts = tokenizePiMessages(outputMessages);
@@ -170292,6 +171071,7 @@ async function runPipeline(args) {
170292
171071
  return {
170293
171072
  messages: outputMessages,
170294
171073
  heuristicsExecuted,
171074
+ executedWorkThisPass,
170295
171075
  historyInjected: injectionResult?.injected ?? false,
170296
171076
  heuristicsResult,
170297
171077
  injectionResult
@@ -170488,7 +171268,7 @@ init_logger();
170488
171268
  init_data_path();
170489
171269
  init_logger();
170490
171270
  await init_sqlite();
170491
- import { join as join10 } from "node:path";
171271
+ import { join as join13 } from "node:path";
170492
171272
  var BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
170493
171273
  function randomBase62(length) {
170494
171274
  const chars = [];
@@ -170509,7 +171289,7 @@ function generatePartId(timestampMs, counter = 0n) {
170509
171289
  return generateId("prt", timestampMs, counter);
170510
171290
  }
170511
171291
  function getOpenCodeDbPath3() {
170512
- return join10(getDataDir(), "opencode", "opencode.db");
171292
+ return join13(getDataDir(), "opencode", "opencode.db");
170513
171293
  }
170514
171294
  var cachedWriteDb = null;
170515
171295
  var REQUIRED_MESSAGE_COLUMNS = ["id", "session_id", "time_created", "time_updated", "data"];
@@ -171059,7 +171839,7 @@ async function runCompressorPass2(args) {
171059
171839
  return null;
171060
171840
  } finally {
171061
171841
  if (agentSessionId) {
171062
- await client.session.delete({ path: { id: agentSessionId }, query: { directory } }).catch((e) => {
171842
+ await client.session.delete({ path: { id: agentSessionId } }).catch((e) => {
171063
171843
  sessionLog(sessionId, "compressor: session cleanup failed:", getErrorMessage(e));
171064
171844
  });
171065
171845
  }
@@ -171068,7 +171848,7 @@ async function runCompressorPass2(args) {
171068
171848
 
171069
171849
  // ../plugin/src/hooks/magic-context/compartment-runner-historian.ts
171070
171850
  import { mkdirSync as mkdirSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "node:fs";
171071
- import { join as join11 } from "node:path";
171851
+ import { join as join14 } from "node:path";
171072
171852
  init_magic_context();
171073
171853
  init_data_path();
171074
171854
  function historianResponseDumpDir(directory) {
@@ -171236,7 +172016,7 @@ async function runHistorianPrompt(args) {
171236
172016
  };
171237
172017
  } finally {
171238
172018
  if (agentSessionId) {
171239
- await client.session.delete({ path: { id: agentSessionId }, query: { directory: sessionDirectory } }).catch((e) => {
172019
+ await client.session.delete({ path: { id: agentSessionId } }).catch((e) => {
171240
172020
  sessionLog(parentSessionId, "compartment agent: session cleanup failed", getErrorMessage(e));
171241
172021
  });
171242
172022
  }
@@ -171301,8 +172081,8 @@ function isTransientHistorianPromptError(message) {
171301
172081
  ].some((token) => normalized.includes(token));
171302
172082
  }
171303
172083
  function sleep(ms) {
171304
- return new Promise((resolve) => {
171305
- setTimeout(resolve, ms);
172084
+ return new Promise((resolve3) => {
172085
+ setTimeout(resolve3, ms);
171306
172086
  });
171307
172087
  }
171308
172088
  function cleanupHistorianDump(sessionId, dumpPath) {
@@ -171323,7 +172103,7 @@ function dumpHistorianResponse(sessionId, directory, label, text) {
171323
172103
  mkdirSync6(dumpDir, { recursive: true });
171324
172104
  const safeSessionId = sanitizeDumpName(sessionId);
171325
172105
  const safeLabel = sanitizeDumpName(label);
171326
- const dumpPath = join11(dumpDir, `${safeSessionId}-${safeLabel}-${Date.now()}.xml`);
172106
+ const dumpPath = join14(dumpDir, `${safeSessionId}-${safeLabel}-${Date.now()}.xml`);
171327
172107
  writeFileSync3(dumpPath, text, "utf8");
171328
172108
  sessionLog(sessionId, "compartment agent: historian response dumped", {
171329
172109
  label,
@@ -172525,11 +173305,11 @@ import {
172525
173305
  matchesKey,
172526
173306
  truncateToWidth,
172527
173307
  visibleWidth
172528
- } from "@mariozechner/pi-tui";
173308
+ } from "@earendil-works/pi-tui";
172529
173309
  // package.json
172530
173310
  var package_default = {
172531
173311
  name: "@cortexkit/pi-magic-context",
172532
- version: "0.19.0",
173312
+ version: "0.20.0",
172533
173313
  type: "module",
172534
173314
  description: "Pi coding agent extension for Magic Context — cross-session memory and context management",
172535
173315
  main: "dist/index.js",
@@ -172555,7 +173335,7 @@ var package_default = {
172555
173335
  "README.md"
172556
173336
  ],
172557
173337
  scripts: {
172558
- build: "bun build src/index.ts src/subagent-entry.ts --outdir dist --target node --format esm --external @mariozechner/pi-coding-agent --external @mariozechner/pi-tui --external @huggingface/transformers --external better-sqlite3",
173338
+ build: "bun build src/index.ts src/subagent-entry.ts --outdir dist --target node --format esm --external @earendil-works/pi-coding-agent --external @earendil-works/pi-tui --external @huggingface/transformers --external better-sqlite3",
172559
173339
  typecheck: "tsc --noEmit",
172560
173340
  test: "bun test",
172561
173341
  lint: "biome check src",
@@ -172575,15 +173355,15 @@ var package_default = {
172575
173355
  },
172576
173356
  devDependencies: {
172577
173357
  "@biomejs/biome": "^2.4.7",
172578
- "@mariozechner/pi-coding-agent": "^0.71.0",
172579
- "@mariozechner/pi-tui": "^0.71.0",
173358
+ "@earendil-works/pi-coding-agent": "^0.74.0",
173359
+ "@earendil-works/pi-tui": "^0.74.0",
172580
173360
  "@types/better-sqlite3": "^7.6.13",
172581
173361
  "@types/node": "^22.0.0",
172582
173362
  typescript: "^5.8.0"
172583
173363
  },
172584
173364
  peerDependencies: {
172585
- "@mariozechner/pi-coding-agent": "^0.71.0",
172586
- "@mariozechner/pi-tui": "^0.71.0"
173365
+ "@earendil-works/pi-coding-agent": "^0.74.0",
173366
+ "@earendil-works/pi-tui": "^0.74.0"
172587
173367
  },
172588
173368
  exports: {
172589
173369
  ".": {
@@ -173066,14 +173846,14 @@ function readHistorianState(db, sessionId, meta3) {
173066
173846
 
173067
173847
  // src/config/index.ts
173068
173848
  init_magic_context();
173069
- import { existsSync as existsSync8, readFileSync as readFileSync3 } from "node:fs";
173070
- import { homedir as homedir4 } from "node:os";
173071
- import { join as join12 } from "node:path";
173849
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "node:fs";
173850
+ import { homedir as homedir5 } from "node:os";
173851
+ import { join as join15 } from "node:path";
173072
173852
 
173073
173853
  // ../plugin/src/config/variable.ts
173074
- import { existsSync as existsSync7, readFileSync as readFileSync2 } from "node:fs";
173075
- import { homedir as homedir3 } from "node:os";
173076
- import { dirname as dirname4, isAbsolute, resolve } from "node:path";
173854
+ import { existsSync as existsSync9, readFileSync as readFileSync5 } from "node:fs";
173855
+ import { homedir as homedir4 } from "node:os";
173856
+ import { dirname as dirname4, isAbsolute, resolve as resolve3 } from "node:path";
173077
173857
  var ENV_PATTERN = /\{env:([^}]+)\}/g;
173078
173858
  var FILE_PATTERN = /\{file:([^}]+)\}/g;
173079
173859
  function substituteConfigVariables(input) {
@@ -173110,17 +173890,17 @@ function substituteConfigVariables(input) {
173110
173890
  }
173111
173891
  let filePath = rawPath.trim();
173112
173892
  if (filePath.startsWith("~/")) {
173113
- filePath = resolve(homedir3(), filePath.slice(2));
173893
+ filePath = resolve3(homedir4(), filePath.slice(2));
173114
173894
  } else if (!isAbsolute(filePath)) {
173115
- filePath = resolve(configDir, filePath);
173895
+ filePath = resolve3(configDir, filePath);
173116
173896
  }
173117
- if (!existsSync7(filePath)) {
173897
+ if (!existsSync9(filePath)) {
173118
173898
  warnings.push(`File not found for ${token} (resolved to ${filePath}); using empty string`);
173119
173899
  continue;
173120
173900
  }
173121
173901
  let contents;
173122
173902
  try {
173123
- contents = readFileSync2(filePath, "utf-8").trim();
173903
+ contents = readFileSync5(filePath, "utf-8").trim();
173124
173904
  } catch (error51) {
173125
173905
  const message = error51 instanceof Error ? error51.message : String(error51);
173126
173906
  warnings.push(`Failed to read file for ${token} (${filePath}): ${message}; using empty string`);
@@ -173133,23 +173913,23 @@ function substituteConfigVariables(input) {
173133
173913
  }
173134
173914
 
173135
173915
  // src/config/index.ts
173136
- var import_comment_json = __toESM(require_src2(), 1);
173916
+ var import_comment_json2 = __toESM(require_src2(), 1);
173137
173917
  var CONFIG_FILE_NAME = "magic-context";
173138
173918
  function getProjectConfigPaths(cwd) {
173139
- const basePath = join12(cwd, ".pi", CONFIG_FILE_NAME);
173919
+ const basePath = join15(cwd, ".pi", CONFIG_FILE_NAME);
173140
173920
  return [`${basePath}.jsonc`, `${basePath}.json`];
173141
173921
  }
173142
173922
  function getUserConfigPaths() {
173143
- const home = process.env.HOME ?? homedir4();
173144
- const basePath = join12(home, ".pi", "agent", CONFIG_FILE_NAME);
173923
+ const home = process.env.HOME ?? homedir5();
173924
+ const basePath = join15(home, ".pi", "agent", CONFIG_FILE_NAME);
173145
173925
  return [`${basePath}.jsonc`, `${basePath}.json`];
173146
173926
  }
173147
173927
  function resolveFirstExisting(paths) {
173148
- return paths.find((path5) => existsSync8(path5));
173928
+ return paths.find((path5) => existsSync10(path5));
173149
173929
  }
173150
173930
  function loadConfigFile(path5, scope) {
173151
173931
  try {
173152
- const rawText = readFileSync3(path5, "utf-8");
173932
+ const rawText = readFileSync6(path5, "utf-8");
173153
173933
  const substituted = substituteConfigVariables({
173154
173934
  text: rawText,
173155
173935
  configPath: path5
@@ -173157,7 +173937,7 @@ function loadConfigFile(path5, scope) {
173157
173937
  return {
173158
173938
  path: path5,
173159
173939
  scope,
173160
- config: import_comment_json.parse(substituted.text),
173940
+ config: import_comment_json2.parse(substituted.text),
173161
173941
  warnings: substituted.warnings.map((warning) => `${path5}: ${warning}`)
173162
173942
  };
173163
173943
  } catch (error51) {
@@ -173271,6 +174051,42 @@ function loadPiConfig(opts = {}) {
173271
174051
  };
173272
174052
  }
173273
174053
 
174054
+ // src/pi-pressure.ts
174055
+ function extractAssistantUsage(message) {
174056
+ if (!message || typeof message !== "object")
174057
+ return null;
174058
+ const m = message;
174059
+ if (m.role !== "assistant")
174060
+ return null;
174061
+ if (!m.usage || typeof m.usage !== "object")
174062
+ return null;
174063
+ const u = m.usage;
174064
+ const result = {};
174065
+ if (typeof u.input === "number")
174066
+ result.input = u.input;
174067
+ if (typeof u.output === "number")
174068
+ result.output = u.output;
174069
+ if (typeof u.cacheRead === "number")
174070
+ result.cacheRead = u.cacheRead;
174071
+ if (typeof u.cacheWrite === "number")
174072
+ result.cacheWrite = u.cacheWrite;
174073
+ if (typeof u.totalTokens === "number")
174074
+ result.totalTokens = u.totalTokens;
174075
+ return result;
174076
+ }
174077
+ function computePiPressure(usage, contextLimit) {
174078
+ if (!usage)
174079
+ return null;
174080
+ const input = usage.input ?? 0;
174081
+ const cacheRead = usage.cacheRead ?? 0;
174082
+ const cacheWrite = usage.cacheWrite ?? 0;
174083
+ const inputTokens = input + cacheRead + cacheWrite;
174084
+ if (inputTokens === 0)
174085
+ return null;
174086
+ const percentage = contextLimit > 0 ? inputTokens / contextLimit * 100 : 0;
174087
+ return { inputTokens, percentage };
174088
+ }
174089
+
173274
174090
  // src/strip-tag-prefix.ts
173275
174091
  var TAG_PREFIX_REGEX2 = /^(\u00a7\d+\u00a7\s*)+/;
173276
174092
  function stripTagPrefixFromAssistantMessage(message) {
@@ -173296,9 +174112,9 @@ function stripTagPrefixFromAssistantMessage(message) {
173296
174112
  }
173297
174113
 
173298
174114
  // src/system-prompt.ts
173299
- import { createHash as createHash4 } from "node:crypto";
173300
- import { existsSync as existsSync9, readFileSync as readFileSync4, realpathSync } from "node:fs";
173301
- import { join as join13, resolve as resolve2, sep } from "node:path";
174115
+ import { createHash as createHash6 } from "node:crypto";
174116
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
174117
+ import { join as join17 } from "node:path";
173302
174118
 
173303
174119
  // ../plugin/src/agents/magic-context-prompt.ts
173304
174120
  function getToolHistoryGuidance(dropToolStructure) {
@@ -173387,23 +174203,146 @@ ${GENERIC_SECTION}
173387
174203
  Prefer many small targeted operations over one large blanket operation. Compress early and often — don't wait for warnings.`;
173388
174204
  }
173389
174205
 
174206
+ // ../plugin/src/hooks/magic-context/key-files-block.ts
174207
+ import { readFileSync as readFileSync7, realpathSync as realpathSync3 } from "node:fs";
174208
+ import { join as join16, sep as sep2 } from "node:path";
174209
+ init_logger();
174210
+ var cachedKeyFilesBySession = new Map;
174211
+ var staleUpdates = new Map;
174212
+ function staleKey(update) {
174213
+ return `${update.projectPath}\x00${update.path}\x00${update.generatedAtWitness}\x00${update.staleReason}`;
174214
+ }
174215
+ function queueStaleUpdate(projectPath, path5, generatedAtWitness, staleReason) {
174216
+ const update = { projectPath, path: path5, generatedAtWitness, staleReason };
174217
+ staleUpdates.set(staleKey(update), update);
174218
+ }
174219
+ function flushStaleUpdates(db) {
174220
+ if (staleUpdates.size === 0)
174221
+ return 0;
174222
+ const updates = [...staleUpdates.values()];
174223
+ staleUpdates.clear();
174224
+ const stmt = db.prepare(`UPDATE project_key_files
174225
+ SET stale_reason = ?1
174226
+ WHERE project_path = ?2
174227
+ AND path = ?3
174228
+ AND generated_at = ?4
174229
+ AND (stale_reason IS NULL OR stale_reason != ?1)`);
174230
+ let changed = 0;
174231
+ for (const update of updates) {
174232
+ try {
174233
+ changed += stmt.run(update.staleReason, update.projectPath, update.path, update.generatedAtWitness).changes;
174234
+ } catch (error51) {
174235
+ log("[key-files] flushStaleUpdates failed:", error51);
174236
+ }
174237
+ }
174238
+ return changed;
174239
+ }
174240
+ function clearKeyFilesCacheForSession(sessionId) {
174241
+ cachedKeyFilesBySession.delete(sessionId);
174242
+ }
174243
+ function isUnderProject(projectPath, absPath) {
174244
+ const root = realpathSync3(projectPath);
174245
+ return absPath.startsWith(root + sep2) || absPath === root;
174246
+ }
174247
+ function buildKeyFilesBlock(db, projectPath, config2 = { enabled: true, tokenBudget: 1e4 }) {
174248
+ if (!config2.enabled)
174249
+ return null;
174250
+ if (!isAftAvailable())
174251
+ return null;
174252
+ const rows = readCurrentKeyFiles(db, projectPath);
174253
+ if (rows.length === 0)
174254
+ return null;
174255
+ for (const row of rows) {
174256
+ if (row.staleReason !== null)
174257
+ continue;
174258
+ let nextStale = null;
174259
+ let observed = false;
174260
+ try {
174261
+ const absPath = join16(projectPath, row.path);
174262
+ const real = realpathSync3(absPath);
174263
+ if (!isUnderProject(projectPath, real)) {
174264
+ nextStale = "missing";
174265
+ observed = true;
174266
+ } else {
174267
+ const diskHash = sha256(readFileSync7(real));
174268
+ if (diskHash !== row.contentHash)
174269
+ nextStale = "content_drift";
174270
+ observed = true;
174271
+ }
174272
+ } catch (error51) {
174273
+ const code = error51?.code;
174274
+ if (code === "ENOENT" || code === "ELOOP") {
174275
+ nextStale = "missing";
174276
+ observed = true;
174277
+ } else {
174278
+ log(`[key-files] freshness check transient failure: ${row.path}: ${error51 instanceof Error ? error51.message : String(error51)}`);
174279
+ }
174280
+ }
174281
+ if (observed && nextStale !== null) {
174282
+ queueStaleUpdate(row.projectPath, row.path, row.generatedAt, nextStale);
174283
+ }
174284
+ }
174285
+ const blocks = [];
174286
+ let used = 0;
174287
+ for (const row of rows) {
174288
+ if (used + row.localTokenEstimate > config2.tokenBudget)
174289
+ break;
174290
+ blocks.push(` <key-file path="${escapeXmlAttr(row.path)}">
174291
+ ${escapeXmlContent(row.content)}
174292
+ </key-file>`);
174293
+ used += row.localTokenEstimate;
174294
+ }
174295
+ if (blocks.length === 0)
174296
+ return null;
174297
+ const rendered = `<key-files>
174298
+ ${blocks.join(`
174299
+ `)}
174300
+ </key-files>`;
174301
+ flushStaleUpdates(db);
174302
+ return rendered;
174303
+ }
174304
+ function readVersionedKeyFiles(args) {
174305
+ const config2 = args.config ?? { enabled: true, tokenBudget: 1e4 };
174306
+ if (args.sessionMeta.isSubagent)
174307
+ return null;
174308
+ if (!config2.enabled)
174309
+ return null;
174310
+ if (!isAftAvailable())
174311
+ return null;
174312
+ const projectPath = resolveProjectPath(args.directory ?? args.sessionMeta.sessionId);
174313
+ if (!projectPath)
174314
+ return null;
174315
+ const currentVersion = getKeyFilesVersion(args.db, projectPath);
174316
+ if (args.sessionId) {
174317
+ const cached2 = cachedKeyFilesBySession.get(args.sessionId);
174318
+ if (cached2 && !args.isCacheBusting && cached2.version === currentVersion) {
174319
+ return cached2.value;
174320
+ }
174321
+ }
174322
+ const value = buildKeyFilesBlock(args.db, projectPath, config2);
174323
+ if (args.sessionId) {
174324
+ cachedKeyFilesBySession.set(args.sessionId, { value, version: currentVersion });
174325
+ if (value)
174326
+ sessionLog(args.sessionId, `loaded key-files block (v${currentVersion}, ${value.length} chars)`);
174327
+ }
174328
+ return value;
174329
+ }
174330
+
173390
174331
  // src/system-prompt.ts
173391
174332
  init_logger();
173392
174333
  var PROJECT_DOCS_MARKER = "<project-docs>";
173393
174334
  var USER_PROFILE_MARKER = "<user-profile>";
173394
- var KEY_FILES_MARKER = "<key-files>";
173395
174335
  var stickyDateBySession = new Map;
173396
174336
  var cachedDocsBySession = new Map;
173397
174337
  var cachedUserProfileBySession = new Map;
173398
- var cachedKeyFilesBySession = new Map;
173399
174338
  var DOC_FILES = ["ARCHITECTURE.md", "STRUCTURE.md"];
173400
174339
  function readProjectDocs(directory) {
173401
174340
  const sections = [];
173402
174341
  for (const filename of DOC_FILES) {
173403
- const filePath = join13(directory, filename);
174342
+ const filePath = join17(directory, filename);
173404
174343
  try {
173405
- if (existsSync9(filePath)) {
173406
- const content = readFileSync4(filePath, "utf-8").trim();
174344
+ if (existsSync11(filePath)) {
174345
+ const content = readFileSync8(filePath, "utf-8").trim();
173407
174346
  if (content.length > 0) {
173408
174347
  sections.push(`<${filename}>
173409
174348
  ${content}
@@ -173432,56 +174371,6 @@ function buildUserProfileBlock(db) {
173432
174371
  ${items}
173433
174372
  </user-profile>`;
173434
174373
  }
173435
- function buildKeyFilesBlock(db, sessionId, directory, tokenBudget) {
173436
- const entries = getKeyFiles(db, sessionId);
173437
- if (entries.length === 0)
173438
- return null;
173439
- const sections = [];
173440
- const projectRoot = resolve2(directory);
173441
- let remainingBudgetTokens = tokenBudget;
173442
- for (const entry of entries) {
173443
- try {
173444
- const absPath = resolve2(directory, entry.filePath);
173445
- if (!absPath.startsWith(projectRoot + sep) && absPath !== projectRoot) {
173446
- log(`[magic-context-pi] key file path escapes project root, skipping: ${entry.filePath}`);
173447
- continue;
173448
- }
173449
- if (!existsSync9(absPath))
173450
- continue;
173451
- let realPath;
173452
- try {
173453
- realPath = realpathSync(absPath);
173454
- } catch {
173455
- continue;
173456
- }
173457
- if (!realPath.startsWith(projectRoot + sep) && realPath !== projectRoot) {
173458
- log(`[magic-context-pi] key file symlink escapes project root, skipping: ${entry.filePath} → ${realPath}`);
173459
- continue;
173460
- }
173461
- const content = readFileSync4(realPath, "utf-8").trim();
173462
- if (content.length === 0)
173463
- continue;
173464
- const fileTokens = estimateTokens(content);
173465
- if (fileTokens > remainingBudgetTokens) {
173466
- log(`[magic-context-pi] key file ${entry.filePath} exceeds remaining budget (${fileTokens} > ${remainingBudgetTokens}), skipping`);
173467
- continue;
173468
- }
173469
- remainingBudgetTokens -= fileTokens;
173470
- sections.push(`<file path="${escapeXmlAttr(entry.filePath)}">
173471
- ${escapeXmlContent(content)}
173472
- </file>`);
173473
- } catch (error51) {
173474
- log(`[magic-context-pi] failed to read key file ${entry.filePath}:`, error51);
173475
- }
173476
- }
173477
- if (sections.length === 0)
173478
- return null;
173479
- return `${KEY_FILES_MARKER}
173480
- ${sections.join(`
173481
-
173482
- `)}
173483
- </key-files>`;
173484
- }
173485
174374
  function buildMagicContextBlock(opts) {
173486
174375
  const sections = [];
173487
174376
  const sessionId = opts.sessionId;
@@ -173509,13 +174398,23 @@ function buildMagicContextBlock(opts) {
173509
174398
  sections.push(profileBlock);
173510
174399
  }
173511
174400
  if (opts.pinKeyFilesEnabled && sessionId) {
173512
- const keyFilesBlock = readCachedAdjunct({
173513
- cache: cachedKeyFilesBySession,
174401
+ let sessionMeta = null;
174402
+ try {
174403
+ sessionMeta = getOrCreateSessionMeta(opts.db, sessionId);
174404
+ } catch (error51) {
174405
+ sessionLog(sessionId, "Pi key-files session meta load failed:", error51);
174406
+ }
174407
+ const keyFilesBlock = sessionMeta ? readVersionedKeyFiles({
174408
+ db: opts.db,
173514
174409
  sessionId,
174410
+ sessionMeta,
174411
+ directory: opts.cwd,
173515
174412
  isCacheBusting,
173516
- compute: () => buildKeyFilesBlock(opts.db, sessionId, opts.cwd, opts.pinKeyFilesTokenBudget ?? 1e4),
173517
- describe: "key files"
173518
- });
174413
+ config: {
174414
+ enabled: opts.pinKeyFilesEnabled,
174415
+ tokenBudget: opts.pinKeyFilesTokenBudget ?? 1e4
174416
+ }
174417
+ }) : null;
173519
174418
  if (keyFilesBlock)
173520
174419
  sections.push(keyFilesBlock);
173521
174420
  }
@@ -173580,7 +174479,7 @@ function processSystemPromptForCache(args) {
173580
174479
  sessionLog(sessionId, `system prompt date frozen: real=${liveDate}, using=${stickyDate} (cache-stable pass)`);
173581
174480
  }
173582
174481
  }
173583
- const currentHash = createHash4("md5").update(frozenPrompt).digest("hex");
174482
+ const currentHash = createHash6("md5").update(frozenPrompt).digest("hex");
173584
174483
  const hashChanged = !isFirstHash && currentHash !== previousHash;
173585
174484
  if (hashChanged) {
173586
174485
  sessionLog(sessionId, `system prompt hash changed: ${previousHash} → ${currentHash} (len=${frozenPrompt.length})`);
@@ -173608,7 +174507,7 @@ function clearPiSystemPromptSession(sessionId) {
173608
174507
  stickyDateBySession.delete(sessionId);
173609
174508
  cachedDocsBySession.delete(sessionId);
173610
174509
  cachedUserProfileBySession.delete(sessionId);
173611
- cachedKeyFilesBySession.delete(sessionId);
174510
+ clearKeyFilesCacheForSession(sessionId);
173612
174511
  }
173613
174512
 
173614
174513
  // ../plugin/src/tools/ctx-expand/constants.ts
@@ -178498,6 +179397,70 @@ function createCtxSearchTool(deps) {
178498
179397
  };
178499
179398
  }
178500
179399
 
179400
+ // src/tools/todowrite.ts
179401
+ var STATUS_VALUES = [
179402
+ "pending",
179403
+ "in_progress",
179404
+ "completed",
179405
+ "cancelled"
179406
+ ];
179407
+ var PRIORITY_VALUES = ["high", "medium", "low"];
179408
+ var TodoItem = exports_typebox.Object({
179409
+ content: exports_typebox.String({ description: "Brief description of the task" }),
179410
+ status: exports_typebox.Union(STATUS_VALUES.map((v) => exports_typebox.Literal(v))),
179411
+ priority: exports_typebox.Optional(exports_typebox.Union(PRIORITY_VALUES.map((v) => exports_typebox.Literal(v)))),
179412
+ id: exports_typebox.Optional(exports_typebox.String({ description: "Optional stable id for the todo" }))
179413
+ });
179414
+ var TodowriteParams = exports_typebox.Object({
179415
+ todos: exports_typebox.Array(TodoItem, {
179416
+ description: "Replace the current task list with this complete set of todos. Include every task you intend to track this turn — pending, in_progress, completed, or cancelled — because the list overwrites previous state."
179417
+ })
179418
+ });
179419
+ var TOOL_DESCRIPTION = [
179420
+ "Manage your task list for this session.",
179421
+ "",
179422
+ "Use this tool to plan multi-step work, track in-flight tasks, and",
179423
+ "mark progress as you complete steps. Pass the COMPLETE updated list",
179424
+ "of todos every time — this tool replaces the prior list rather than",
179425
+ "appending to it.",
179426
+ "",
179427
+ "Task states:",
179428
+ " - pending: not started yet",
179429
+ " - in_progress: currently working on (limit to ONE task at a time)",
179430
+ " - completed: finished successfully",
179431
+ " - cancelled: no longer needed",
179432
+ "",
179433
+ "Use this tool proactively for non-trivial work spanning 3+ steps.",
179434
+ "Skip it for single-shot answers or trivial 1-2 step tasks."
179435
+ ].join(`
179436
+ `);
179437
+ function createTodowriteTool() {
179438
+ return {
179439
+ name: "todowrite",
179440
+ label: "Todos",
179441
+ description: TOOL_DESCRIPTION,
179442
+ parameters: TodowriteParams,
179443
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
179444
+ const todos = params.todos ?? [];
179445
+ const completed = todos.filter((t) => t.status === "completed").length;
179446
+ const active = todos.length - completed;
179447
+ return {
179448
+ content: [
179449
+ {
179450
+ type: "text",
179451
+ text: JSON.stringify(todos, null, 2)
179452
+ }
179453
+ ],
179454
+ details: {
179455
+ todos,
179456
+ title: `${active} todos`,
179457
+ truncated: false
179458
+ }
179459
+ };
179460
+ }
179461
+ };
179462
+ }
179463
+
178501
179464
  // src/tools/index.ts
178502
179465
  function registerMagicContextTools(pi, opts) {
178503
179466
  pi.registerTool(createCtxSearchTool({
@@ -178517,6 +179480,7 @@ function registerMagicContextTools(pi, opts) {
178517
179480
  dreamerEnabled: opts.dreamerEnabled ?? false
178518
179481
  }));
178519
179482
  pi.registerTool(createCtxExpandTool({ db: opts.db }));
179483
+ pi.registerTool(createTodowriteTool());
178520
179484
  if (opts.ctxReduceEnabled === true) {
178521
179485
  pi.registerTool(createCtxReduceTool({
178522
179486
  db: opts.db,
@@ -178631,7 +179595,7 @@ function resolveDreamerFromConfig(config2) {
178631
179595
  }
178632
179596
  async function src_default2(pi) {
178633
179597
  const storageDir = getMagicContextStorageDir();
178634
- const dbPath = join14(storageDir, "context.db");
179598
+ const dbPath = join18(storageDir, "context.db");
178635
179599
  let db;
178636
179600
  try {
178637
179601
  db = openDatabase();
@@ -178841,9 +179805,17 @@ ${block}` : event.systemPrompt;
178841
179805
  if (event.toolName === "todowrite") {
178842
179806
  const todoArgs = event.args;
178843
179807
  const todos = todoArgs?.todos;
179808
+ const sessionMeta = Array.isArray(todos) ? getOrCreateSessionMeta(db, sessionId) : null;
179809
+ if (sessionMeta && !sessionMeta.isSubagent) {
179810
+ const normalizedTodos = normalizeTodoStateJson(todos);
179811
+ if (normalizedTodos !== null) {
179812
+ updateSessionMeta(db, sessionId, {
179813
+ lastTodoState: normalizedTodos
179814
+ });
179815
+ }
179816
+ }
178844
179817
  if (Array.isArray(todos) && todos.length > 0 && todos.every((t) => t.status === "completed" || t.status === "cancelled")) {
178845
- const sessionMeta = getOrCreateSessionMeta(db, sessionId);
178846
- if (!sessionMeta.isSubagent) {
179818
+ if (sessionMeta && !sessionMeta.isSubagent) {
178847
179819
  onNoteTrigger(db, sessionId, "todos_complete");
178848
179820
  }
178849
179821
  }
@@ -178897,14 +179869,62 @@ ${block}` : event.systemPrompt;
178897
179869
  cacheTtlConfig: config2.cache_ttl
178898
179870
  });
178899
179871
  const piUsage = ctx.getContextUsage?.();
178900
- const updates = { lastResponseTime: Date.now() };
178901
- if (piUsage && typeof piUsage.percent === "number") {
178902
- updates.lastContextPercentage = piUsage.percent;
179872
+ const piContextWindow = piUsage && typeof piUsage.contextWindow === "number" ? piUsage.contextWindow : 0;
179873
+ let effectiveContextLimit = piContextWindow;
179874
+ try {
179875
+ const overflowState = getOverflowState(db, sessionId);
179876
+ if (overflowState.detectedContextLimit > 0) {
179877
+ effectiveContextLimit = effectiveContextLimit > 0 ? Math.min(effectiveContextLimit, overflowState.detectedContextLimit) : overflowState.detectedContextLimit;
179878
+ }
179879
+ } catch (err5) {
179880
+ warn("message_end: getOverflowState failed:", err5);
178903
179881
  }
178904
- if (piUsage && typeof piUsage.tokens === "number") {
179882
+ const usage = extractAssistantUsage(event.message);
179883
+ const pressure = computePiPressure(usage, effectiveContextLimit);
179884
+ const updates = { lastResponseTime: Date.now() };
179885
+ if (pressure) {
179886
+ updates.lastContextPercentage = pressure.percentage;
179887
+ updates.lastInputTokens = pressure.inputTokens;
179888
+ } else if (piUsage && typeof piUsage.tokens === "number") {
178905
179889
  updates.lastInputTokens = piUsage.tokens;
179890
+ if (piUsage.contextWindow > 0) {
179891
+ updates.lastContextPercentage = piUsage.tokens / piUsage.contextWindow * 100;
179892
+ }
178906
179893
  }
178907
179894
  updateSessionMeta(db, sessionId, updates);
179895
+ try {
179896
+ const sessionMetaForTodo = getOrCreateSessionMeta(db, sessionId);
179897
+ if (!sessionMetaForTodo.isSubagent) {
179898
+ const msg = event.message;
179899
+ if (msg && msg.role === "assistant" && Array.isArray(msg.content)) {
179900
+ for (const block of msg.content) {
179901
+ if (!block || typeof block !== "object")
179902
+ continue;
179903
+ const b = block;
179904
+ if (b.type !== "toolCall")
179905
+ continue;
179906
+ if (typeof b.name !== "string")
179907
+ continue;
179908
+ if (!/^todo.*write$|^write.*todo$|^todowrite$/i.test(b.name)) {
179909
+ continue;
179910
+ }
179911
+ const args = b.arguments;
179912
+ const todos = args?.todos;
179913
+ if (!Array.isArray(todos))
179914
+ continue;
179915
+ const normalized = normalizeTodoStateJson(todos);
179916
+ if (normalized === null)
179917
+ continue;
179918
+ updateSessionMeta(db, sessionId, {
179919
+ lastTodoState: normalized
179920
+ });
179921
+ break;
179922
+ }
179923
+ }
179924
+ }
179925
+ } catch (err5) {
179926
+ warn("message_end: synthetic todowrite capture failed:", err5);
179927
+ }
178908
179928
  } catch (err5) {
178909
179929
  warn("message_end: persist session_meta usage failed:", err5);
178910
179930
  }
@@ -178936,7 +179956,7 @@ ${block}` : event.systemPrompt;
178936
179956
  try {
178937
179957
  await Promise.race([
178938
179958
  awaitInFlightHistorians(),
178939
- new Promise((resolve3) => setTimeout(resolve3, SHUTDOWN_DRAIN_MS))
179959
+ new Promise((resolve4) => setTimeout(resolve4, SHUTDOWN_DRAIN_MS))
178940
179960
  ]);
178941
179961
  } catch (err5) {
178942
179962
  warn("shutdown: historian drain threw:", err5);
@@ -178944,7 +179964,7 @@ ${block}` : event.systemPrompt;
178944
179964
  try {
178945
179965
  await Promise.race([
178946
179966
  awaitInFlightDreamers(),
178947
- new Promise((resolve3) => setTimeout(resolve3, SHUTDOWN_DRAIN_MS))
179967
+ new Promise((resolve4) => setTimeout(resolve4, SHUTDOWN_DRAIN_MS))
178948
179968
  ]);
178949
179969
  } catch (err5) {
178950
179970
  warn("shutdown: dreamer drain threw:", err5);