crispy-recall 0.3.0 → 0.4.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/stop-hook.js CHANGED
@@ -34,6 +34,47 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
34
34
  ));
35
35
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
36
36
 
37
+ // src/paths.ts
38
+ function recallRoot() {
39
+ if (rootOverride)
40
+ return rootOverride;
41
+ const envRoot = process.env["RECALL_HOME"];
42
+ if (envRoot && envRoot.length > 0)
43
+ return envRoot;
44
+ return (0, import_node_path.join)((0, import_node_os.homedir)(), ".recall");
45
+ }
46
+ function remoteRoot() {
47
+ const env = process.env["RECALL_REMOTE_ROOT"];
48
+ if (env && env.length > 0)
49
+ return env;
50
+ return (0, import_node_path.join)(recallRoot(), "remote");
51
+ }
52
+ function dbPath() {
53
+ return (0, import_node_path.join)(recallRoot(), "recall.db");
54
+ }
55
+ function binDir() {
56
+ return (0, import_node_path.join)(recallRoot(), "bin");
57
+ }
58
+ function runDir() {
59
+ return (0, import_node_path.join)(recallRoot(), "run");
60
+ }
61
+ function logsDir() {
62
+ return (0, import_node_path.join)(recallRoot(), "logs");
63
+ }
64
+ function ensureDir() {
65
+ (0, import_node_fs.mkdirSync)(recallRoot(), { recursive: true });
66
+ }
67
+ var import_node_fs, import_node_os, import_node_path, rootOverride;
68
+ var init_paths = __esm({
69
+ "src/paths.ts"() {
70
+ "use strict";
71
+ import_node_fs = require("node:fs");
72
+ import_node_os = require("node:os");
73
+ import_node_path = require("node:path");
74
+ rootOverride = null;
75
+ }
76
+ });
77
+
37
78
  // src/log.ts
38
79
  function log(entry) {
39
80
  if (LEVEL_ORDER[entry.level] < _threshold)
@@ -68,38 +109,6 @@ var init_log = __esm({
68
109
  }
69
110
  });
70
111
 
71
- // src/paths.ts
72
- function recallRoot() {
73
- if (rootOverride)
74
- return rootOverride;
75
- const envRoot = process.env["RECALL_HOME"];
76
- if (envRoot && envRoot.length > 0)
77
- return envRoot;
78
- return (0, import_node_path.join)((0, import_node_os.homedir)(), ".recall");
79
- }
80
- function dbPath() {
81
- return (0, import_node_path.join)(recallRoot(), "recall.db");
82
- }
83
- function binDir() {
84
- return (0, import_node_path.join)(recallRoot(), "bin");
85
- }
86
- function logsDir() {
87
- return (0, import_node_path.join)(recallRoot(), "logs");
88
- }
89
- function ensureDir() {
90
- (0, import_node_fs.mkdirSync)(recallRoot(), { recursive: true });
91
- }
92
- var import_node_fs, import_node_os, import_node_path, rootOverride;
93
- var init_paths = __esm({
94
- "src/paths.ts"() {
95
- "use strict";
96
- import_node_fs = require("node:fs");
97
- import_node_os = require("node:os");
98
- import_node_path = require("node:path");
99
- rootOverride = null;
100
- }
101
- });
102
-
103
112
  // node_modules/better-sqlite3/lib/util.js
104
113
  var require_util = __commonJS({
105
114
  "node_modules/better-sqlite3/lib/util.js"(exports2) {
@@ -144,7 +153,7 @@ var require_sqlite_error = __commonJS({
144
153
  // node_modules/file-uri-to-path/index.js
145
154
  var require_file_uri_to_path = __commonJS({
146
155
  "node_modules/file-uri-to-path/index.js"(exports2, module2) {
147
- var sep = require("path").sep || "/";
156
+ var sep2 = require("path").sep || "/";
148
157
  module2.exports = fileUriToPath;
149
158
  function fileUriToPath(uri) {
150
159
  if ("string" != typeof uri || uri.length <= 7 || "file://" != uri.substring(0, 7)) {
@@ -157,15 +166,15 @@ var require_file_uri_to_path = __commonJS({
157
166
  if ("localhost" == host)
158
167
  host = "";
159
168
  if (host) {
160
- host = sep + sep + host;
169
+ host = sep2 + sep2 + host;
161
170
  }
162
171
  path = path.replace(/^(.+)\|/, "$1:");
163
- if (sep == "\\") {
172
+ if (sep2 == "\\") {
164
173
  path = path.replace(/\//g, "\\");
165
174
  }
166
175
  if (/^.+\:/.test(path)) {
167
176
  } else {
168
- path = sep + path;
177
+ path = sep2 + path;
169
178
  }
170
179
  return host + path;
171
180
  }
@@ -178,8 +187,8 @@ var require_bindings = __commonJS({
178
187
  var fs3 = require("fs");
179
188
  var path = require("path");
180
189
  var fileURLToPath = require_file_uri_to_path();
181
- var join4 = path.join;
182
- var dirname2 = path.dirname;
190
+ var join7 = path.join;
191
+ var dirname4 = path.dirname;
183
192
  var exists = fs3.accessSync && function(path2) {
184
193
  try {
185
194
  fs3.accessSync(path2);
@@ -239,7 +248,7 @@ var require_bindings = __commonJS({
239
248
  var requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;
240
249
  var tries = [], i = 0, l = opts.try.length, n, b, err;
241
250
  for (; i < l; i++) {
242
- n = join4.apply(
251
+ n = join7.apply(
243
252
  null,
244
253
  opts.try[i].map(function(p) {
245
254
  return opts[p] || p;
@@ -295,12 +304,12 @@ var require_bindings = __commonJS({
295
304
  return fileName;
296
305
  };
297
306
  exports2.getRoot = function getRoot(file) {
298
- var dir = dirname2(file), prev;
307
+ var dir = dirname4(file), prev;
299
308
  while (true) {
300
309
  if (dir === ".") {
301
310
  dir = process.cwd();
302
311
  }
303
- if (exists(join4(dir, "package.json")) || exists(join4(dir, "node_modules"))) {
312
+ if (exists(join7(dir, "package.json")) || exists(join7(dir, "node_modules"))) {
304
313
  return dir;
305
314
  }
306
315
  if (prev === dir) {
@@ -309,7 +318,7 @@ var require_bindings = __commonJS({
309
318
  );
310
319
  }
311
320
  prev = dir;
312
- dir = join4(dir, "..");
321
+ dir = join7(dir, "..");
313
322
  }
314
323
  };
315
324
  }
@@ -510,13 +519,13 @@ var require_backup = __commonJS({
510
519
  var runBackup = (backup, handler) => {
511
520
  let rate = 0;
512
521
  let useDefault = true;
513
- return new Promise((resolve, reject) => {
522
+ return new Promise((resolve3, reject) => {
514
523
  setImmediate(function step() {
515
524
  try {
516
525
  const progress = backup.transfer(rate);
517
526
  if (!progress.remainingPages) {
518
527
  backup.close();
519
- resolve(progress);
528
+ resolve3(progress);
520
529
  return;
521
530
  }
522
531
  if (useDefault) {
@@ -930,15 +939,141 @@ var init_quantize = __esm({
930
939
  }
931
940
  });
932
941
 
942
+ // src/installer/stable-node.ts
943
+ var init_stable_node = __esm({
944
+ "src/installer/stable-node.ts"() {
945
+ "use strict";
946
+ }
947
+ });
948
+
949
+ // src/installer/settings-merge.ts
950
+ var init_settings_merge = __esm({
951
+ "src/installer/settings-merge.ts"() {
952
+ "use strict";
953
+ init_stable_node();
954
+ init_config();
955
+ init_paths();
956
+ }
957
+ });
958
+
959
+ // src/installer/config.ts
960
+ function configPath() {
961
+ return (0, import_node_path6.join)(recallRoot(), "config.json");
962
+ }
963
+ function satelliteTokenPath() {
964
+ return (0, import_node_path6.join)(recallRoot(), "satellite-token");
965
+ }
966
+ function readConfig() {
967
+ const p = configPath();
968
+ if (!(0, import_node_fs6.existsSync)(p))
969
+ return null;
970
+ try {
971
+ return JSON.parse((0, import_node_fs6.readFileSync)(p, "utf-8"));
972
+ } catch (err) {
973
+ log({
974
+ source: "installer/config",
975
+ level: "warn",
976
+ summary: `config.json unparseable (${err.message}) \u2014 treating as absent`
977
+ });
978
+ return null;
979
+ }
980
+ }
981
+ function readSatelliteConfig() {
982
+ const sat = readConfig()?.satellite;
983
+ if (!sat || typeof sat.hubUrl !== "string" || sat.hubUrl.length === 0)
984
+ return null;
985
+ let token = null;
986
+ try {
987
+ const raw = (0, import_node_fs6.readFileSync)(satelliteTokenPath(), "utf-8").trim();
988
+ if (raw.length > 0)
989
+ token = raw;
990
+ } catch {
991
+ }
992
+ return { hubUrl: sat.hubUrl, host: typeof sat.host === "string" ? sat.host : "", token };
993
+ }
994
+ var import_node_fs6, import_node_path6;
995
+ var init_config = __esm({
996
+ "src/installer/config.ts"() {
997
+ "use strict";
998
+ import_node_fs6 = require("node:fs");
999
+ import_node_path6 = require("node:path");
1000
+ init_paths();
1001
+ init_log();
1002
+ init_settings_merge();
1003
+ }
1004
+ });
1005
+
933
1006
  // src/hooks/stop-hook.ts
934
1007
  var stop_hook_exports = {};
935
1008
  __export(stop_hook_exports, {
936
1009
  resolveIngestTarget: () => resolveIngestTarget,
1010
+ retryBusyIngest: () => retryBusyIngest,
937
1011
  vendorForTranscript: () => vendorForTranscript
938
1012
  });
939
1013
  module.exports = __toCommonJS(stop_hook_exports);
940
1014
 
1015
+ // src/recall/embed-failures.ts
1016
+ init_paths();
1017
+
1018
+ // src/adapters/system-context.ts
1019
+ function firstTextContent(message) {
1020
+ if (!message)
1021
+ return void 0;
1022
+ const content = message.content;
1023
+ if (typeof content === "string")
1024
+ return content;
1025
+ if (Array.isArray(content)) {
1026
+ const firstText = content.find(
1027
+ (b) => typeof b === "object" && b !== null && b.type === "text"
1028
+ );
1029
+ if (firstText && "text" in firstText) {
1030
+ return firstText.text;
1031
+ }
1032
+ }
1033
+ return void 0;
1034
+ }
1035
+ function isSystemContextContent(message) {
1036
+ if (!message || message.role !== "user")
1037
+ return false;
1038
+ const text = firstTextContent(message);
1039
+ if (!text)
1040
+ return false;
1041
+ if (text.startsWith("<system-reminder>"))
1042
+ return true;
1043
+ if (text.startsWith("<environment_context>"))
1044
+ return true;
1045
+ if (text.startsWith("<INSTRUCTIONS>"))
1046
+ return true;
1047
+ if (text.startsWith("# AGENTS.md instructions for"))
1048
+ return true;
1049
+ if (text.startsWith("<context>"))
1050
+ return true;
1051
+ if (text.startsWith("<task-notification>"))
1052
+ return true;
1053
+ if (text.startsWith("<command-name>"))
1054
+ return true;
1055
+ if (text.startsWith("<local-command-stdout>"))
1056
+ return true;
1057
+ if (text.startsWith("<local-command-caveat>"))
1058
+ return true;
1059
+ return false;
1060
+ }
1061
+
941
1062
  // src/recall/transcript-utils.ts
1063
+ var META_KEEP_PREFIXES = [
1064
+ "<task-notification>",
1065
+ "[SYSTEM NOTIFICATION",
1066
+ "Another Claude session sent a message"
1067
+ ];
1068
+ function shouldDropAsMeta(entry) {
1069
+ if (entry.isMeta !== true)
1070
+ return false;
1071
+ const text = firstTextContent(entry.message);
1072
+ if (text && META_KEEP_PREFIXES.some((prefix) => text.startsWith(prefix))) {
1073
+ return false;
1074
+ }
1075
+ return true;
1076
+ }
942
1077
  function stripToolContent(entries) {
943
1078
  const out = [];
944
1079
  for (const entry of entries) {
@@ -988,11 +1123,12 @@ Run \`recall doctor\` for details.`
988
1123
  }
989
1124
  };
990
1125
  var MigrationPendingError = class extends Error {
991
- constructor(dbPath2) {
1126
+ constructor(dbPath2, kind = "retrieval-class") {
992
1127
  super(
993
- "recall: this database needs a one-time schema migration \u2014 run `recall install` to finish it. (Normal commands refuse to rewrite the index unattended.)"
1128
+ kind === "codex-rekey" ? "recall: this database needs a one-time Codex message-id migration \u2014 run `recall install` (or `recall repair --rekey-codex`) to finish it. (Normal commands refuse to rewrite the index unattended.)" : "recall: this database needs a one-time schema migration \u2014 run `recall install` to finish it. (Normal commands refuse to rewrite the index unattended.)"
994
1129
  );
995
1130
  this.dbPath = dbPath2;
1131
+ this.kind = kind;
996
1132
  this.name = "MigrationPendingError";
997
1133
  }
998
1134
  };
@@ -1033,16 +1169,43 @@ function getDb(dbPath2, opts) {
1033
1169
  }
1034
1170
  db = adapter;
1035
1171
  currentDbPath = dbPath2;
1172
+ ensureStemScratch(db);
1036
1173
  log({ source: "db", level: "info", summary: `DB: opened pending-migration DB at ${dbPath2} (installer mode)` });
1037
1174
  return db;
1038
1175
  }
1176
+ if (isCodexRekeyPending(adapter) && !opts?.allowPendingMigration) {
1177
+ raw.close();
1178
+ throw new MigrationPendingError(dbPath2, "codex-rekey");
1179
+ }
1039
1180
  db = adapter;
1040
1181
  currentDbPath = dbPath2;
1041
- ensureSchema(db);
1182
+ try {
1183
+ ensureSchema(db);
1184
+ ensureStemScratch(db);
1185
+ } catch (error) {
1186
+ closeDb();
1187
+ throw error;
1188
+ }
1042
1189
  log({ source: "db", level: "info", summary: `DB: initialized at ${dbPath2}` });
1043
1190
  return db;
1044
1191
  }
1192
+ function ensureStemScratch(db3) {
1193
+ db3.exec(`
1194
+ CREATE VIRTUAL TABLE IF NOT EXISTS temp._stem USING fts5(
1195
+ t, tokenize='porter unicode61'
1196
+ );
1197
+ CREATE VIRTUAL TABLE IF NOT EXISTS temp._stem_vocab
1198
+ USING fts5vocab(temp, _stem, 'row');
1199
+ `);
1200
+ }
1045
1201
  var RETRIEVAL_MIGRATION_KEY = "retrieval_class_migration";
1202
+ function rethrowIfBusy(err) {
1203
+ const code = err?.code;
1204
+ const message = err?.message;
1205
+ if (typeof code === "string" && code.startsWith("SQLITE_BUSY") || typeof message === "string" && /database is locked|SQLITE_BUSY/i.test(message))
1206
+ throw err;
1207
+ }
1208
+ var PROJECT_KEY_BACKFILL_KEY = "project_key_backfill";
1046
1209
  function isRetrievalMigrationPending(d) {
1047
1210
  try {
1048
1211
  const hasMessages = d.get(
@@ -1060,13 +1223,40 @@ function isRetrievalMigrationPending(d) {
1060
1223
  [RETRIEVAL_MIGRATION_KEY]
1061
1224
  );
1062
1225
  return row?.value !== "complete";
1063
- } catch {
1226
+ } catch (err) {
1227
+ rethrowIfBusy(err);
1228
+ return true;
1229
+ }
1230
+ }
1231
+ var CODEX_REKEY_MIGRATION_KEY = "codex_message_id_v2";
1232
+ function isCodexRekeyPending(d) {
1233
+ try {
1234
+ const hasMessages = d.get(
1235
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'messages'`
1236
+ );
1237
+ if (!hasMessages)
1238
+ return false;
1239
+ const hasMeta = d.get(
1240
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'schema_meta'`
1241
+ );
1242
+ if (!hasMeta)
1243
+ return true;
1244
+ const row = d.get(
1245
+ `SELECT value FROM schema_meta WHERE key = ?`,
1246
+ [CODEX_REKEY_MIGRATION_KEY]
1247
+ );
1248
+ return row?.value !== "complete";
1249
+ } catch (err) {
1250
+ rethrowIfBusy(err);
1064
1251
  return true;
1065
1252
  }
1066
1253
  }
1067
1254
  function closeDb() {
1068
- if (db) {
1255
+ if (!db)
1256
+ return;
1257
+ try {
1069
1258
  db.close();
1259
+ } finally {
1070
1260
  db = null;
1071
1261
  currentDbPath = null;
1072
1262
  }
@@ -1186,6 +1376,7 @@ var RETRIEVAL_SCHEMA_DDL = {
1186
1376
  created_at INTEGER NOT NULL,
1187
1377
  message_role TEXT,
1188
1378
  retrieval_class TEXT NOT NULL DEFAULT 'hot',
1379
+ project_key TEXT,
1189
1380
  UNIQUE(session_id, message_id)
1190
1381
  );
1191
1382
 
@@ -1275,15 +1466,18 @@ var RETRIEVAL_SCHEMA_DDL = {
1275
1466
  `
1276
1467
  };
1277
1468
  function ensureSchema(db3) {
1469
+ const fresh = !db3.get(
1470
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'messages'`
1471
+ );
1278
1472
  db3.exec("BEGIN IMMEDIATE");
1279
1473
  try {
1280
1474
  db3.exec(RETRIEVAL_SCHEMA_DDL.tables);
1281
1475
  db3.exec(RETRIEVAL_SCHEMA_DDL.fts);
1282
1476
  db3.exec(`
1283
- CREATE VIRTUAL TABLE IF NOT EXISTS _stem USING fts5(
1477
+ CREATE VIRTUAL TABLE IF NOT EXISTS main._stem USING fts5(
1284
1478
  t, tokenize='porter unicode61'
1285
1479
  );
1286
- CREATE VIRTUAL TABLE IF NOT EXISTS _stem_vocab
1480
+ CREATE VIRTUAL TABLE IF NOT EXISTS main._stem_vocab
1287
1481
  USING fts5vocab(_stem, 'row');
1288
1482
  `);
1289
1483
  db3.exec(`
@@ -1304,6 +1498,16 @@ function ensureSchema(db3) {
1304
1498
  throw e;
1305
1499
  }
1306
1500
  }
1501
+ const hasProjectKey = () => db3.all(`PRAGMA table_info(messages)`).some((c) => c.name === "project_key");
1502
+ if (!hasProjectKey()) {
1503
+ try {
1504
+ db3.exec(`ALTER TABLE messages ADD COLUMN project_key TEXT`);
1505
+ } catch (e) {
1506
+ if (!hasProjectKey())
1507
+ throw e;
1508
+ }
1509
+ }
1510
+ db3.exec(`CREATE INDEX IF NOT EXISTS idx_messages_project_key ON messages(project_key)`);
1307
1511
  db3.exec(`
1308
1512
  CREATE TABLE IF NOT EXISTS ingest_watermark (
1309
1513
  transcript_path TEXT PRIMARY KEY,
@@ -1317,6 +1521,18 @@ function ensureSchema(db3) {
1317
1521
  INSERT OR IGNORE INTO schema_meta(key, value)
1318
1522
  VALUES ('${RETRIEVAL_MIGRATION_KEY}', 'complete');
1319
1523
  `);
1524
+ if (fresh) {
1525
+ db3.exec(`
1526
+ INSERT OR IGNORE INTO schema_meta(key, value)
1527
+ VALUES ('${CODEX_REKEY_MIGRATION_KEY}', 'complete');
1528
+ `);
1529
+ }
1530
+ if (fresh) {
1531
+ db3.exec(`
1532
+ INSERT OR IGNORE INTO schema_meta(key, value)
1533
+ VALUES ('${PROJECT_KEY_BACKFILL_KEY}', 'complete');
1534
+ `);
1535
+ }
1320
1536
  db3.exec("COMMIT");
1321
1537
  } catch (e) {
1322
1538
  try {
@@ -1348,10 +1564,11 @@ function ensureDir2() {
1348
1564
  }
1349
1565
  function insertMessages(messages, opts) {
1350
1566
  if (messages.length === 0 && !opts?.replaceSessionId && !opts?.provenance)
1351
- return;
1567
+ return 0;
1352
1568
  ensureDir2();
1353
1569
  const d = db2();
1354
1570
  d.exec("BEGIN IMMEDIATE");
1571
+ let inserted = 0;
1355
1572
  try {
1356
1573
  if (opts?.replaceSessionId) {
1357
1574
  d.run(
@@ -1363,20 +1580,65 @@ function insertMessages(messages, opts) {
1363
1580
  }
1364
1581
  const stmt = d.prepare(
1365
1582
  `INSERT OR IGNORE INTO messages
1366
- (message_id, session_id, message_seq, message_text, project_id, created_at, message_role, retrieval_class)
1367
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1583
+ (message_id, session_id, message_seq, message_text, project_id, created_at, message_role, retrieval_class, project_key)
1584
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
1368
1585
  );
1586
+ const invalidatedSessions = /* @__PURE__ */ new Set();
1587
+ const sourceIds = /* @__PURE__ */ new Map();
1369
1588
  for (const m of messages) {
1370
- stmt.run([
1371
- m.message_id,
1589
+ const ids = sourceIds.get(m.session_id) ?? /* @__PURE__ */ new Set();
1590
+ ids.add(m.message_id);
1591
+ ids.add(`session:${m.session_id}:${m.message_id}`);
1592
+ sourceIds.set(m.session_id, ids);
1593
+ }
1594
+ if (opts?.provenance && opts.sourceMessageIds) {
1595
+ const sid = opts.provenance.sessionId;
1596
+ const ids = sourceIds.get(sid);
1597
+ for (const id of opts.sourceMessageIds) {
1598
+ ids?.add(id);
1599
+ ids?.add(`session:${sid}:${id}`);
1600
+ }
1601
+ }
1602
+ const partialSessions = /* @__PURE__ */ new Map();
1603
+ const maxStoredSeq = /* @__PURE__ */ new Map();
1604
+ for (const [sid, ids] of sourceIds) {
1605
+ const stored = d.all("SELECT message_id, message_seq FROM messages WHERE session_id = ?", [sid]);
1606
+ const max = stored.reduce((acc, r) => Math.max(acc, r.message_seq), -1);
1607
+ maxStoredSeq.set(sid, max);
1608
+ if (stored.some((r) => !ids.has(r.message_id)))
1609
+ partialSessions.set(sid, max);
1610
+ }
1611
+ for (const m of messages) {
1612
+ const scopedId = `session:${m.session_id}:${m.message_id}`;
1613
+ const scoped = d.get("SELECT message_id FROM messages WHERE message_id = ? AND session_id = ?", [scopedId, m.session_id]);
1614
+ const owner = d.get("SELECT session_id FROM messages WHERE message_id = ?", [m.message_id]);
1615
+ const id = scoped || owner && owner.session_id !== m.session_id ? scopedId : m.message_id;
1616
+ const existing = d.get("SELECT message_seq FROM messages WHERE message_id = ? AND session_id = ?", [id, m.session_id]);
1617
+ let sequence = m.message_seq;
1618
+ if (partialSessions.has(m.session_id)) {
1619
+ sequence = existing?.message_seq ?? partialSessions.get(m.session_id) + 1;
1620
+ partialSessions.set(m.session_id, Math.max(partialSessions.get(m.session_id), sequence));
1621
+ }
1622
+ const insertsPredecessor = !existing && sequence <= (maxStoredSeq.get(m.session_id) ?? -1);
1623
+ if (existing && existing.message_seq !== sequence || insertsPredecessor) {
1624
+ if (!invalidatedSessions.has(m.session_id)) {
1625
+ d.run("DELETE FROM message_vectors WHERE message_id IN (SELECT message_id FROM messages WHERE session_id = ?)", [m.session_id]);
1626
+ invalidatedSessions.add(m.session_id);
1627
+ }
1628
+ d.run("UPDATE messages SET message_seq = ? WHERE message_id = ? AND session_id = ?", [sequence, id, m.session_id]);
1629
+ }
1630
+ maxStoredSeq.set(m.session_id, Math.max(maxStoredSeq.get(m.session_id) ?? -1, sequence));
1631
+ inserted += stmt.run([
1632
+ id,
1372
1633
  m.session_id,
1373
- m.message_seq,
1634
+ sequence,
1374
1635
  m.message_text,
1375
1636
  m.project_id,
1376
1637
  m.created_at,
1377
1638
  m.message_role,
1378
- m.retrieval_class ?? "hot"
1379
- ]);
1639
+ m.retrieval_class ?? "hot",
1640
+ m.project_key ?? null
1641
+ ]).changes;
1380
1642
  }
1381
1643
  if (opts?.provenance) {
1382
1644
  const p = opts.provenance;
@@ -1416,6 +1678,15 @@ function insertMessages(messages, opts) {
1416
1678
  );
1417
1679
  }
1418
1680
  }
1681
+ if (opts?.adopt) {
1682
+ const { sessionId, projectKey, projectId } = opts.adopt;
1683
+ if (projectKey !== null) {
1684
+ d.run("UPDATE messages SET project_key = ? WHERE session_id = ? AND project_key IS NULL", [projectKey, sessionId]);
1685
+ }
1686
+ if (projectId !== null && projectId !== "") {
1687
+ d.run("UPDATE messages SET project_id = ? WHERE session_id = ? AND (project_id IS NULL OR project_id = '')", [projectId, sessionId]);
1688
+ }
1689
+ }
1419
1690
  for (const a of opts?.aliases ?? []) {
1420
1691
  d.run(
1421
1692
  `INSERT OR REPLACE INTO session_aliases (alias_id, session_id, source, created_at)
@@ -1424,11 +1695,23 @@ function insertMessages(messages, opts) {
1424
1695
  );
1425
1696
  }
1426
1697
  d.exec("COMMIT");
1698
+ return inserted;
1427
1699
  } catch (e) {
1428
1700
  d.exec("ROLLBACK");
1429
1701
  throw e;
1430
1702
  }
1431
1703
  }
1704
+ function hasSessionMessages(sessionId) {
1705
+ try {
1706
+ const row = db2().get(
1707
+ "SELECT 1 FROM messages WHERE session_id = ? LIMIT 1",
1708
+ [sessionId]
1709
+ );
1710
+ return row != null;
1711
+ } catch {
1712
+ return false;
1713
+ }
1714
+ }
1432
1715
 
1433
1716
  // src/recall/session-classifier.ts
1434
1717
  init_paths();
@@ -1437,6 +1720,83 @@ init_log();
1437
1720
  // src/adapters/codex/codex-jsonl-reader.ts
1438
1721
  var fs = __toESM(require("fs"));
1439
1722
  init_log();
1723
+
1724
+ // src/recall/transcript-roots.ts
1725
+ var import_node_os2 = require("node:os");
1726
+ var import_node_path4 = require("node:path");
1727
+
1728
+ // src/hub/runtime.ts
1729
+ var import_node_fs3 = require("node:fs");
1730
+ var import_node_path3 = require("node:path");
1731
+ init_paths();
1732
+ var STALE_RECORD_MS = 60 * 60 * 1e3;
1733
+ var REFUSED_RECENT_MAX = 5;
1734
+ function hubHostsPath() {
1735
+ return (0, import_node_path3.join)(runDir(), "hub-hosts.json");
1736
+ }
1737
+ function readHostRecords() {
1738
+ try {
1739
+ const parsed = JSON.parse((0, import_node_fs3.readFileSync)(hubHostsPath(), "utf-8"));
1740
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1741
+ return {};
1742
+ const out = {};
1743
+ for (const [host, rec] of Object.entries(parsed)) {
1744
+ if (!rec || typeof rec !== "object")
1745
+ continue;
1746
+ const r = rec;
1747
+ out[host] = {
1748
+ ...typeof r["lastPushAt"] === "string" ? { lastPushAt: r["lastPushAt"] } : {},
1749
+ ...typeof r["lastQueryAt"] === "string" ? { lastQueryAt: r["lastQueryAt"] } : {},
1750
+ ...typeof r["lastFullManifestAt"] === "string" ? { lastFullManifestAt: r["lastFullManifestAt"] } : {},
1751
+ refusedCollisions: typeof r["refusedCollisions"] === "number" ? r["refusedCollisions"] : 0,
1752
+ refusedRecent: Array.isArray(r["refusedRecent"]) ? r["refusedRecent"].filter((v) => typeof v === "string").slice(0, REFUSED_RECENT_MAX) : []
1753
+ };
1754
+ }
1755
+ return out;
1756
+ } catch {
1757
+ return {};
1758
+ }
1759
+ }
1760
+
1761
+ // src/recall/transcript-roots.ts
1762
+ init_log();
1763
+ var DRVFS = /^\/mnt\/[a-zA-Z]\//;
1764
+ var warned = /* @__PURE__ */ new Set();
1765
+ function defaultCodexRoot() {
1766
+ return (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".codex");
1767
+ }
1768
+ function guardedHosts(override) {
1769
+ if (process.platform !== "linux")
1770
+ return [];
1771
+ const abs = (0, import_node_path4.resolve)(override).replace(/\\/g, "/");
1772
+ if (!DRVFS.test(abs.endsWith("/") ? abs : `${abs}/`))
1773
+ return [];
1774
+ return Object.keys(readHostRecords());
1775
+ }
1776
+ function resolveRoot(envVar, fallback) {
1777
+ const override = process.env[envVar];
1778
+ if (!override || override.length === 0)
1779
+ return fallback;
1780
+ const hosts = guardedHosts(override);
1781
+ if (hosts.length === 0)
1782
+ return override;
1783
+ if (!warned.has(envVar)) {
1784
+ warned.add(envVar);
1785
+ log({
1786
+ level: "warn",
1787
+ source: "transcript-roots",
1788
+ summary: `ignoring ${envVar}=${override}: this hub serves satellite ${hosts.join(", ")} and that tree is pushed by a satellite; set the variable elsewhere or unset it`
1789
+ });
1790
+ }
1791
+ return fallback;
1792
+ }
1793
+ function codexRoot() {
1794
+ return resolveRoot("CODEX_HOME", defaultCodexRoot());
1795
+ }
1796
+
1797
+ // src/adapters/codex/codex-jsonl-reader.ts
1798
+ var META_READ_LIMIT = 256 * 1024;
1799
+ var META_CHUNK_SIZE = 8192;
1440
1800
  function parseCodexJsonlFile(filepath) {
1441
1801
  try {
1442
1802
  const content = fs.readFileSync(filepath, "utf-8");
@@ -1456,20 +1816,39 @@ function parseCodexJsonlFile(filepath) {
1456
1816
  return records;
1457
1817
  } catch (error) {
1458
1818
  log({ level: "error", source: "codex-jsonl-reader", summary: `Failed to read ${filepath}: ${error instanceof Error ? error.message : String(error)}`, data: { filepath, error: String(error) } });
1459
- return [];
1819
+ throw error;
1460
1820
  }
1461
1821
  }
1462
1822
  function extractCodexSessionMeta(filepath) {
1463
1823
  let fd = null;
1464
1824
  try {
1465
1825
  fd = fs.openSync(filepath, "r");
1466
- const buffer = Buffer.alloc(8192);
1467
- const bytesRead = fs.readSync(fd, buffer, 0, 8192, 0);
1468
- if (bytesRead === 0)
1826
+ const chunks = [];
1827
+ let position = 0;
1828
+ let newlineIdx = -1;
1829
+ let atEof = false;
1830
+ while (position < META_READ_LIMIT) {
1831
+ const chunk = Buffer.alloc(Math.min(META_CHUNK_SIZE, META_READ_LIMIT - position));
1832
+ const bytesRead = fs.readSync(fd, chunk, 0, chunk.length, position);
1833
+ if (bytesRead === 0) {
1834
+ atEof = true;
1835
+ break;
1836
+ }
1837
+ const slice = chunk.subarray(0, bytesRead);
1838
+ const idx = slice.indexOf(10);
1839
+ chunks.push(slice);
1840
+ if (idx >= 0) {
1841
+ newlineIdx = position + idx;
1842
+ break;
1843
+ }
1844
+ position += bytesRead;
1845
+ }
1846
+ if (chunks.length === 0)
1847
+ return null;
1848
+ if (newlineIdx < 0 && !atEof)
1469
1849
  return null;
1470
- const content = buffer.toString("utf-8", 0, bytesRead);
1471
- const newlineIdx = content.indexOf("\n");
1472
- const firstLine = newlineIdx >= 0 ? content.slice(0, newlineIdx) : content;
1850
+ const joined = Buffer.concat(chunks);
1851
+ const firstLine = (newlineIdx >= 0 ? joined.subarray(0, newlineIdx) : joined).toString("utf-8");
1473
1852
  const record = JSON.parse(firstLine.trim());
1474
1853
  if (record.type !== "session_meta")
1475
1854
  return null;
@@ -1661,7 +2040,10 @@ function parseSubagentSource(source, _transcriptPath) {
1661
2040
  const looksSubagent = subRaw !== void 0 || typeTag === "subagent";
1662
2041
  if (!looksSubagent)
1663
2042
  return none;
1664
- if (subRaw !== void 0 && (typeof subRaw !== "object" || subRaw === null)) {
2043
+ if (typeof subRaw === "string" && ["review", "compact", "memory_consolidation"].includes(subRaw)) {
2044
+ return { ...none, isSubagent: true, meta: { type: subRaw } };
2045
+ }
2046
+ if (subRaw !== void 0 && (typeof subRaw !== "object" || subRaw === null || Array.isArray(subRaw))) {
1665
2047
  return { ...none, malformed: true };
1666
2048
  }
1667
2049
  const sub = subRaw ?? {};
@@ -1670,7 +2052,7 @@ function parseSubagentSource(source, _transcriptPath) {
1670
2052
  const parentThreadId = typeof spawn2.parent_thread_id === "string" && spawn2.parent_thread_id ? spawn2.parent_thread_id : null;
1671
2053
  const depth = typeof spawn2.depth === "number" && Number.isFinite(spawn2.depth) ? spawn2.depth : null;
1672
2054
  const meta = {};
1673
- for (const k of ["agent_path", "agent_type", "agent_name", "path", "type", "name"]) {
2055
+ for (const k of ["agent_path", "agent_type", "agent_name", "agent_nickname", "agent_role", "path", "type", "name"]) {
1674
2056
  const v = spawn2[k] ?? sub[k];
1675
2057
  if (typeof v === "string" || typeof v === "number")
1676
2058
  meta[k] = v;
@@ -1698,6 +2080,255 @@ function normalizePath(p) {
1698
2080
  return normalized;
1699
2081
  }
1700
2082
 
2083
+ // src/recall/project-key.ts
2084
+ var import_node_fs4 = require("node:fs");
2085
+ var import_node_os3 = require("node:os");
2086
+ var import_node_child_process = require("node:child_process");
2087
+ var cache = /* @__PURE__ */ new Map();
2088
+ var GIT_TIMEOUT_MS = 3e3;
2089
+ function runGit(cwd, args, timeout) {
2090
+ return (0, import_node_child_process.spawnSync)("git", args, {
2091
+ cwd,
2092
+ encoding: "utf8",
2093
+ stdio: ["ignore", "pipe", "pipe"],
2094
+ ...timeout === void 0 ? {} : { timeout },
2095
+ windowsHide: true
2096
+ });
2097
+ }
2098
+ function isTransient(r) {
2099
+ const code = r.error?.code;
2100
+ if (code !== void 0 && code !== "ENOENT")
2101
+ return true;
2102
+ return r.signal != null;
2103
+ }
2104
+ function foldKeyPath(p, platform = process.platform) {
2105
+ if (/^[A-Za-z]:\//.test(p))
2106
+ return p.toLowerCase();
2107
+ if (/^\/(?!\/)/.test(p))
2108
+ return p;
2109
+ return platform === "win32" ? p.toLowerCase() : p;
2110
+ }
2111
+ function wslUncToPosix(p) {
2112
+ let s = p.trim();
2113
+ if (/^[\\/]{2}\?[\\/]UNC[\\/]/i.test(s))
2114
+ s = "//" + s.slice(8);
2115
+ else if (/^[\\/]{2}\?[\\/]/.test(s))
2116
+ s = s.slice(4);
2117
+ const m = /^[\\/]{2}(?:wsl\$|wsl\.localhost)[\\/]+([^\\/]+)(?:[\\/]+(.*))?$/i.exec(s);
2118
+ if (!m)
2119
+ return void 0;
2120
+ const rest = (m[2] ?? "").replace(/[\\/]+/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
2121
+ return { distro: m[1], posix: "/" + rest };
2122
+ }
2123
+ var MAX_RESOLVE_DEPTH = 64;
2124
+ function resolveCaseInsensitive(p) {
2125
+ if ((0, import_node_fs4.existsSync)(p))
2126
+ return p;
2127
+ const home = (0, import_node_os3.homedir)();
2128
+ if (!home || home === "/")
2129
+ return void 0;
2130
+ const prefix = home.replace(/\/+$/, "") + "/";
2131
+ if (!p.toLowerCase().startsWith(prefix.toLowerCase()))
2132
+ return void 0;
2133
+ let current = prefix.slice(0, -1);
2134
+ if (!(0, import_node_fs4.existsSync)(current))
2135
+ return void 0;
2136
+ const segments = p.slice(prefix.length).split("/").filter((seg) => seg.length > 0);
2137
+ if (segments.length > MAX_RESOLVE_DEPTH)
2138
+ return void 0;
2139
+ for (const seg of segments) {
2140
+ const exact = current + "/" + seg;
2141
+ if ((0, import_node_fs4.existsSync)(exact)) {
2142
+ current = exact;
2143
+ continue;
2144
+ }
2145
+ let entries;
2146
+ try {
2147
+ entries = (0, import_node_fs4.readdirSync)(current);
2148
+ } catch {
2149
+ return void 0;
2150
+ }
2151
+ const lower = seg.toLowerCase();
2152
+ const hits = entries.filter((e) => e.toLowerCase() === lower);
2153
+ if (hits.length !== 1)
2154
+ return void 0;
2155
+ current = current + "/" + hits[0];
2156
+ }
2157
+ return (0, import_node_fs4.existsSync)(current) ? current : void 0;
2158
+ }
2159
+ function pathKey(p) {
2160
+ const unc = wslUncToPosix(p);
2161
+ if (unc)
2162
+ return "path:" + unc.posix;
2163
+ return "path:" + foldKeyPath(normalizePath(p));
2164
+ }
2165
+ var upgradeCache = /* @__PURE__ */ new Map();
2166
+ function upgradeLocalPathKey(key) {
2167
+ if (!key.startsWith("path:"))
2168
+ return key;
2169
+ const hit = upgradeCache.get(key);
2170
+ if (hit !== void 0)
2171
+ return hit;
2172
+ const answer = (() => {
2173
+ const raw = key.slice("path:".length);
2174
+ const unc = wslUncToPosix(raw);
2175
+ const p = unc ? unc.posix : raw;
2176
+ if (!/^\/(?!\/)/.test(p))
2177
+ return key;
2178
+ const dir = resolveCaseInsensitive(p);
2179
+ if (dir === void 0)
2180
+ return key;
2181
+ const r = deriveProjectKey(dir);
2182
+ if (r.key && (r.kind === "git" || r.kind === "origin"))
2183
+ return r.key;
2184
+ return key;
2185
+ })();
2186
+ upgradeCache.set(key, answer);
2187
+ return answer;
2188
+ }
2189
+ function normalizeOrigin(url) {
2190
+ let s = url.trim();
2191
+ if (!s)
2192
+ return void 0;
2193
+ if (/^file:\/\//i.test(s))
2194
+ return void 0;
2195
+ if (/^[A-Za-z]:/.test(s))
2196
+ return void 0;
2197
+ const scheme = /^([A-Za-z][A-Za-z0-9+.-]*:)?\/\//.exec(s);
2198
+ if (scheme) {
2199
+ s = s.slice(scheme[0].length);
2200
+ } else if (s.includes("://")) {
2201
+ return void 0;
2202
+ }
2203
+ const firstSlash = s.indexOf("/");
2204
+ const authEnd = firstSlash < 0 ? s.length : firstSlash;
2205
+ const at = s.lastIndexOf("@", authEnd - 1);
2206
+ if (at >= 0)
2207
+ s = s.slice(at + 1);
2208
+ if (!scheme) {
2209
+ if (s.startsWith("/") || s.startsWith("."))
2210
+ return void 0;
2211
+ const colon = s.indexOf(":");
2212
+ if (colon < 0)
2213
+ return void 0;
2214
+ s = s.slice(0, colon) + "/" + s.slice(colon + 1).replace(/^\/+/, "");
2215
+ }
2216
+ const slash = s.indexOf("/");
2217
+ let host = slash < 0 ? s : s.slice(0, slash);
2218
+ const rest = slash < 0 ? "" : s.slice(slash);
2219
+ const port = /^(.*):(\d+)$/.exec(host);
2220
+ if (port)
2221
+ host = port[1];
2222
+ if (!host)
2223
+ return void 0;
2224
+ s = host + rest;
2225
+ s = s.replace(/\/+$/, "");
2226
+ s = s.replace(/\.git$/i, "");
2227
+ s = s.replace(/\/+$/, "");
2228
+ if (!s.includes("/"))
2229
+ return void 0;
2230
+ return s.toLowerCase();
2231
+ }
2232
+ function transient() {
2233
+ return { key: void 0, kind: "transient", transientFailure: true };
2234
+ }
2235
+ function deriveProjectKey(cwd) {
2236
+ const cacheKey = normalizePath(cwd);
2237
+ const hit = cache.get(cacheKey);
2238
+ if (hit)
2239
+ return hit;
2240
+ const result = derive(cwd);
2241
+ if (!result.transientFailure)
2242
+ cache.set(cacheKey, result);
2243
+ return result;
2244
+ }
2245
+ function derive(cwd) {
2246
+ if (!(0, import_node_fs4.existsSync)(cwd))
2247
+ return { kind: "path", key: pathKey(cwd) };
2248
+ let rev = runGit(cwd, ["rev-parse", "--is-shallow-repository", "--show-toplevel"], GIT_TIMEOUT_MS);
2249
+ if (rev.error?.code === "ENOENT") {
2250
+ return { kind: "path", key: pathKey(cwd), gitMissing: true };
2251
+ }
2252
+ if (isTransient(rev)) {
2253
+ rev = runGit(cwd, ["rev-parse", "--is-shallow-repository", "--show-toplevel"]);
2254
+ if (rev.error?.code === "ENOENT") {
2255
+ return { kind: "path", key: pathKey(cwd), gitMissing: true };
2256
+ }
2257
+ if (isTransient(rev))
2258
+ return transient();
2259
+ }
2260
+ if (rev.status === 128)
2261
+ return { kind: "path", key: pathKey(cwd) };
2262
+ if (rev.status !== 0)
2263
+ return { kind: "path", key: pathKey(cwd) };
2264
+ const lines = (rev.stdout ?? "").split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
2265
+ const shallow = lines[0] === "true";
2266
+ const toplevel = lines[1];
2267
+ if (!shallow) {
2268
+ const args = ["--no-replace-objects", "rev-list", "--max-parents=0", "--format=%ct %H", "HEAD"];
2269
+ let roots = runGit(cwd, args, GIT_TIMEOUT_MS);
2270
+ if (isTransient(roots)) {
2271
+ roots = runGit(cwd, args);
2272
+ if (isTransient(roots))
2273
+ return transient();
2274
+ }
2275
+ if (roots.status === 0) {
2276
+ const parsed = [];
2277
+ for (const line of (roots.stdout ?? "").split("\n")) {
2278
+ const t = line.trim();
2279
+ if (!t || t.startsWith("commit "))
2280
+ continue;
2281
+ const m = /^(\d+)\s+([0-9a-f]{40})$/.exec(t);
2282
+ if (m)
2283
+ parsed.push({ ct: Number(m[1]), hash: m[2] });
2284
+ }
2285
+ parsed.sort((a, b) => a.ct - b.ct || (a.hash < b.hash ? -1 : a.hash > b.hash ? 1 : 0));
2286
+ const first = parsed[0];
2287
+ if (first) {
2288
+ return { kind: "git", key: "git:" + first.hash, ...toplevel ? { toplevel } : {} };
2289
+ }
2290
+ }
2291
+ }
2292
+ let origin = runGit(cwd, ["config", "--get", "remote.origin.url"], GIT_TIMEOUT_MS);
2293
+ if (isTransient(origin)) {
2294
+ origin = runGit(cwd, ["config", "--get", "remote.origin.url"]);
2295
+ if (isTransient(origin))
2296
+ return transient();
2297
+ }
2298
+ if (origin.status === 0) {
2299
+ const value = normalizeOrigin(origin.stdout ?? "");
2300
+ if (value)
2301
+ return { kind: "origin", key: "origin:" + value, ...toplevel ? { toplevel } : {} };
2302
+ }
2303
+ return { kind: "path", key: pathKey(toplevel ?? cwd), ...toplevel ? { toplevel } : {} };
2304
+ }
2305
+
2306
+ // src/recall/mirror-meta.ts
2307
+ var import_node_fs5 = require("node:fs");
2308
+ var import_node_path5 = require("node:path");
2309
+ init_paths();
2310
+ function isUnderRemoteRoot(p) {
2311
+ return (0, import_node_path5.resolve)(p).startsWith((0, import_node_path5.resolve)(remoteRoot()) + import_node_path5.sep);
2312
+ }
2313
+ function readMirrorMeta(transcriptPath) {
2314
+ let parsed;
2315
+ try {
2316
+ parsed = JSON.parse((0, import_node_fs5.readFileSync)(`${transcriptPath}.meta.json`, "utf-8"));
2317
+ } catch {
2318
+ return null;
2319
+ }
2320
+ if (!parsed || typeof parsed !== "object")
2321
+ return null;
2322
+ const m = parsed;
2323
+ if (m["v"] !== 1)
2324
+ return null;
2325
+ if (m["key"] !== void 0 && typeof m["key"] !== "string")
2326
+ return null;
2327
+ if (m["cwd"] !== void 0 && typeof m["cwd"] !== "string")
2328
+ return null;
2329
+ return parsed;
2330
+ }
2331
+
1701
2332
  // src/recall/message-ingest.ts
1702
2333
  init_log();
1703
2334
 
@@ -1727,48 +2358,11 @@ function parseJsonlFile(filepath) {
1727
2358
  return entries;
1728
2359
  } catch (error) {
1729
2360
  log({ level: "error", source: "jsonl-reader", summary: `Failed to read JSONL file ${filepath}: ${error instanceof Error ? error.message : String(error)}`, data: { filepath, error: String(error) } });
1730
- return [];
2361
+ throw error;
1731
2362
  }
1732
2363
  }
1733
2364
 
1734
2365
  // src/adapters/claude/claude-entry-adapter.ts
1735
- function isSystemContextContent(message) {
1736
- if (!message || message.role !== "user")
1737
- return false;
1738
- const content = message.content;
1739
- let text;
1740
- if (typeof content === "string") {
1741
- text = content;
1742
- } else if (Array.isArray(content)) {
1743
- const firstText = content.find(
1744
- (b) => typeof b === "object" && b !== null && b.type === "text"
1745
- );
1746
- if (firstText && "text" in firstText) {
1747
- text = firstText.text;
1748
- }
1749
- }
1750
- if (!text)
1751
- return false;
1752
- if (text.startsWith("<system-reminder>"))
1753
- return true;
1754
- if (text.startsWith("<environment_context>"))
1755
- return true;
1756
- if (text.startsWith("<INSTRUCTIONS>"))
1757
- return true;
1758
- if (text.startsWith("# AGENTS.md instructions for"))
1759
- return true;
1760
- if (text.startsWith("<context>"))
1761
- return true;
1762
- if (text.startsWith("<task-notification>"))
1763
- return true;
1764
- if (text.startsWith("<command-name>"))
1765
- return true;
1766
- if (text.startsWith("<local-command-stdout>"))
1767
- return true;
1768
- if (text.startsWith("<local-command-caveat>"))
1769
- return true;
1770
- return false;
1771
- }
1772
2366
  function sanitizeContentBlocks(content) {
1773
2367
  if (typeof content === "string")
1774
2368
  return content;
@@ -1988,15 +2582,17 @@ function emitMessage(payload, base, model, counter) {
1988
2582
  if (role === "developer")
1989
2583
  return [];
1990
2584
  if (role === "user") {
2585
+ const message = {
2586
+ role: "user",
2587
+ content: adaptContentItems(contentItems)
2588
+ };
1991
2589
  return [
1992
2590
  {
1993
2591
  type: "user",
1994
2592
  uuid: generateId(base.sessionId, counter),
1995
2593
  ...base,
1996
- message: {
1997
- role: "user",
1998
- content: adaptContentItems(contentItems)
1999
- }
2594
+ message,
2595
+ ...isSystemContextContent(message) && { isMeta: true }
2000
2596
  }
2001
2597
  ];
2002
2598
  }
@@ -2473,6 +3069,11 @@ function parseCodexPatch(input) {
2473
3069
  currentKind = "update";
2474
3070
  continue;
2475
3071
  }
3072
+ const moveMatch = line.match(/^\*\*\* Move to:\s*(.+)$/);
3073
+ if (moveMatch && currentKind === "update") {
3074
+ currentPath = moveMatch[1].trim();
3075
+ continue;
3076
+ }
2476
3077
  const addMatch = line.match(/^\*\*\* Add File:\s*(.+)$/);
2477
3078
  if (addMatch) {
2478
3079
  flushCurrent();
@@ -2508,7 +3109,7 @@ function parseCodexPatch(input) {
2508
3109
  return changes;
2509
3110
  }
2510
3111
  function generateId(sessionId, counter) {
2511
- return `codex-jsonl-${sessionId.slice(0, 8)}-${counter}`;
3112
+ return `codex-jsonl-${sessionId}-${counter}`;
2512
3113
  }
2513
3114
 
2514
3115
  // src/recall/message-ingest.ts
@@ -2517,12 +3118,19 @@ function extractEntryText(entry) {
2517
3118
  if (!msg)
2518
3119
  return "";
2519
3120
  if (typeof msg.content === "string")
2520
- return msg.content.trim();
3121
+ return msg.content.replaceAll("\0", "").trim();
2521
3122
  if (Array.isArray(msg.content)) {
2522
- return msg.content.filter((b) => b.type === "text").map((b) => b.text?.trim()).filter(Boolean).join("\n\n");
3123
+ return msg.content.filter((b) => b.type === "text").map((b) => b.text?.replaceAll("\0", "").trim()).filter(Boolean).join("\n\n");
2523
3124
  }
2524
3125
  return "";
2525
3126
  }
3127
+ function loadTranscriptEntries(transcriptPath, vendor, canonicalSessionId) {
3128
+ if (vendor === "claude") {
3129
+ const raw = parseJsonlFile(transcriptPath);
3130
+ return adaptClaudeEntries(raw);
3131
+ }
3132
+ return adaptCodexJsonlRecords(parseCodexJsonlFile(transcriptPath), canonicalSessionId);
3133
+ }
2526
3134
  function entriesCwd(entries) {
2527
3135
  for (const e of entries) {
2528
3136
  if (e.cwd)
@@ -2563,13 +3171,7 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
2563
3171
  }));
2564
3172
  let rawEntries;
2565
3173
  try {
2566
- if (vendor === "claude") {
2567
- const raw = parseJsonlFile(transcriptPath);
2568
- rawEntries = adaptClaudeEntries(raw);
2569
- } else {
2570
- const envelopes = parseCodexJsonlFile(transcriptPath);
2571
- rawEntries = adaptCodexJsonlRecords(envelopes, canonicalId);
2572
- }
3174
+ rawEntries = loadTranscriptEntries(transcriptPath, vendor, canonicalId);
2573
3175
  } catch (err) {
2574
3176
  return {
2575
3177
  sessionId: canonicalId,
@@ -2590,11 +3192,22 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
2590
3192
  }
2591
3193
  const rawProjectId = options?.projectId ?? entriesCwd(rawEntries);
2592
3194
  const projectId = rawProjectId ? normalizePath(rawProjectId) : null;
3195
+ const localKey = () => {
3196
+ const k = deriveProjectKey(rawProjectId).key ?? null;
3197
+ return k !== null && wslUncToPosix(rawProjectId) ? upgradeLocalPathKey(k) : k;
3198
+ };
3199
+ const sidecarKey = () => {
3200
+ const k = readMirrorMeta(transcriptPath)?.key;
3201
+ return k === void 0 ? null : upgradeLocalPathKey(k);
3202
+ };
3203
+ const projectKey = rawProjectId ? options?.projectKey !== void 0 ? options.projectKey : isUnderRemoteRoot(transcriptPath) ? sidecarKey() : localKey() : null;
2593
3204
  const filtered = stripToolContent(rawEntries);
2594
3205
  const topLevel = filtered.filter((e) => !e.parentToolUseID);
2595
3206
  const records = [];
2596
3207
  for (let i = 0; i < topLevel.length; i++) {
2597
3208
  const entry = topLevel[i];
3209
+ if (shouldDropAsMeta(entry))
3210
+ continue;
2598
3211
  if (!entry.uuid)
2599
3212
  continue;
2600
3213
  const text = extractEntryText(entry);
@@ -2609,7 +3222,8 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
2609
3222
  project_id: projectId,
2610
3223
  created_at: createdAt,
2611
3224
  message_role: entry.message?.role ?? entry.type ?? null,
2612
- retrieval_class: retrievalClass
3225
+ retrieval_class: retrievalClass,
3226
+ project_key: projectKey
2613
3227
  });
2614
3228
  }
2615
3229
  if (records.length === 0) {
@@ -2621,11 +3235,19 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
2621
3235
  }
2622
3236
  return { sessionId: canonicalId, chunksCreated: 0, skipped: true, retrievalClass };
2623
3237
  }
3238
+ let inserted = 0;
3239
+ const previouslyIndexed = hasSessionMessages(canonicalId);
2624
3240
  try {
2625
- insertMessages(records, {
3241
+ inserted = insertMessages(records, {
3242
+ sourceMessageIds: topLevel.flatMap((e) => e.uuid ? [e.uuid] : []),
2626
3243
  ...options?.force ? { replaceSessionId: canonicalId } : {},
2627
3244
  provenance,
2628
- aliases
3245
+ aliases,
3246
+ // Same identity this chunk's own rows get; back-fills the session's
3247
+ // NULL-identity rows atomically with the insert (M2). Runs on every
3248
+ // ingest path — push, sweep, backfill — so a sweep of a mirror whose
3249
+ // sidecar now carries the key repairs rows without a new push.
3250
+ adopt: { sessionId: canonicalId, projectKey, projectId }
2629
3251
  });
2630
3252
  } catch (err) {
2631
3253
  return {
@@ -2636,26 +3258,58 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
2636
3258
  retrievalClass
2637
3259
  };
2638
3260
  }
3261
+ if (!previouslyIndexed && inserted < records.length) {
3262
+ log({ source: "recall:ingest", level: "warn", summary: `Session ${canonicalId}: ${records.length} records offered, ${inserted} inserted (duplicate message IDs)` });
3263
+ }
2639
3264
  return {
2640
3265
  sessionId: canonicalId,
2641
- chunksCreated: records.length,
3266
+ chunksCreated: inserted,
3267
+ recordsOffered: records.length,
2642
3268
  skipped: false,
2643
3269
  retrievalClass
2644
3270
  };
2645
3271
  }
2646
3272
 
2647
3273
  // src/hooks/stop-hook.ts
3274
+ init_config();
2648
3275
  init_paths();
2649
3276
  var import_fs = require("fs");
2650
3277
  var import_child_process = require("child_process");
2651
3278
  var import_path = require("path");
2652
- function vendorForTranscript(transcriptPath, codexHome = process.env["CODEX_HOME"]) {
3279
+ var import_promises = require("node:timers/promises");
3280
+ async function retryBusyIngest(operation) {
3281
+ for (let attempt = 0; ; attempt++) {
3282
+ try {
3283
+ const result = await operation();
3284
+ if (attempt < 2 && result.error && /database is locked|SQLITE_BUSY/i.test(result.error)) {
3285
+ await (0, import_promises.setTimeout)(25 * (attempt + 1));
3286
+ continue;
3287
+ }
3288
+ return result;
3289
+ } catch (error) {
3290
+ if (attempt >= 2 || !/database is locked|SQLITE_BUSY/i.test(String(error)))
3291
+ throw error;
3292
+ await (0, import_promises.setTimeout)(25 * (attempt + 1));
3293
+ }
3294
+ }
3295
+ }
3296
+ function vendorForTranscript(transcriptPath, codexHome = codexRoot()) {
2653
3297
  const p = transcriptPath.replace(/\\/g, "/");
2654
3298
  const root = codexHome?.replace(/\\/g, "/").replace(/\/$/, "");
2655
3299
  const underCodexHome = root ? p === root || p.startsWith(`${root}/`) : false;
2656
3300
  return p.includes("/.codex/") || underCodexHome ? "codex" : "claude";
2657
3301
  }
2658
3302
  function resolveIngestTarget(payload) {
3303
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
3304
+ return null;
3305
+ if (payload.agent_transcript_path != null && typeof payload.agent_transcript_path !== "string")
3306
+ return null;
3307
+ if (payload.transcript_path != null && typeof payload.transcript_path !== "string")
3308
+ return null;
3309
+ if (payload.session_id != null && typeof payload.session_id !== "string")
3310
+ return null;
3311
+ if (payload.agent_id != null && typeof payload.agent_id !== "string")
3312
+ return null;
2659
3313
  if (payload.agent_transcript_path) {
2660
3314
  const transcriptPath = payload.agent_transcript_path;
2661
3315
  const base = transcriptPath.replace(/\\/g, "/").split("/").pop().replace(/\.jsonl$/i, "");
@@ -2696,6 +3350,8 @@ async function runStopHook() {
2696
3350
  } catch {
2697
3351
  process.exit(0);
2698
3352
  }
3353
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
3354
+ process.exit(0);
2699
3355
  if (payload.stop_hook_active)
2700
3356
  process.exit(0);
2701
3357
  const target = resolveIngestTarget(payload);
@@ -2703,15 +3359,45 @@ async function runStopHook() {
2703
3359
  process.exit(0);
2704
3360
  let ingestedClass;
2705
3361
  let canonicalId = target.sessionId;
3362
+ let satellite = false;
2706
3363
  try {
2707
- getDb(dbPath()).exec("PRAGMA busy_timeout = 5000;");
3364
+ const sat = readSatelliteConfig();
3365
+ if (sat) {
3366
+ satellite = true;
3367
+ (0, import_child_process.spawn)(
3368
+ process.execPath,
3369
+ [
3370
+ (0, import_path.join)(binDir(), "push-pending.js"),
3371
+ "--named",
3372
+ target.transcriptPath,
3373
+ "--hook",
3374
+ JSON.stringify(target.hook),
3375
+ "--cwd",
3376
+ payload.cwd ?? ""
3377
+ ],
3378
+ { detached: true, stdio: "ignore", windowsHide: true }
3379
+ ).unref();
3380
+ process.exit(0);
3381
+ }
2708
3382
  const vendor = vendorForTranscript(target.transcriptPath);
2709
- const result = await ingestSessionMessages(
2710
- target.sessionId,
2711
- target.transcriptPath,
2712
- vendor,
2713
- { projectId: payload.cwd ?? void 0, hook: target.hook }
2714
- );
3383
+ const projectKey = payload.cwd ? deriveProjectKey(payload.cwd).key ?? null : void 0;
3384
+ const result = await retryBusyIngest(async () => {
3385
+ getDb(dbPath()).exec("PRAGMA busy_timeout = 5000;");
3386
+ return ingestSessionMessages(
3387
+ target.sessionId,
3388
+ target.transcriptPath,
3389
+ vendor,
3390
+ {
3391
+ projectId: payload.cwd ?? void 0,
3392
+ // Derive ONCE per turn (spec §4.2): two git spawns, ~4-7 ms. A throw
3393
+ // is caught by the enclosing try, so the hook still exits 0. A
3394
+ // transient failure becomes an explicit null — ingest must not repeat
3395
+ // the derivation that just failed.
3396
+ projectKey,
3397
+ hook: target.hook
3398
+ }
3399
+ );
3400
+ });
2715
3401
  ingestedClass = result?.retrievalClass;
2716
3402
  canonicalId = result?.sessionId ?? target.sessionId;
2717
3403
  if (result?.error) {
@@ -2726,7 +3412,7 @@ async function runStopHook() {
2726
3412
  `
2727
3413
  );
2728
3414
  }
2729
- if (!target.hook.isSubagent && ingestedClass !== "agent") {
3415
+ if (!satellite && !target.hook.isSubagent && ingestedClass !== "agent") {
2730
3416
  (0, import_child_process.spawn)(
2731
3417
  process.execPath,
2732
3418
  [(0, import_path.join)(binDir(), "embed-pending.js"), canonicalId],
@@ -2740,10 +3426,15 @@ async function runStopHook() {
2740
3426
  process.exit(0);
2741
3427
  }
2742
3428
  if (typeof require !== "undefined" && typeof module !== "undefined" && require.main === module) {
2743
- void runStopHook();
3429
+ void runStopHook().catch((error) => {
3430
+ logStopHook(`${(/* @__PURE__ */ new Date()).toISOString()} hook-failed err=${String(error)}
3431
+ `);
3432
+ process.exit(0);
3433
+ });
2744
3434
  }
2745
3435
  // Annotate the CommonJS export names for ESM import in node:
2746
3436
  0 && (module.exports = {
2747
3437
  resolveIngestTarget,
3438
+ retryBusyIngest,
2748
3439
  vendorForTranscript
2749
3440
  });