crispy-recall 0.2.1 → 0.3.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.
@@ -158,21 +158,21 @@ var require_file_uri_to_path = __commonJS({
158
158
  var rest = decodeURI(uri.substring(7));
159
159
  var firstSlash = rest.indexOf("/");
160
160
  var host = rest.substring(0, firstSlash);
161
- var path3 = rest.substring(firstSlash + 1);
161
+ var path2 = rest.substring(firstSlash + 1);
162
162
  if ("localhost" == host)
163
163
  host = "";
164
164
  if (host) {
165
165
  host = sep2 + sep2 + host;
166
166
  }
167
- path3 = path3.replace(/^(.+)\|/, "$1:");
167
+ path2 = path2.replace(/^(.+)\|/, "$1:");
168
168
  if (sep2 == "\\") {
169
- path3 = path3.replace(/\//g, "\\");
169
+ path2 = path2.replace(/\//g, "\\");
170
170
  }
171
- if (/^.+\:/.test(path3)) {
171
+ if (/^.+\:/.test(path2)) {
172
172
  } else {
173
- path3 = sep2 + path3;
173
+ path2 = sep2 + path2;
174
174
  }
175
- return host + path3;
175
+ return host + path2;
176
176
  }
177
177
  }
178
178
  });
@@ -181,18 +181,18 @@ var require_file_uri_to_path = __commonJS({
181
181
  var require_bindings = __commonJS({
182
182
  "node_modules/bindings/bindings.js"(exports2, module2) {
183
183
  var fs3 = require("fs");
184
- var path3 = require("path");
184
+ var path2 = require("path");
185
185
  var fileURLToPath3 = require_file_uri_to_path();
186
- var join8 = path3.join;
187
- var dirname3 = path3.dirname;
188
- var exists = fs3.accessSync && function(path4) {
186
+ var join7 = path2.join;
187
+ var dirname3 = path2.dirname;
188
+ var exists = fs3.accessSync && function(path3) {
189
189
  try {
190
- fs3.accessSync(path4);
190
+ fs3.accessSync(path3);
191
191
  } catch (e) {
192
192
  return false;
193
193
  }
194
194
  return true;
195
- } || fs3.existsSync || path3.existsSync;
195
+ } || fs3.existsSync || path2.existsSync;
196
196
  var defaults2 = {
197
197
  arrow: process.env.NODE_BINDINGS_ARROW || " \u2192 ",
198
198
  compiled: process.env.NODE_BINDINGS_COMPILED_DIR || "compiled",
@@ -238,13 +238,13 @@ var require_bindings = __commonJS({
238
238
  if (!opts.module_root) {
239
239
  opts.module_root = exports2.getRoot(exports2.getFileName());
240
240
  }
241
- if (path3.extname(opts.bindings) != ".node") {
241
+ if (path2.extname(opts.bindings) != ".node") {
242
242
  opts.bindings += ".node";
243
243
  }
244
244
  var requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;
245
245
  var tries = [], i = 0, l = opts.try.length, n, b, err;
246
246
  for (; i < l; i++) {
247
- n = join8.apply(
247
+ n = join7.apply(
248
248
  null,
249
249
  opts.try[i].map(function(p) {
250
250
  return opts[p] || p;
@@ -305,7 +305,7 @@ var require_bindings = __commonJS({
305
305
  if (dir === ".") {
306
306
  dir = process.cwd();
307
307
  }
308
- if (exists(join8(dir, "package.json")) || exists(join8(dir, "node_modules"))) {
308
+ if (exists(join7(dir, "package.json")) || exists(join7(dir, "node_modules"))) {
309
309
  return dir;
310
310
  }
311
311
  if (prev === dir) {
@@ -314,7 +314,7 @@ var require_bindings = __commonJS({
314
314
  );
315
315
  }
316
316
  prev = dir;
317
- dir = join8(dir, "..");
317
+ dir = join7(dir, "..");
318
318
  }
319
319
  };
320
320
  }
@@ -482,7 +482,7 @@ var require_backup = __commonJS({
482
482
  "node_modules/better-sqlite3/lib/methods/backup.js"(exports2, module2) {
483
483
  "use strict";
484
484
  var fs3 = require("fs");
485
- var path3 = require("path");
485
+ var path2 = require("path");
486
486
  var { promisify: promisify2 } = require("util");
487
487
  var { cppdb } = require_util();
488
488
  var fsAccess = promisify2(fs3.access);
@@ -506,7 +506,7 @@ var require_backup = __commonJS({
506
506
  throw new TypeError('The "attached" option cannot be an empty string');
507
507
  if (handler != null && typeof handler !== "function")
508
508
  throw new TypeError('Expected the "progress" option to be a function');
509
- await fsAccess(path3.dirname(filename)).catch(() => {
509
+ await fsAccess(path2.dirname(filename)).catch(() => {
510
510
  throw new TypeError("Cannot save backup because the directory does not exist");
511
511
  });
512
512
  const isNewFile = await fsAccess(filename).then(() => false, () => true);
@@ -837,7 +837,7 @@ var require_database = __commonJS({
837
837
  "node_modules/better-sqlite3/lib/database.js"(exports2, module2) {
838
838
  "use strict";
839
839
  var fs3 = require("fs");
840
- var path3 = require("path");
840
+ var path2 = require("path");
841
841
  var util = require_util();
842
842
  var SqliteError = require_sqlite_error();
843
843
  var DEFAULT_ADDON;
@@ -884,7 +884,7 @@ var require_database = __commonJS({
884
884
  addon = DEFAULT_ADDON || (DEFAULT_ADDON = require_bindings()("better_sqlite3.node"));
885
885
  } else if (typeof nativeBinding === "string") {
886
886
  const requireFunc = typeof __non_webpack_require__ === "function" ? __non_webpack_require__ : require;
887
- addon = requireFunc(path3.resolve(nativeBinding).replace(/(\.node)?$/, ".node"));
887
+ addon = requireFunc(path2.resolve(nativeBinding).replace(/(\.node)?$/, ".node"));
888
888
  } else {
889
889
  addon = nativeBinding;
890
890
  }
@@ -892,7 +892,7 @@ var require_database = __commonJS({
892
892
  addon.setErrorConstructor(SqliteError);
893
893
  addon.isInitialized = true;
894
894
  }
895
- if (!anonymous && !filename.startsWith("file:") && !fs3.existsSync(path3.dirname(filename))) {
895
+ if (!anonymous && !filename.startsWith("file:") && !fs3.existsSync(path2.dirname(filename))) {
896
896
  throw new TypeError("Cannot open database because the directory does not exist");
897
897
  }
898
898
  Object.defineProperties(this, {
@@ -2648,7 +2648,7 @@ var require_yauzl = __commonJS({
2648
2648
  exports2.ZipFile = ZipFile;
2649
2649
  exports2.Entry = Entry;
2650
2650
  exports2.RandomAccessReader = RandomAccessReader;
2651
- function open(path3, options, callback) {
2651
+ function open(path2, options, callback) {
2652
2652
  if (typeof options === "function") {
2653
2653
  callback = options;
2654
2654
  options = null;
@@ -2667,7 +2667,7 @@ var require_yauzl = __commonJS({
2667
2667
  options.strictFileNames = false;
2668
2668
  if (callback == null)
2669
2669
  callback = defaultCallback;
2670
- fs3.open(path3, "r", function(err, fd) {
2670
+ fs3.open(path2, "r", function(err, fd) {
2671
2671
  if (err)
2672
2672
  return callback(err);
2673
2673
  fromFd(fd, options, function(err2, zipfile) {
@@ -3334,7 +3334,7 @@ var require_extract_zip = __commonJS({
3334
3334
  var debug = require_src()("extract-zip");
3335
3335
  var { createWriteStream: createWriteStream2, promises: fs3 } = require("fs");
3336
3336
  var getStream = require_get_stream();
3337
- var path3 = require("path");
3337
+ var path2 = require("path");
3338
3338
  var { promisify: promisify2 } = require("util");
3339
3339
  var stream2 = require("stream");
3340
3340
  var yauzl = require_yauzl();
@@ -3371,12 +3371,12 @@ var require_extract_zip = __commonJS({
3371
3371
  this.zipfile.readEntry();
3372
3372
  return;
3373
3373
  }
3374
- const destDir = path3.dirname(path3.join(this.opts.dir, entry.fileName));
3374
+ const destDir = path2.dirname(path2.join(this.opts.dir, entry.fileName));
3375
3375
  try {
3376
3376
  await fs3.mkdir(destDir, { recursive: true });
3377
3377
  const canonicalDestDir = await fs3.realpath(destDir);
3378
- const relativeDestDir = path3.relative(this.opts.dir, canonicalDestDir);
3379
- if (relativeDestDir.split(path3.sep).includes("..")) {
3378
+ const relativeDestDir = path2.relative(this.opts.dir, canonicalDestDir);
3379
+ if (relativeDestDir.split(path2.sep).includes("..")) {
3380
3380
  throw new Error(`Out of bound path "${canonicalDestDir}" found while processing file ${entry.fileName}`);
3381
3381
  }
3382
3382
  await this.extractEntry(entry);
@@ -3398,7 +3398,7 @@ var require_extract_zip = __commonJS({
3398
3398
  if (this.opts.onEntry) {
3399
3399
  this.opts.onEntry(entry, this.zipfile);
3400
3400
  }
3401
- const dest = path3.join(this.opts.dir, entry.fileName);
3401
+ const dest = path2.join(this.opts.dir, entry.fileName);
3402
3402
  const mode = entry.externalFileAttributes >> 16 & 65535;
3403
3403
  const IFMT2 = 61440;
3404
3404
  const IFDIR2 = 16384;
@@ -3413,7 +3413,7 @@ var require_extract_zip = __commonJS({
3413
3413
  isDir = madeBy === 0 && entry.externalFileAttributes === 16;
3414
3414
  debug("extracting entry", { filename: entry.fileName, isDir, isSymlink: symlink });
3415
3415
  const procMode = this.getExtractedMode(mode, isDir) & 511;
3416
- const destDir = isDir ? dest : path3.dirname(dest);
3416
+ const destDir = isDir ? dest : path2.dirname(dest);
3417
3417
  const mkdirOptions = { recursive: true };
3418
3418
  if (isDir) {
3419
3419
  mkdirOptions.mode = procMode;
@@ -3456,7 +3456,7 @@ var require_extract_zip = __commonJS({
3456
3456
  };
3457
3457
  module2.exports = async function(zipPath, opts) {
3458
3458
  debug("creating target directory", opts.dir);
3459
- if (!path3.isAbsolute(opts.dir)) {
3459
+ if (!path2.isAbsolute(opts.dir)) {
3460
3460
  throw new Error("Target directory is expected to be absolute");
3461
3461
  }
3462
3462
  await fs3.mkdir(opts.dir, { recursive: true });
@@ -3478,6 +3478,8 @@ var init_settings_merge = __esm({
3478
3478
  "src/installer/settings-merge.ts"() {
3479
3479
  "use strict";
3480
3480
  init_stable_node();
3481
+ init_config();
3482
+ init_paths();
3481
3483
  }
3482
3484
  });
3483
3485
 
@@ -3527,6 +3529,7 @@ __export(embedder_exports, {
3527
3529
  disposeEmbedder: () => disposeEmbedder,
3528
3530
  embed: () => embed,
3529
3531
  embedBatch: () => embedBatch,
3532
+ embedBatchOneShot: () => embedBatchOneShot,
3530
3533
  ensureBinary: () => ensureBinary,
3531
3534
  ensureModel: () => ensureModel,
3532
3535
  extractArchive: () => extractArchive,
@@ -4227,13 +4230,13 @@ async function retryServer() {
4227
4230
  serverRetryPromise = null;
4228
4231
  }
4229
4232
  }
4230
- function httpPost(socketPath, path3, body, timeoutMs) {
4233
+ function httpPost(socketPath, path2, body, timeoutMs) {
4231
4234
  return new Promise((resolve, reject) => {
4232
4235
  const data = JSON.stringify(body);
4233
4236
  const req = (0, import_node_http.request)(
4234
4237
  {
4235
4238
  socketPath,
4236
- path: path3,
4239
+ path: path2,
4237
4240
  method: "POST",
4238
4241
  headers: {
4239
4242
  "Content-Type": "application/json",
@@ -4286,7 +4289,7 @@ async function embedViaHttp(socketPath, texts) {
4286
4289
  return new Float32Array(item.embedding);
4287
4290
  });
4288
4291
  }
4289
- async function embedViaProcess(texts, modelPath) {
4292
+ async function embedViaProcess(texts, modelPath, opts) {
4290
4293
  if (!binaryPath)
4291
4294
  throw new Error("llama-embedding binary not available");
4292
4295
  const sanitized = texts.map((t) => t.replaceAll(BATCH_SEPARATOR, " "));
@@ -4335,7 +4338,8 @@ async function embedViaProcess(texts, modelPath) {
4335
4338
  }
4336
4339
  const { stdout, stderr } = await execFileAsync(binaryPath, args, {
4337
4340
  maxBuffer: 2 * 1024 * 1024,
4338
- env
4341
+ env,
4342
+ ...opts?.timeoutMs ? { timeout: opts.timeoutMs, killSignal: "SIGKILL" } : {}
4339
4343
  });
4340
4344
  if (stderr) {
4341
4345
  log({
@@ -4377,6 +4381,19 @@ async function embedBatch(texts) {
4377
4381
  return [];
4378
4382
  return withEmbedMutex(() => embedBatchInner(texts));
4379
4383
  }
4384
+ async function embedBatchOneShot(texts, opts) {
4385
+ if (texts.length === 0)
4386
+ return [];
4387
+ return withEmbedMutex(async () => {
4388
+ if (!binaryPath) {
4389
+ await ensureBinary();
4390
+ }
4391
+ if (!binaryPath)
4392
+ throw new Error("llama-embedding binary not available");
4393
+ const modelPath = await ensureModel();
4394
+ return embedViaProcess(texts, modelPath, opts);
4395
+ });
4396
+ }
4380
4397
  async function embedBatchInner(texts) {
4381
4398
  if (!binaryPath) {
4382
4399
  await ensureBinary();
@@ -4773,6 +4790,15 @@ Run \`recall doctor\` for details.`
4773
4790
  this.name = "BindingLoadError";
4774
4791
  }
4775
4792
  };
4793
+ var MigrationPendingError = class extends Error {
4794
+ constructor(dbPath2) {
4795
+ super(
4796
+ "recall: this database needs a one-time schema migration \u2014 run `recall install` to finish it. (Normal commands refuse to rewrite the index unattended.)"
4797
+ );
4798
+ this.dbPath = dbPath2;
4799
+ this.name = "MigrationPendingError";
4800
+ }
4801
+ };
4776
4802
  function isBindingLoadError(e) {
4777
4803
  if (!e || typeof e !== "object")
4778
4804
  return false;
@@ -4787,7 +4813,7 @@ function isBindingLoadError(e) {
4787
4813
  }
4788
4814
  var db = null;
4789
4815
  var currentDbPath = null;
4790
- function getDb(dbPath2) {
4816
+ function getDb(dbPath2, opts) {
4791
4817
  if (db && currentDbPath === dbPath2)
4792
4818
  return db;
4793
4819
  if (db) {
@@ -4802,12 +4828,45 @@ function getDb(dbPath2) {
4802
4828
  raw.close();
4803
4829
  throw e;
4804
4830
  }
4805
- db = createAdapter(raw);
4831
+ const adapter = createAdapter(raw);
4832
+ if (isRetrievalMigrationPending(adapter)) {
4833
+ if (!opts?.allowPendingMigration) {
4834
+ raw.close();
4835
+ throw new MigrationPendingError(dbPath2);
4836
+ }
4837
+ db = adapter;
4838
+ currentDbPath = dbPath2;
4839
+ log({ source: "db", level: "info", summary: `DB: opened pending-migration DB at ${dbPath2} (installer mode)` });
4840
+ return db;
4841
+ }
4842
+ db = adapter;
4806
4843
  currentDbPath = dbPath2;
4807
4844
  ensureSchema(db);
4808
4845
  log({ source: "db", level: "info", summary: `DB: initialized at ${dbPath2}` });
4809
4846
  return db;
4810
4847
  }
4848
+ var RETRIEVAL_MIGRATION_KEY = "retrieval_class_migration";
4849
+ function isRetrievalMigrationPending(d) {
4850
+ try {
4851
+ const hasMessages = d.get(
4852
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'messages'`
4853
+ );
4854
+ if (!hasMessages)
4855
+ return false;
4856
+ const hasMeta = d.get(
4857
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'schema_meta'`
4858
+ );
4859
+ if (!hasMeta)
4860
+ return true;
4861
+ const row = d.get(
4862
+ `SELECT value FROM schema_meta WHERE key = ?`,
4863
+ [RETRIEVAL_MIGRATION_KEY]
4864
+ );
4865
+ return row?.value !== "complete";
4866
+ } catch {
4867
+ return true;
4868
+ }
4869
+ }
4811
4870
  function closeDb() {
4812
4871
  if (db) {
4813
4872
  db.close();
@@ -4918,16 +4977,18 @@ function cleanupWasmArtifacts(dbPath2) {
4918
4977
  } catch {
4919
4978
  }
4920
4979
  }
4921
- function ensureSchema(db3) {
4922
- db3.exec(`
4980
+ var RETRIEVAL_SCHEMA_DDL = {
4981
+ /** messages + provenance tables + indexes (no FTS objects). */
4982
+ tables: `
4923
4983
  CREATE TABLE IF NOT EXISTS messages (
4924
- message_id TEXT PRIMARY KEY,
4925
- session_id TEXT NOT NULL,
4926
- message_seq INTEGER NOT NULL,
4927
- message_text TEXT NOT NULL,
4928
- project_id TEXT,
4929
- created_at INTEGER NOT NULL,
4930
- message_role TEXT,
4984
+ message_id TEXT PRIMARY KEY,
4985
+ session_id TEXT NOT NULL,
4986
+ message_seq INTEGER NOT NULL,
4987
+ message_text TEXT NOT NULL,
4988
+ project_id TEXT,
4989
+ created_at INTEGER NOT NULL,
4990
+ message_role TEXT,
4991
+ retrieval_class TEXT NOT NULL DEFAULT 'hot',
4931
4992
  UNIQUE(session_id, message_id)
4932
4993
  );
4933
4994
 
@@ -4939,68 +5000,134 @@ function ensureSchema(db3) {
4939
5000
  -- embed drain and every Stop-hook catch-up. This index serves the ORDER BY so
4940
5001
  -- the planner walks it and early-terminates at LIMIT instead.
4941
5002
  CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
4942
- `);
4943
- db3.exec(`
5003
+
5004
+ -- session_provenance \u2014 durable per-session classification evidence:
5005
+ -- canonical id, vendor, root-vs-agent kind, parent thread, hook/agent
5006
+ -- metadata, and the transcript path \u2192 canonical id mapping that lets
5007
+ -- later T1/mtime scans resolve a child to ONE identity.
5008
+ CREATE TABLE IF NOT EXISTS session_provenance (
5009
+ session_id TEXT PRIMARY KEY,
5010
+ vendor TEXT NOT NULL,
5011
+ kind TEXT NOT NULL,
5012
+ parent_session_id TEXT,
5013
+ agent_depth INTEGER,
5014
+ agent_meta TEXT,
5015
+ transcript_path TEXT,
5016
+ updated_at INTEGER NOT NULL
5017
+ );
5018
+ CREATE INDEX IF NOT EXISTS idx_provenance_path ON session_provenance(transcript_path);
5019
+
5020
+ -- session_aliases \u2014 alternate identifiers (e.g. a hook agent_id that
5021
+ -- differs from the child rollout's session-meta UUID) \u2192 canonical id.
5022
+ CREATE TABLE IF NOT EXISTS session_aliases (
5023
+ alias_id TEXT PRIMARY KEY,
5024
+ session_id TEXT NOT NULL,
5025
+ source TEXT,
5026
+ created_at INTEGER NOT NULL
5027
+ );
5028
+
5029
+ CREATE TABLE IF NOT EXISTS schema_meta (
5030
+ key TEXT PRIMARY KEY,
5031
+ value TEXT NOT NULL
5032
+ );
5033
+ `,
5034
+ /** Filtered external-content view + FTS + four-state triggers + vocab. */
5035
+ fts: `
5036
+ -- Filtered external-content source: FTS5 rebuild/integrity-check read
5037
+ -- THIS view, so 'rebuild' repopulates only hot rows and the (rank-1)
5038
+ -- integrity check compares against the filtered corpus.
5039
+ CREATE VIEW IF NOT EXISTS searchable_messages AS
5040
+ SELECT rowid, message_text FROM messages WHERE retrieval_class = 'hot';
5041
+
4944
5042
  CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
4945
5043
  message_text,
4946
- content=messages,
5044
+ content=searchable_messages,
4947
5045
  content_rowid=rowid,
4948
5046
  tokenize='porter unicode61'
4949
5047
  );
4950
5048
 
4951
- CREATE TRIGGER IF NOT EXISTS messages_fts_ai AFTER INSERT ON messages BEGIN
5049
+ -- Four-state trigger behavior:
5050
+ -- insert hot \u2192 add | insert agent \u2192 no-op
5051
+ -- delete hot \u2192 delete | delete agent \u2192 no-op
5052
+ -- update hot\u2192hot \u2192 delete old + add new
5053
+ -- update hot\u2192agent \u2192 delete old only
5054
+ -- update agent\u2192hot \u2192 add new only
5055
+ -- update agent\u2192agent \u2192 no-op
5056
+ CREATE TRIGGER IF NOT EXISTS messages_fts_ai AFTER INSERT ON messages
5057
+ WHEN new.retrieval_class = 'hot'
5058
+ BEGIN
4952
5059
  INSERT INTO messages_fts(rowid, message_text) VALUES (new.rowid, new.message_text);
4953
5060
  END;
4954
5061
 
4955
- CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN
5062
+ CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages
5063
+ WHEN old.retrieval_class = 'hot'
5064
+ BEGIN
4956
5065
  INSERT INTO messages_fts(messages_fts, rowid, message_text)
4957
5066
  VALUES ('delete', old.rowid, old.message_text);
4958
5067
  END;
4959
5068
 
4960
5069
  CREATE TRIGGER IF NOT EXISTS messages_fts_au AFTER UPDATE ON messages BEGIN
4961
5070
  INSERT INTO messages_fts(messages_fts, rowid, message_text)
4962
- VALUES ('delete', old.rowid, old.message_text);
4963
- INSERT INTO messages_fts(rowid, message_text) VALUES (new.rowid, new.message_text);
5071
+ SELECT 'delete', old.rowid, old.message_text WHERE old.retrieval_class = 'hot';
5072
+ INSERT INTO messages_fts(rowid, message_text)
5073
+ SELECT new.rowid, new.message_text WHERE new.retrieval_class = 'hot';
4964
5074
  END;
4965
- `);
4966
- db3.exec(`
5075
+
4967
5076
  CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts_vocab
4968
5077
  USING fts5vocab(messages_fts, 'row');
4969
- `);
4970
- db3.exec(`
4971
- CREATE VIRTUAL TABLE IF NOT EXISTS _stem USING fts5(
4972
- t, tokenize='porter unicode61'
4973
- );
4974
- CREATE VIRTUAL TABLE IF NOT EXISTS _stem_vocab
4975
- USING fts5vocab(_stem, 'row');
4976
- `);
4977
- db3.exec(`
4978
- CREATE TABLE IF NOT EXISTS message_vectors (
4979
- message_id TEXT PRIMARY KEY REFERENCES messages(message_id) ON DELETE CASCADE,
4980
- embedding_q8 BLOB NOT NULL,
4981
- norm REAL NOT NULL,
4982
- quant_scale REAL NOT NULL,
4983
- embed_version INTEGER NOT NULL DEFAULT 1
4984
- );
4985
- `);
4986
- const hasEmbedVersion = () => db3.all(`PRAGMA table_info(message_vectors)`).some((c) => c.name === "embed_version");
4987
- if (!hasEmbedVersion()) {
5078
+ `
5079
+ };
5080
+ function ensureSchema(db3) {
5081
+ db3.exec("BEGIN IMMEDIATE");
5082
+ try {
5083
+ db3.exec(RETRIEVAL_SCHEMA_DDL.tables);
5084
+ db3.exec(RETRIEVAL_SCHEMA_DDL.fts);
5085
+ db3.exec(`
5086
+ CREATE VIRTUAL TABLE IF NOT EXISTS _stem USING fts5(
5087
+ t, tokenize='porter unicode61'
5088
+ );
5089
+ CREATE VIRTUAL TABLE IF NOT EXISTS _stem_vocab
5090
+ USING fts5vocab(_stem, 'row');
5091
+ `);
5092
+ db3.exec(`
5093
+ CREATE TABLE IF NOT EXISTS message_vectors (
5094
+ message_id TEXT PRIMARY KEY REFERENCES messages(message_id) ON DELETE CASCADE,
5095
+ embedding_q8 BLOB NOT NULL,
5096
+ norm REAL NOT NULL,
5097
+ quant_scale REAL NOT NULL,
5098
+ embed_version INTEGER NOT NULL DEFAULT 1
5099
+ );
5100
+ `);
5101
+ const hasEmbedVersion = () => db3.all(`PRAGMA table_info(message_vectors)`).some((c) => c.name === "embed_version");
5102
+ if (!hasEmbedVersion()) {
5103
+ try {
5104
+ db3.exec(`ALTER TABLE message_vectors ADD COLUMN embed_version INTEGER NOT NULL DEFAULT 1`);
5105
+ } catch (e) {
5106
+ if (!hasEmbedVersion())
5107
+ throw e;
5108
+ }
5109
+ }
5110
+ db3.exec(`
5111
+ CREATE TABLE IF NOT EXISTS ingest_watermark (
5112
+ transcript_path TEXT PRIMARY KEY,
5113
+ last_mtime INTEGER NOT NULL,
5114
+ last_size INTEGER NOT NULL,
5115
+ vendor TEXT NOT NULL
5116
+ );
5117
+ CREATE INDEX IF NOT EXISTS idx_watermark_vendor ON ingest_watermark(vendor);
5118
+ `);
5119
+ db3.exec(`
5120
+ INSERT OR IGNORE INTO schema_meta(key, value)
5121
+ VALUES ('${RETRIEVAL_MIGRATION_KEY}', 'complete');
5122
+ `);
5123
+ db3.exec("COMMIT");
5124
+ } catch (e) {
4988
5125
  try {
4989
- db3.exec(`ALTER TABLE message_vectors ADD COLUMN embed_version INTEGER NOT NULL DEFAULT 1`);
4990
- } catch (e) {
4991
- if (!hasEmbedVersion())
4992
- throw e;
5126
+ db3.exec("ROLLBACK");
5127
+ } catch {
4993
5128
  }
5129
+ throw e;
4994
5130
  }
4995
- db3.exec(`
4996
- CREATE TABLE IF NOT EXISTS ingest_watermark (
4997
- transcript_path TEXT PRIMARY KEY,
4998
- last_mtime INTEGER NOT NULL,
4999
- last_size INTEGER NOT NULL,
5000
- vendor TEXT NOT NULL
5001
- );
5002
- CREATE INDEX IF NOT EXISTS idx_watermark_vendor ON ingest_watermark(vendor);
5003
- `);
5004
5131
  }
5005
5132
 
5006
5133
  // src/recall/message-store.ts
@@ -5037,7 +5164,7 @@ function ensureDir2() {
5037
5164
  }
5038
5165
  }
5039
5166
  function insertMessages(messages, opts) {
5040
- if (messages.length === 0 && !opts?.replaceSessionId)
5167
+ if (messages.length === 0 && !opts?.replaceSessionId && !opts?.provenance)
5041
5168
  return;
5042
5169
  ensureDir2();
5043
5170
  const d = db2();
@@ -5053,8 +5180,8 @@ function insertMessages(messages, opts) {
5053
5180
  }
5054
5181
  const stmt = d.prepare(
5055
5182
  `INSERT OR IGNORE INTO messages
5056
- (message_id, session_id, message_seq, message_text, project_id, created_at, message_role)
5057
- VALUES (?, ?, ?, ?, ?, ?, ?)`
5183
+ (message_id, session_id, message_seq, message_text, project_id, created_at, message_role, retrieval_class)
5184
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
5058
5185
  );
5059
5186
  for (const m of messages) {
5060
5187
  stmt.run([
@@ -5064,9 +5191,55 @@ function insertMessages(messages, opts) {
5064
5191
  m.message_text,
5065
5192
  m.project_id,
5066
5193
  m.created_at,
5067
- m.message_role
5194
+ m.message_role,
5195
+ m.retrieval_class ?? "hot"
5068
5196
  ]);
5069
5197
  }
5198
+ if (opts?.provenance) {
5199
+ const p = opts.provenance;
5200
+ d.run(
5201
+ `INSERT INTO session_provenance
5202
+ (session_id, vendor, kind, parent_session_id, agent_depth, agent_meta, transcript_path, updated_at)
5203
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
5204
+ ON CONFLICT(session_id) DO UPDATE SET
5205
+ vendor = excluded.vendor,
5206
+ kind = excluded.kind,
5207
+ parent_session_id = COALESCE(excluded.parent_session_id, session_provenance.parent_session_id),
5208
+ agent_depth = COALESCE(excluded.agent_depth, session_provenance.agent_depth),
5209
+ agent_meta = COALESCE(excluded.agent_meta, session_provenance.agent_meta),
5210
+ transcript_path = COALESCE(excluded.transcript_path, session_provenance.transcript_path),
5211
+ updated_at = excluded.updated_at`,
5212
+ [
5213
+ p.sessionId,
5214
+ p.vendor,
5215
+ p.kind,
5216
+ p.parentSessionId,
5217
+ p.agentDepth,
5218
+ p.agentMeta ? JSON.stringify(p.agentMeta) : null,
5219
+ p.transcriptPath,
5220
+ Date.now()
5221
+ ]
5222
+ );
5223
+ if (p.kind === "agent") {
5224
+ d.run(
5225
+ `UPDATE messages SET retrieval_class = 'agent'
5226
+ WHERE session_id = ? AND retrieval_class != 'agent'`,
5227
+ [p.sessionId]
5228
+ );
5229
+ d.run(
5230
+ `DELETE FROM message_vectors WHERE message_id IN
5231
+ (SELECT message_id FROM messages WHERE session_id = ?)`,
5232
+ [p.sessionId]
5233
+ );
5234
+ }
5235
+ }
5236
+ for (const a of opts?.aliases ?? []) {
5237
+ d.run(
5238
+ `INSERT OR REPLACE INTO session_aliases (alias_id, session_id, source, created_at)
5239
+ VALUES (?, ?, ?, ?)`,
5240
+ [a.aliasId, a.sessionId, a.source, Date.now()]
5241
+ );
5242
+ }
5070
5243
  d.exec("COMMIT");
5071
5244
  } catch (e) {
5072
5245
  d.exec("ROLLBACK");
@@ -5083,15 +5256,17 @@ function insertMessageVectors(records) {
5083
5256
  const stmt = d.prepare(
5084
5257
  `INSERT OR REPLACE INTO message_vectors
5085
5258
  (message_id, embedding_q8, norm, quant_scale, embed_version)
5086
- VALUES (?, ?, ?, ?, ?)`
5259
+ SELECT m.message_id, ?, ?, ?, ?
5260
+ FROM messages m
5261
+ WHERE m.message_id = ? AND m.retrieval_class = 'hot'`
5087
5262
  );
5088
5263
  for (const r of records) {
5089
5264
  stmt.run([
5090
- r.messageId,
5091
5265
  Buffer.from(r.embeddingQ8.buffer, r.embeddingQ8.byteOffset, r.embeddingQ8.byteLength),
5092
5266
  r.norm,
5093
5267
  r.quantScale,
5094
- EMBED_VERSION
5268
+ EMBED_VERSION,
5269
+ r.messageId
5095
5270
  ]);
5096
5271
  }
5097
5272
  d.exec("COMMIT");
@@ -5106,12 +5281,13 @@ function getUnembeddedMessages(limit) {
5106
5281
  const rows = db2().all(
5107
5282
  `SELECT m.message_id, m.session_id, m.message_text,
5108
5283
  (SELECT p.message_text FROM messages p
5109
- WHERE p.session_id = m.session_id AND p.message_seq < m.message_seq
5284
+ WHERE p.session_id = m.session_id AND p.message_seq < m.message_seq AND p.retrieval_class = 'hot'
5110
5285
  ORDER BY p.message_seq DESC LIMIT 1) AS prev_text
5111
5286
  FROM messages m
5112
5287
  WHERE m.message_text != ''
5288
+ AND m.retrieval_class = 'hot'
5113
5289
  AND (LENGTH(m.message_text) >= ${MIN_EMBED_CHARS}
5114
- OR EXISTS (SELECT 1 FROM messages p2 WHERE p2.session_id = m.session_id AND p2.message_seq < m.message_seq))
5290
+ OR EXISTS (SELECT 1 FROM messages p2 WHERE p2.session_id = m.session_id AND p2.message_seq < m.message_seq AND p2.retrieval_class = 'hot'))
5115
5291
  AND NOT EXISTS (SELECT 1 FROM message_vectors mv WHERE mv.message_id = m.message_id AND mv.embed_version = ?)
5116
5292
  ORDER BY m.created_at DESC
5117
5293
  LIMIT ?`,
@@ -5132,6 +5308,260 @@ function getUnembeddedMessages(limit) {
5132
5308
  }
5133
5309
  }
5134
5310
 
5311
+ // src/recall/session-classifier.ts
5312
+ init_paths();
5313
+ init_log();
5314
+
5315
+ // src/adapters/codex/codex-jsonl-reader.ts
5316
+ var fs = __toESM(require("fs"));
5317
+ init_log();
5318
+ function parseCodexJsonlFile(filepath) {
5319
+ try {
5320
+ const content = fs.readFileSync(filepath, "utf-8");
5321
+ const lines = content.split("\n");
5322
+ const records = [];
5323
+ for (const line of lines) {
5324
+ const trimmed = line.trim();
5325
+ if (!trimmed)
5326
+ continue;
5327
+ try {
5328
+ const record = JSON.parse(trimmed);
5329
+ records.push(record);
5330
+ } catch (err) {
5331
+ log({ level: "warn", source: "codex-jsonl-reader", summary: `Skipping unparseable line: ${err.message}` });
5332
+ }
5333
+ }
5334
+ return records;
5335
+ } catch (error) {
5336
+ log({ level: "error", source: "codex-jsonl-reader", summary: `Failed to read ${filepath}: ${error instanceof Error ? error.message : String(error)}`, data: { filepath, error: String(error) } });
5337
+ return [];
5338
+ }
5339
+ }
5340
+ function extractCodexSessionMeta(filepath) {
5341
+ let fd = null;
5342
+ try {
5343
+ fd = fs.openSync(filepath, "r");
5344
+ const buffer = Buffer.alloc(8192);
5345
+ const bytesRead = fs.readSync(fd, buffer, 0, 8192, 0);
5346
+ if (bytesRead === 0)
5347
+ return null;
5348
+ const content = buffer.toString("utf-8", 0, bytesRead);
5349
+ const newlineIdx = content.indexOf("\n");
5350
+ const firstLine = newlineIdx >= 0 ? content.slice(0, newlineIdx) : content;
5351
+ const record = JSON.parse(firstLine.trim());
5352
+ if (record.type !== "session_meta")
5353
+ return null;
5354
+ const payload = record.payload;
5355
+ return {
5356
+ id: payload.id,
5357
+ cwd: payload.cwd,
5358
+ cli_version: payload.cli_version,
5359
+ git: payload.git,
5360
+ ...payload.source && typeof payload.source === "object" ? { source: payload.source } : {}
5361
+ };
5362
+ } catch {
5363
+ return null;
5364
+ } finally {
5365
+ if (fd !== null) {
5366
+ try {
5367
+ fs.closeSync(fd);
5368
+ } catch {
5369
+ }
5370
+ }
5371
+ }
5372
+ }
5373
+ var READ_BUFFER_SIZE = 64 * 1024;
5374
+ var TURN_CONTENT_CAP = 8 * 1024;
5375
+
5376
+ // src/recall/session-classifier.ts
5377
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5378
+ var ROLLOUT_UUID_RE = /rollout-.*-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
5379
+ function normalize(p) {
5380
+ return p.replace(/\\/g, "/");
5381
+ }
5382
+ function basenameNoExt(p) {
5383
+ return normalize(p).split("/").pop().replace(/\.jsonl$/i, "");
5384
+ }
5385
+ function lookupStoredProvenance(transcriptPath) {
5386
+ try {
5387
+ const row = getDb(dbPath()).get(
5388
+ `SELECT session_id, kind, parent_session_id FROM session_provenance
5389
+ WHERE transcript_path = ? LIMIT 1`,
5390
+ [normalize(transcriptPath)]
5391
+ );
5392
+ return row ?? null;
5393
+ } catch {
5394
+ return null;
5395
+ }
5396
+ }
5397
+ function claudeSubagentParent(normPath) {
5398
+ const segments = normPath.split("/");
5399
+ const idx = segments.lastIndexOf("subagents");
5400
+ if (idx <= 0)
5401
+ return null;
5402
+ const parent = segments[idx - 1];
5403
+ return parent || null;
5404
+ }
5405
+ function classifySession(args) {
5406
+ const { sessionId, transcriptPath, vendor, hook } = args;
5407
+ const normPath = normalize(transcriptPath);
5408
+ const base = basenameNoExt(transcriptPath);
5409
+ const stored = lookupStoredProvenance(transcriptPath);
5410
+ if (stored && !(stored.kind !== "agent" && hook?.isSubagent)) {
5411
+ return {
5412
+ kind: stored.kind === "agent" ? "agent" : "root",
5413
+ canonicalSessionId: stored.session_id,
5414
+ parentSessionId: stored.parent_session_id,
5415
+ agentDepth: null,
5416
+ agentMeta: null,
5417
+ aliases: aliasCandidates(stored.session_id, sessionId, hook),
5418
+ evidence: "stored"
5419
+ };
5420
+ }
5421
+ if (vendor === "claude")
5422
+ return classifyClaude(sessionId, normPath, base, hook);
5423
+ return classifyCodex(sessionId, transcriptPath, base, hook);
5424
+ }
5425
+ function aliasCandidates(canonical, proposed, hook) {
5426
+ const out = /* @__PURE__ */ new Set();
5427
+ if (hook?.agentId && hook.agentId !== canonical)
5428
+ out.add(hook.agentId);
5429
+ if (proposed && proposed !== canonical)
5430
+ out.add(proposed);
5431
+ return [...out];
5432
+ }
5433
+ function classifyClaude(sessionId, normPath, base, hook) {
5434
+ if (normPath.includes("/subagents/") && base.startsWith("agent-")) {
5435
+ return {
5436
+ kind: "agent",
5437
+ canonicalSessionId: base,
5438
+ parentSessionId: claudeSubagentParent(normPath) ?? hook?.payloadSessionId ?? null,
5439
+ agentDepth: null,
5440
+ agentMeta: hook?.agentId ? { hookAgentId: hook.agentId } : null,
5441
+ aliases: aliasCandidates(base, sessionId, hook),
5442
+ evidence: "claude-path"
5443
+ };
5444
+ }
5445
+ if (base.startsWith("agent-") || sessionId.startsWith("agent-")) {
5446
+ const canonical = base.startsWith("agent-") ? base : sessionId;
5447
+ return {
5448
+ kind: "agent",
5449
+ canonicalSessionId: canonical,
5450
+ parentSessionId: hook?.payloadSessionId ?? null,
5451
+ agentDepth: null,
5452
+ agentMeta: hook?.agentId ? { hookAgentId: hook.agentId } : null,
5453
+ aliases: aliasCandidates(canonical, sessionId, hook),
5454
+ evidence: "claude-name"
5455
+ };
5456
+ }
5457
+ if (hook?.isSubagent) {
5458
+ return {
5459
+ kind: "agent",
5460
+ canonicalSessionId: base || sessionId,
5461
+ parentSessionId: hook.payloadSessionId ?? null,
5462
+ agentDepth: null,
5463
+ agentMeta: hook.agentId ? { hookAgentId: hook.agentId } : null,
5464
+ aliases: aliasCandidates(base || sessionId, sessionId, hook),
5465
+ evidence: "hook"
5466
+ };
5467
+ }
5468
+ return {
5469
+ kind: "root",
5470
+ canonicalSessionId: sessionId,
5471
+ parentSessionId: null,
5472
+ agentDepth: null,
5473
+ agentMeta: null,
5474
+ aliases: [],
5475
+ evidence: "default-root"
5476
+ };
5477
+ }
5478
+ function classifyCodex(sessionId, transcriptPath, base, hook) {
5479
+ const meta = extractCodexSessionMeta(transcriptPath);
5480
+ const filenameUuid = ROLLOUT_UUID_RE.exec(normalize(transcriptPath))?.[1] ?? null;
5481
+ const canonical = meta?.id && typeof meta.id === "string" && meta.id.length > 0 ? meta.id : filenameUuid ?? (UUID_RE.test(sessionId) ? sessionId : null);
5482
+ const sub = parseSubagentSource(meta?.source, transcriptPath);
5483
+ if (sub.malformed) {
5484
+ log({
5485
+ source: "recall:classify",
5486
+ level: "warn",
5487
+ summary: `codex session_meta.source looks subagent-like but is malformed \u2014 leaving session hot (conservative): ${transcriptPath}`
5488
+ });
5489
+ }
5490
+ if (sub.isSubagent || hook?.isSubagent) {
5491
+ if (!canonical) {
5492
+ log({
5493
+ source: "recall:classify",
5494
+ level: "warn",
5495
+ summary: `codex subagent transcript has no derivable canonical id \u2014 skipping ingest (conservative): ${transcriptPath}`
5496
+ });
5497
+ return {
5498
+ kind: "agent",
5499
+ canonicalSessionId: base || sessionId,
5500
+ parentSessionId: sub.parentThreadId ?? hook?.payloadSessionId ?? null,
5501
+ agentDepth: sub.depth,
5502
+ agentMeta: sub.meta,
5503
+ aliases: [],
5504
+ evidence: sub.isSubagent ? "codex-meta" : "hook",
5505
+ unresolvable: true
5506
+ };
5507
+ }
5508
+ return {
5509
+ kind: "agent",
5510
+ canonicalSessionId: canonical,
5511
+ parentSessionId: sub.parentThreadId ?? hook?.payloadSessionId ?? null,
5512
+ agentDepth: sub.depth,
5513
+ agentMeta: mergeMeta(sub.meta, hook?.agentId),
5514
+ aliases: aliasCandidates(canonical, sessionId, hook),
5515
+ evidence: sub.isSubagent ? "codex-meta" : "hook"
5516
+ };
5517
+ }
5518
+ return {
5519
+ kind: "root",
5520
+ canonicalSessionId: canonical ?? sessionId,
5521
+ parentSessionId: null,
5522
+ agentDepth: null,
5523
+ agentMeta: null,
5524
+ aliases: canonical && canonical !== sessionId ? [sessionId] : [],
5525
+ evidence: "default-root"
5526
+ };
5527
+ }
5528
+ function mergeMeta(meta, hookAgentId) {
5529
+ if (!hookAgentId)
5530
+ return meta;
5531
+ return { ...meta ?? {}, hookAgentId };
5532
+ }
5533
+ function parseSubagentSource(source, _transcriptPath) {
5534
+ const none = { isSubagent: false, malformed: false, parentThreadId: null, depth: null, meta: null };
5535
+ if (!source || typeof source !== "object")
5536
+ return none;
5537
+ const subRaw = source.subagent;
5538
+ const typeTag = source.type;
5539
+ const looksSubagent = subRaw !== void 0 || typeTag === "subagent";
5540
+ if (!looksSubagent)
5541
+ return none;
5542
+ if (subRaw !== void 0 && (typeof subRaw !== "object" || subRaw === null)) {
5543
+ return { ...none, malformed: true };
5544
+ }
5545
+ const sub = subRaw ?? {};
5546
+ const spawnRaw = sub.thread_spawn ?? source.thread_spawn;
5547
+ const spawn2 = spawnRaw && typeof spawnRaw === "object" ? spawnRaw : {};
5548
+ const parentThreadId = typeof spawn2.parent_thread_id === "string" && spawn2.parent_thread_id ? spawn2.parent_thread_id : null;
5549
+ const depth = typeof spawn2.depth === "number" && Number.isFinite(spawn2.depth) ? spawn2.depth : null;
5550
+ const meta = {};
5551
+ for (const k of ["agent_path", "agent_type", "agent_name", "path", "type", "name"]) {
5552
+ const v = spawn2[k] ?? sub[k];
5553
+ if (typeof v === "string" || typeof v === "number")
5554
+ meta[k] = v;
5555
+ }
5556
+ return {
5557
+ isSubagent: true,
5558
+ malformed: false,
5559
+ parentThreadId,
5560
+ depth,
5561
+ meta: Object.keys(meta).length > 0 ? meta : null
5562
+ };
5563
+ }
5564
+
5135
5565
  // src/recall/message-ingest.ts
5136
5566
  init_paths();
5137
5567
 
@@ -5150,13 +5580,13 @@ function normalizePath(p) {
5150
5580
  init_log();
5151
5581
 
5152
5582
  // src/adapters/claude/jsonl-reader.ts
5153
- var fs = __toESM(require("fs"));
5583
+ var fs2 = __toESM(require("fs"));
5154
5584
  init_log();
5155
- var READ_BUFFER_SIZE = 64 * 1024;
5156
- var TURN_CONTENT_CAP = 8 * 1024;
5585
+ var READ_BUFFER_SIZE2 = 64 * 1024;
5586
+ var TURN_CONTENT_CAP2 = 8 * 1024;
5157
5587
  function parseJsonlFile(filepath) {
5158
5588
  try {
5159
- const content = fs.readFileSync(filepath, "utf-8");
5589
+ const content = fs2.readFileSync(filepath, "utf-8");
5160
5590
  const lines = content.split("\n");
5161
5591
  const entries = [];
5162
5592
  for (const line of lines) {
@@ -5345,37 +5775,6 @@ function adaptClaudeEntries(rawEntries) {
5345
5775
  return rawEntries.map(adaptClaudeEntry).filter((entry) => entry !== null);
5346
5776
  }
5347
5777
 
5348
- // src/adapters/codex/codex-jsonl-reader.ts
5349
- var fs2 = __toESM(require("fs"));
5350
- init_log();
5351
- var path = __toESM(require("path"));
5352
- var os = __toESM(require("os"));
5353
- var CODEX_SESSIONS_DIR = path.join(os.homedir(), ".codex", "sessions");
5354
- function parseCodexJsonlFile(filepath) {
5355
- try {
5356
- const content = fs2.readFileSync(filepath, "utf-8");
5357
- const lines = content.split("\n");
5358
- const records = [];
5359
- for (const line of lines) {
5360
- const trimmed = line.trim();
5361
- if (!trimmed)
5362
- continue;
5363
- try {
5364
- const record = JSON.parse(trimmed);
5365
- records.push(record);
5366
- } catch (err) {
5367
- log({ level: "warn", source: "codex-jsonl-reader", summary: `Skipping unparseable line: ${err.message}` });
5368
- }
5369
- }
5370
- return records;
5371
- } catch (error) {
5372
- log({ level: "error", source: "codex-jsonl-reader", summary: `Failed to read ${filepath}: ${error instanceof Error ? error.message : String(error)}`, data: { filepath, error: String(error) } });
5373
- return [];
5374
- }
5375
- }
5376
- var READ_BUFFER_SIZE2 = 64 * 1024;
5377
- var TURN_CONTENT_CAP2 = 8 * 1024;
5378
-
5379
5778
  // src/adapters/codex/codex-jsonl-adapter.ts
5380
5779
  function adaptCodexJsonlRecords(records, sessionId) {
5381
5780
  const outputIndex = /* @__PURE__ */ new Map();
@@ -6010,6 +6409,36 @@ function entriesCwd(entries) {
6010
6409
  return void 0;
6011
6410
  }
6012
6411
  async function ingestSessionMessages(sessionId, transcriptPath, vendor, options) {
6412
+ const classification = classifySession({
6413
+ sessionId,
6414
+ transcriptPath,
6415
+ vendor,
6416
+ ...options?.hook ? { hook: options.hook } : {}
6417
+ });
6418
+ if (classification.unresolvable) {
6419
+ return {
6420
+ sessionId,
6421
+ chunksCreated: 0,
6422
+ skipped: true,
6423
+ error: "subagent transcript has no derivable canonical identity \u2014 skipped (conservative)"
6424
+ };
6425
+ }
6426
+ const canonicalId = classification.canonicalSessionId;
6427
+ const retrievalClass = classification.kind === "agent" ? "agent" : "hot";
6428
+ const provenance = {
6429
+ sessionId: canonicalId,
6430
+ vendor,
6431
+ kind: classification.kind,
6432
+ parentSessionId: classification.parentSessionId,
6433
+ agentDepth: classification.agentDepth,
6434
+ agentMeta: classification.agentMeta,
6435
+ transcriptPath: transcriptPath.replace(/\\/g, "/")
6436
+ };
6437
+ const aliases = classification.aliases.map((aliasId) => ({
6438
+ aliasId,
6439
+ sessionId: canonicalId,
6440
+ source: options?.hook?.agentId === aliasId ? "hook-agent-id" : "ingest-request-id"
6441
+ }));
6013
6442
  let rawEntries;
6014
6443
  try {
6015
6444
  if (vendor === "claude") {
@@ -6017,18 +6446,25 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
6017
6446
  rawEntries = adaptClaudeEntries(raw);
6018
6447
  } else {
6019
6448
  const envelopes = parseCodexJsonlFile(transcriptPath);
6020
- rawEntries = adaptCodexJsonlRecords(envelopes, sessionId);
6449
+ rawEntries = adaptCodexJsonlRecords(envelopes, canonicalId);
6021
6450
  }
6022
6451
  } catch (err) {
6023
6452
  return {
6024
- sessionId,
6453
+ sessionId: canonicalId,
6025
6454
  chunksCreated: 0,
6026
6455
  skipped: false,
6027
- error: `Failed to load session: ${err instanceof Error ? err.message : String(err)}`
6456
+ error: `Failed to load session: ${err instanceof Error ? err.message : String(err)}`,
6457
+ retrievalClass
6028
6458
  };
6029
6459
  }
6030
6460
  if (rawEntries.length === 0) {
6031
- return { sessionId, chunksCreated: 0, skipped: true };
6461
+ if (options?.force) {
6462
+ try {
6463
+ insertMessages([], { replaceSessionId: canonicalId, provenance, aliases });
6464
+ } catch {
6465
+ }
6466
+ }
6467
+ return { sessionId: canonicalId, chunksCreated: 0, skipped: true, retrievalClass };
6032
6468
  }
6033
6469
  const rawProjectId = options?.projectId ?? entriesCwd(rawEntries);
6034
6470
  const projectId = rawProjectId ? normalizePath(rawProjectId) : null;
@@ -6045,31 +6481,44 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
6045
6481
  const createdAt = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
6046
6482
  records.push({
6047
6483
  message_id: entry.uuid,
6048
- session_id: sessionId,
6484
+ session_id: canonicalId,
6049
6485
  message_seq: i,
6050
6486
  message_text: text,
6051
6487
  project_id: projectId,
6052
6488
  created_at: createdAt,
6053
- message_role: entry.message?.role ?? entry.type ?? null
6489
+ message_role: entry.message?.role ?? entry.type ?? null,
6490
+ retrieval_class: retrievalClass
6054
6491
  });
6055
6492
  }
6056
6493
  if (records.length === 0) {
6057
- return { sessionId, chunksCreated: 0, skipped: true };
6494
+ if (options?.force) {
6495
+ try {
6496
+ insertMessages([], { replaceSessionId: canonicalId, provenance, aliases });
6497
+ } catch {
6498
+ }
6499
+ }
6500
+ return { sessionId: canonicalId, chunksCreated: 0, skipped: true, retrievalClass };
6058
6501
  }
6059
6502
  try {
6060
- insertMessages(records, options?.force ? { replaceSessionId: sessionId } : void 0);
6503
+ insertMessages(records, {
6504
+ ...options?.force ? { replaceSessionId: canonicalId } : {},
6505
+ provenance,
6506
+ aliases
6507
+ });
6061
6508
  } catch (err) {
6062
6509
  return {
6063
- sessionId,
6510
+ sessionId: canonicalId,
6064
6511
  chunksCreated: 0,
6065
6512
  skipped: false,
6066
- error: `DB insert failed: ${err instanceof Error ? err.message : String(err)}`
6513
+ error: `DB insert failed: ${err instanceof Error ? err.message : String(err)}`,
6514
+ retrievalClass
6067
6515
  };
6068
6516
  }
6069
6517
  return {
6070
- sessionId,
6518
+ sessionId: canonicalId,
6071
6519
  chunksCreated: records.length,
6072
- skipped: false
6520
+ skipped: false,
6521
+ retrievalClass
6073
6522
  };
6074
6523
  }
6075
6524
  var MAX_EMBED_CHARS = 14e3;
@@ -6113,14 +6562,14 @@ async function embedSessionMessages(sessionId, force) {
6113
6562
  const rows = d.all(
6114
6563
  force ? `SELECT m.message_id, m.message_text,
6115
6564
  (SELECT p.message_text FROM messages p
6116
- WHERE p.session_id = m.session_id AND p.message_seq < m.message_seq
6565
+ WHERE p.session_id = m.session_id AND p.message_seq < m.message_seq AND p.retrieval_class = 'hot'
6117
6566
  ORDER BY p.message_seq DESC LIMIT 1) AS prev_text
6118
- FROM messages m WHERE m.session_id = ? ORDER BY m.message_seq ASC` : `SELECT m.message_id, m.message_text,
6567
+ FROM messages m WHERE m.session_id = ? AND m.retrieval_class = 'hot' ORDER BY m.message_seq ASC` : `SELECT m.message_id, m.message_text,
6119
6568
  (SELECT p.message_text FROM messages p
6120
- WHERE p.session_id = m.session_id AND p.message_seq < m.message_seq
6569
+ WHERE p.session_id = m.session_id AND p.message_seq < m.message_seq AND p.retrieval_class = 'hot'
6121
6570
  ORDER BY p.message_seq DESC LIMIT 1) AS prev_text
6122
6571
  FROM messages m
6123
- WHERE m.session_id = ?
6572
+ WHERE m.session_id = ? AND m.retrieval_class = 'hot'
6124
6573
  AND NOT EXISTS (SELECT 1 FROM message_vectors mv WHERE mv.message_id = m.message_id AND mv.embed_version = ?)
6125
6574
  ORDER BY m.message_seq ASC`,
6126
6575
  force ? [sessionId] : [sessionId, EMBED_VERSION]
@@ -6173,7 +6622,9 @@ init_embedder();
6173
6622
  var import_node_fs5 = require("node:fs");
6174
6623
  var import_node_path5 = require("node:path");
6175
6624
  init_paths();
6176
- var LOCK_PATH = (0, import_node_path5.join)(runDir(), "embed.lock");
6625
+ function embedLockPath() {
6626
+ return (0, import_node_path5.join)(runDir(), "embed.lock");
6627
+ }
6177
6628
  var STALE_MS = 30 * 60 * 1e3;
6178
6629
  function isAlive(pid) {
6179
6630
  try {
@@ -6186,21 +6637,21 @@ function isAlive(pid) {
6186
6637
  function tryAcquireEmbedLock() {
6187
6638
  for (let attempt = 0; attempt < 2; attempt++) {
6188
6639
  try {
6189
- (0, import_node_fs5.writeFileSync)(LOCK_PATH, String(process.pid), { flag: "wx" });
6640
+ (0, import_node_fs5.writeFileSync)(embedLockPath(), String(process.pid), { flag: "wx" });
6190
6641
  return true;
6191
6642
  } catch {
6192
6643
  let heldPid;
6193
6644
  let ageMs;
6194
6645
  try {
6195
- heldPid = parseInt((0, import_node_fs5.readFileSync)(LOCK_PATH, "utf8"), 10);
6196
- ageMs = Date.now() - (0, import_node_fs5.statSync)(LOCK_PATH).mtimeMs;
6646
+ heldPid = parseInt((0, import_node_fs5.readFileSync)(embedLockPath(), "utf8"), 10);
6647
+ ageMs = Date.now() - (0, import_node_fs5.statSync)(embedLockPath()).mtimeMs;
6197
6648
  } catch {
6198
6649
  continue;
6199
6650
  }
6200
6651
  if (isAlive(heldPid) && ageMs < STALE_MS)
6201
6652
  return false;
6202
6653
  try {
6203
- (0, import_node_fs5.unlinkSync)(LOCK_PATH);
6654
+ (0, import_node_fs5.unlinkSync)(embedLockPath());
6204
6655
  } catch {
6205
6656
  }
6206
6657
  }
@@ -6209,9 +6660,9 @@ function tryAcquireEmbedLock() {
6209
6660
  }
6210
6661
  function releaseEmbedLock() {
6211
6662
  try {
6212
- const held = parseInt((0, import_node_fs5.readFileSync)(LOCK_PATH, "utf8"), 10);
6663
+ const held = parseInt((0, import_node_fs5.readFileSync)(embedLockPath(), "utf8"), 10);
6213
6664
  if (held === process.pid)
6214
- (0, import_node_fs5.unlinkSync)(LOCK_PATH);
6665
+ (0, import_node_fs5.unlinkSync)(embedLockPath());
6215
6666
  } catch {
6216
6667
  }
6217
6668
  }
@@ -7021,11 +7472,11 @@ var qmarksTestNoExtDot = ([$0]) => {
7021
7472
  return (f) => f.length === len && f !== "." && f !== "..";
7022
7473
  };
7023
7474
  var defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix";
7024
- var path2 = {
7475
+ var path = {
7025
7476
  win32: { sep: "\\" },
7026
7477
  posix: { sep: "/" }
7027
7478
  };
7028
- var sep = defaultPlatform === "win32" ? path2.win32.sep : path2.posix.sep;
7479
+ var sep = defaultPlatform === "win32" ? path.win32.sep : path.posix.sep;
7029
7480
  minimatch.sep = sep;
7030
7481
  var GLOBSTAR = Symbol("globstar **");
7031
7482
  minimatch.GLOBSTAR = GLOBSTAR;
@@ -10057,7 +10508,7 @@ var ENOCHILD = ENOTDIR | ENOENT | ENOREALPATH;
10057
10508
  var TYPEMASK = 1023;
10058
10509
  var entToType = (s) => s.isFile() ? IFREG : s.isDirectory() ? IFDIR : s.isSymbolicLink() ? IFLNK : s.isCharacterDevice() ? IFCHR : s.isBlockDevice() ? IFBLK : s.isSocket() ? IFSOCK : s.isFIFO() ? IFIFO : UNKNOWN;
10059
10510
  var normalizeCache = /* @__PURE__ */ new Map();
10060
- var normalize = (s) => {
10511
+ var normalize2 = (s) => {
10061
10512
  const c = normalizeCache.get(s);
10062
10513
  if (c)
10063
10514
  return c;
@@ -10070,7 +10521,7 @@ var normalizeNocase = (s) => {
10070
10521
  const c = normalizeNocaseCache.get(s);
10071
10522
  if (c)
10072
10523
  return c;
10073
- const n = normalize(s.toLowerCase());
10524
+ const n = normalize2(s.toLowerCase());
10074
10525
  normalizeNocaseCache.set(s, n);
10075
10526
  return n;
10076
10527
  };
@@ -10237,7 +10688,7 @@ var PathBase = class {
10237
10688
  */
10238
10689
  constructor(name, type = UNKNOWN, root, roots, nocase, children, opts) {
10239
10690
  this.name = name;
10240
- this.#matchName = nocase ? normalizeNocase(name) : normalize(name);
10691
+ this.#matchName = nocase ? normalizeNocase(name) : normalize2(name);
10241
10692
  this.#type = type & TYPEMASK;
10242
10693
  this.nocase = nocase;
10243
10694
  this.roots = roots;
@@ -10274,12 +10725,12 @@ var PathBase = class {
10274
10725
  /**
10275
10726
  * Get the Path object referenced by the string path, resolved from this Path
10276
10727
  */
10277
- resolve(path3) {
10278
- if (!path3) {
10728
+ resolve(path2) {
10729
+ if (!path2) {
10279
10730
  return this;
10280
10731
  }
10281
- const rootPath = this.getRootString(path3);
10282
- const dir = path3.substring(rootPath.length);
10732
+ const rootPath = this.getRootString(path2);
10733
+ const dir = path2.substring(rootPath.length);
10283
10734
  const dirParts = dir.split(this.splitSep);
10284
10735
  const result = rootPath ? this.getRoot(rootPath).#resolveParts(dirParts) : this.#resolveParts(dirParts);
10285
10736
  return result;
@@ -10330,7 +10781,7 @@ var PathBase = class {
10330
10781
  return this.parent || this;
10331
10782
  }
10332
10783
  const children = this.children();
10333
- const name = this.nocase ? normalizeNocase(pathPart) : normalize(pathPart);
10784
+ const name = this.nocase ? normalizeNocase(pathPart) : normalize2(pathPart);
10334
10785
  for (const p of children) {
10335
10786
  if (p.#matchName === name) {
10336
10787
  return p;
@@ -10575,7 +11026,7 @@ var PathBase = class {
10575
11026
  * directly.
10576
11027
  */
10577
11028
  isNamed(n) {
10578
- return !this.nocase ? this.#matchName === normalize(n) : this.#matchName === normalizeNocase(n);
11029
+ return !this.nocase ? this.#matchName === normalize2(n) : this.#matchName === normalizeNocase(n);
10579
11030
  }
10580
11031
  /**
10581
11032
  * Return the Path object corresponding to the target of a symbolic link.
@@ -10714,7 +11165,7 @@ var PathBase = class {
10714
11165
  #readdirMaybePromoteChild(e, c) {
10715
11166
  for (let p = c.provisional; p < c.length; p++) {
10716
11167
  const pchild = c[p];
10717
- const name = this.nocase ? normalizeNocase(e.name) : normalize(e.name);
11168
+ const name = this.nocase ? normalizeNocase(e.name) : normalize2(e.name);
10718
11169
  if (name !== pchild.#matchName) {
10719
11170
  continue;
10720
11171
  }
@@ -11031,8 +11482,8 @@ var PathWin32 = class _PathWin32 extends PathBase {
11031
11482
  /**
11032
11483
  * @internal
11033
11484
  */
11034
- getRootString(path3) {
11035
- return import_node_path6.win32.parse(path3).root;
11485
+ getRootString(path2) {
11486
+ return import_node_path6.win32.parse(path2).root;
11036
11487
  }
11037
11488
  /**
11038
11489
  * @internal
@@ -11078,8 +11529,8 @@ var PathPosix = class _PathPosix extends PathBase {
11078
11529
  /**
11079
11530
  * @internal
11080
11531
  */
11081
- getRootString(path3) {
11082
- return path3.startsWith("/") ? "/" : "";
11532
+ getRootString(path2) {
11533
+ return path2.startsWith("/") ? "/" : "";
11083
11534
  }
11084
11535
  /**
11085
11536
  * @internal
@@ -11168,11 +11619,11 @@ var PathScurryBase = class {
11168
11619
  /**
11169
11620
  * Get the depth of a provided path, string, or the cwd
11170
11621
  */
11171
- depth(path3 = this.cwd) {
11172
- if (typeof path3 === "string") {
11173
- path3 = this.cwd.resolve(path3);
11622
+ depth(path2 = this.cwd) {
11623
+ if (typeof path2 === "string") {
11624
+ path2 = this.cwd.resolve(path2);
11174
11625
  }
11175
- return path3.depth();
11626
+ return path2.depth();
11176
11627
  }
11177
11628
  /**
11178
11629
  * Return the cache of child entries. Exposed so subclasses can create
@@ -11659,9 +12110,9 @@ var PathScurryBase = class {
11659
12110
  process2();
11660
12111
  return results;
11661
12112
  }
11662
- chdir(path3 = this.cwd) {
12113
+ chdir(path2 = this.cwd) {
11663
12114
  const oldCwd = this.cwd;
11664
- this.cwd = typeof path3 === "string" ? this.cwd.resolve(path3) : path3;
12115
+ this.cwd = typeof path2 === "string" ? this.cwd.resolve(path2) : path2;
11665
12116
  this.cwd[setAsCwd](oldCwd);
11666
12117
  }
11667
12118
  };
@@ -12017,8 +12468,8 @@ var MatchRecord = class {
12017
12468
  }
12018
12469
  // match, absolute, ifdir
12019
12470
  entries() {
12020
- return [...this.store.entries()].map(([path3, n]) => [
12021
- path3,
12471
+ return [...this.store.entries()].map(([path2, n]) => [
12472
+ path2,
12022
12473
  !!(n & 2),
12023
12474
  !!(n & 1)
12024
12475
  ]);
@@ -12223,9 +12674,9 @@ var GlobUtil = class {
12223
12674
  signal;
12224
12675
  maxDepth;
12225
12676
  includeChildMatches;
12226
- constructor(patterns, path3, opts) {
12677
+ constructor(patterns, path2, opts) {
12227
12678
  this.patterns = patterns;
12228
- this.path = path3;
12679
+ this.path = path2;
12229
12680
  this.opts = opts;
12230
12681
  this.#sep = !opts.posix && opts.platform === "win32" ? "\\" : "/";
12231
12682
  this.includeChildMatches = opts.includeChildMatches !== false;
@@ -12244,11 +12695,11 @@ var GlobUtil = class {
12244
12695
  });
12245
12696
  }
12246
12697
  }
12247
- #ignored(path3) {
12248
- return this.seen.has(path3) || !!this.#ignore?.ignored?.(path3);
12698
+ #ignored(path2) {
12699
+ return this.seen.has(path2) || !!this.#ignore?.ignored?.(path2);
12249
12700
  }
12250
- #childrenIgnored(path3) {
12251
- return !!this.#ignore?.childrenIgnored?.(path3);
12701
+ #childrenIgnored(path2) {
12702
+ return !!this.#ignore?.childrenIgnored?.(path2);
12252
12703
  }
12253
12704
  // backpressure mechanism
12254
12705
  pause() {
@@ -12463,8 +12914,8 @@ var GlobUtil = class {
12463
12914
  };
12464
12915
  var GlobWalker = class extends GlobUtil {
12465
12916
  matches = /* @__PURE__ */ new Set();
12466
- constructor(patterns, path3, opts) {
12467
- super(patterns, path3, opts);
12917
+ constructor(patterns, path2, opts) {
12918
+ super(patterns, path2, opts);
12468
12919
  }
12469
12920
  matchEmit(e) {
12470
12921
  this.matches.add(e);
@@ -12501,8 +12952,8 @@ var GlobWalker = class extends GlobUtil {
12501
12952
  };
12502
12953
  var GlobStream = class extends GlobUtil {
12503
12954
  results;
12504
- constructor(patterns, path3, opts) {
12505
- super(patterns, path3, opts);
12955
+ constructor(patterns, path2, opts) {
12956
+ super(patterns, path2, opts);
12506
12957
  this.results = new Minipass({
12507
12958
  signal: this.signal,
12508
12959
  objectMode: true
@@ -12890,7 +13341,7 @@ var HEARTBEAT_INTERVAL_MS = 5 * 60 * 1e3;
12890
13341
  process.on("SIGTERM", onSignal);
12891
13342
  const heartbeat = setInterval(() => {
12892
13343
  try {
12893
- (0, import_node_fs7.utimesSync)(LOCK_PATH, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
13344
+ (0, import_node_fs7.utimesSync)(embedLockPath(), /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
12894
13345
  } catch {
12895
13346
  }
12896
13347
  }, HEARTBEAT_INTERVAL_MS);
@@ -12937,4 +13388,6 @@ var HEARTBEAT_INTERVAL_MS = 5 * 60 * 1e3;
12937
13388
  }
12938
13389
  releaseEmbedLock();
12939
13390
  }
12940
- })();
13391
+ })().catch(() => {
13392
+ process.exit(0);
13393
+ });