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.
- package/README.md +230 -192
- package/THIRD-PARTY-LICENSES +43 -145
- package/dist/SKILL.md.template +7 -0
- package/dist/embed-pending.js +642 -189
- package/dist/recall.js +4935 -3049
- package/dist/statusline.js +180 -0
- package/dist/stop-hook.js +607 -143
- package/package.json +2 -1
package/dist/stop-hook.js
CHANGED
|
@@ -153,21 +153,21 @@ var require_file_uri_to_path = __commonJS({
|
|
|
153
153
|
var rest = decodeURI(uri.substring(7));
|
|
154
154
|
var firstSlash = rest.indexOf("/");
|
|
155
155
|
var host = rest.substring(0, firstSlash);
|
|
156
|
-
var
|
|
156
|
+
var path = rest.substring(firstSlash + 1);
|
|
157
157
|
if ("localhost" == host)
|
|
158
158
|
host = "";
|
|
159
159
|
if (host) {
|
|
160
160
|
host = sep + sep + host;
|
|
161
161
|
}
|
|
162
|
-
|
|
162
|
+
path = path.replace(/^(.+)\|/, "$1:");
|
|
163
163
|
if (sep == "\\") {
|
|
164
|
-
|
|
164
|
+
path = path.replace(/\//g, "\\");
|
|
165
165
|
}
|
|
166
|
-
if (/^.+\:/.test(
|
|
166
|
+
if (/^.+\:/.test(path)) {
|
|
167
167
|
} else {
|
|
168
|
-
|
|
168
|
+
path = sep + path;
|
|
169
169
|
}
|
|
170
|
-
return host +
|
|
170
|
+
return host + path;
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
});
|
|
@@ -176,18 +176,18 @@ var require_file_uri_to_path = __commonJS({
|
|
|
176
176
|
var require_bindings = __commonJS({
|
|
177
177
|
"node_modules/bindings/bindings.js"(exports2, module2) {
|
|
178
178
|
var fs3 = require("fs");
|
|
179
|
-
var
|
|
179
|
+
var path = require("path");
|
|
180
180
|
var fileURLToPath = require_file_uri_to_path();
|
|
181
|
-
var
|
|
182
|
-
var dirname2 =
|
|
183
|
-
var exists = fs3.accessSync && function(
|
|
181
|
+
var join4 = path.join;
|
|
182
|
+
var dirname2 = path.dirname;
|
|
183
|
+
var exists = fs3.accessSync && function(path2) {
|
|
184
184
|
try {
|
|
185
|
-
fs3.accessSync(
|
|
185
|
+
fs3.accessSync(path2);
|
|
186
186
|
} catch (e) {
|
|
187
187
|
return false;
|
|
188
188
|
}
|
|
189
189
|
return true;
|
|
190
|
-
} || fs3.existsSync ||
|
|
190
|
+
} || fs3.existsSync || path.existsSync;
|
|
191
191
|
var defaults = {
|
|
192
192
|
arrow: process.env.NODE_BINDINGS_ARROW || " \u2192 ",
|
|
193
193
|
compiled: process.env.NODE_BINDINGS_COMPILED_DIR || "compiled",
|
|
@@ -233,13 +233,13 @@ var require_bindings = __commonJS({
|
|
|
233
233
|
if (!opts.module_root) {
|
|
234
234
|
opts.module_root = exports2.getRoot(exports2.getFileName());
|
|
235
235
|
}
|
|
236
|
-
if (
|
|
236
|
+
if (path.extname(opts.bindings) != ".node") {
|
|
237
237
|
opts.bindings += ".node";
|
|
238
238
|
}
|
|
239
239
|
var requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;
|
|
240
240
|
var tries = [], i = 0, l = opts.try.length, n, b, err;
|
|
241
241
|
for (; i < l; i++) {
|
|
242
|
-
n =
|
|
242
|
+
n = join4.apply(
|
|
243
243
|
null,
|
|
244
244
|
opts.try[i].map(function(p) {
|
|
245
245
|
return opts[p] || p;
|
|
@@ -300,7 +300,7 @@ var require_bindings = __commonJS({
|
|
|
300
300
|
if (dir === ".") {
|
|
301
301
|
dir = process.cwd();
|
|
302
302
|
}
|
|
303
|
-
if (exists(
|
|
303
|
+
if (exists(join4(dir, "package.json")) || exists(join4(dir, "node_modules"))) {
|
|
304
304
|
return dir;
|
|
305
305
|
}
|
|
306
306
|
if (prev === dir) {
|
|
@@ -309,7 +309,7 @@ var require_bindings = __commonJS({
|
|
|
309
309
|
);
|
|
310
310
|
}
|
|
311
311
|
prev = dir;
|
|
312
|
-
dir =
|
|
312
|
+
dir = join4(dir, "..");
|
|
313
313
|
}
|
|
314
314
|
};
|
|
315
315
|
}
|
|
@@ -477,7 +477,7 @@ var require_backup = __commonJS({
|
|
|
477
477
|
"node_modules/better-sqlite3/lib/methods/backup.js"(exports2, module2) {
|
|
478
478
|
"use strict";
|
|
479
479
|
var fs3 = require("fs");
|
|
480
|
-
var
|
|
480
|
+
var path = require("path");
|
|
481
481
|
var { promisify } = require("util");
|
|
482
482
|
var { cppdb } = require_util();
|
|
483
483
|
var fsAccess = promisify(fs3.access);
|
|
@@ -501,7 +501,7 @@ var require_backup = __commonJS({
|
|
|
501
501
|
throw new TypeError('The "attached" option cannot be an empty string');
|
|
502
502
|
if (handler != null && typeof handler !== "function")
|
|
503
503
|
throw new TypeError('Expected the "progress" option to be a function');
|
|
504
|
-
await fsAccess(
|
|
504
|
+
await fsAccess(path.dirname(filename)).catch(() => {
|
|
505
505
|
throw new TypeError("Cannot save backup because the directory does not exist");
|
|
506
506
|
});
|
|
507
507
|
const isNewFile = await fsAccess(filename).then(() => false, () => true);
|
|
@@ -832,7 +832,7 @@ var require_database = __commonJS({
|
|
|
832
832
|
"node_modules/better-sqlite3/lib/database.js"(exports2, module2) {
|
|
833
833
|
"use strict";
|
|
834
834
|
var fs3 = require("fs");
|
|
835
|
-
var
|
|
835
|
+
var path = require("path");
|
|
836
836
|
var util = require_util();
|
|
837
837
|
var SqliteError = require_sqlite_error();
|
|
838
838
|
var DEFAULT_ADDON;
|
|
@@ -879,7 +879,7 @@ var require_database = __commonJS({
|
|
|
879
879
|
addon = DEFAULT_ADDON || (DEFAULT_ADDON = require_bindings()("better_sqlite3.node"));
|
|
880
880
|
} else if (typeof nativeBinding === "string") {
|
|
881
881
|
const requireFunc = typeof __non_webpack_require__ === "function" ? __non_webpack_require__ : require;
|
|
882
|
-
addon = requireFunc(
|
|
882
|
+
addon = requireFunc(path.resolve(nativeBinding).replace(/(\.node)?$/, ".node"));
|
|
883
883
|
} else {
|
|
884
884
|
addon = nativeBinding;
|
|
885
885
|
}
|
|
@@ -887,7 +887,7 @@ var require_database = __commonJS({
|
|
|
887
887
|
addon.setErrorConstructor(SqliteError);
|
|
888
888
|
addon.isInitialized = true;
|
|
889
889
|
}
|
|
890
|
-
if (!anonymous && !filename.startsWith("file:") && !fs3.existsSync(
|
|
890
|
+
if (!anonymous && !filename.startsWith("file:") && !fs3.existsSync(path.dirname(filename))) {
|
|
891
891
|
throw new TypeError("Cannot open database because the directory does not exist");
|
|
892
892
|
}
|
|
893
893
|
Object.defineProperties(this, {
|
|
@@ -933,6 +933,7 @@ var init_quantize = __esm({
|
|
|
933
933
|
// src/hooks/stop-hook.ts
|
|
934
934
|
var stop_hook_exports = {};
|
|
935
935
|
__export(stop_hook_exports, {
|
|
936
|
+
resolveIngestTarget: () => resolveIngestTarget,
|
|
936
937
|
vendorForTranscript: () => vendorForTranscript
|
|
937
938
|
});
|
|
938
939
|
module.exports = __toCommonJS(stop_hook_exports);
|
|
@@ -986,6 +987,15 @@ Run \`recall doctor\` for details.`
|
|
|
986
987
|
this.name = "BindingLoadError";
|
|
987
988
|
}
|
|
988
989
|
};
|
|
990
|
+
var MigrationPendingError = class extends Error {
|
|
991
|
+
constructor(dbPath2) {
|
|
992
|
+
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.)"
|
|
994
|
+
);
|
|
995
|
+
this.dbPath = dbPath2;
|
|
996
|
+
this.name = "MigrationPendingError";
|
|
997
|
+
}
|
|
998
|
+
};
|
|
989
999
|
function isBindingLoadError(e) {
|
|
990
1000
|
if (!e || typeof e !== "object")
|
|
991
1001
|
return false;
|
|
@@ -1000,7 +1010,7 @@ function isBindingLoadError(e) {
|
|
|
1000
1010
|
}
|
|
1001
1011
|
var db = null;
|
|
1002
1012
|
var currentDbPath = null;
|
|
1003
|
-
function getDb(dbPath2) {
|
|
1013
|
+
function getDb(dbPath2, opts) {
|
|
1004
1014
|
if (db && currentDbPath === dbPath2)
|
|
1005
1015
|
return db;
|
|
1006
1016
|
if (db) {
|
|
@@ -1015,12 +1025,45 @@ function getDb(dbPath2) {
|
|
|
1015
1025
|
raw.close();
|
|
1016
1026
|
throw e;
|
|
1017
1027
|
}
|
|
1018
|
-
|
|
1028
|
+
const adapter = createAdapter(raw);
|
|
1029
|
+
if (isRetrievalMigrationPending(adapter)) {
|
|
1030
|
+
if (!opts?.allowPendingMigration) {
|
|
1031
|
+
raw.close();
|
|
1032
|
+
throw new MigrationPendingError(dbPath2);
|
|
1033
|
+
}
|
|
1034
|
+
db = adapter;
|
|
1035
|
+
currentDbPath = dbPath2;
|
|
1036
|
+
log({ source: "db", level: "info", summary: `DB: opened pending-migration DB at ${dbPath2} (installer mode)` });
|
|
1037
|
+
return db;
|
|
1038
|
+
}
|
|
1039
|
+
db = adapter;
|
|
1019
1040
|
currentDbPath = dbPath2;
|
|
1020
1041
|
ensureSchema(db);
|
|
1021
1042
|
log({ source: "db", level: "info", summary: `DB: initialized at ${dbPath2}` });
|
|
1022
1043
|
return db;
|
|
1023
1044
|
}
|
|
1045
|
+
var RETRIEVAL_MIGRATION_KEY = "retrieval_class_migration";
|
|
1046
|
+
function isRetrievalMigrationPending(d) {
|
|
1047
|
+
try {
|
|
1048
|
+
const hasMessages = d.get(
|
|
1049
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'messages'`
|
|
1050
|
+
);
|
|
1051
|
+
if (!hasMessages)
|
|
1052
|
+
return false;
|
|
1053
|
+
const hasMeta = d.get(
|
|
1054
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'schema_meta'`
|
|
1055
|
+
);
|
|
1056
|
+
if (!hasMeta)
|
|
1057
|
+
return true;
|
|
1058
|
+
const row = d.get(
|
|
1059
|
+
`SELECT value FROM schema_meta WHERE key = ?`,
|
|
1060
|
+
[RETRIEVAL_MIGRATION_KEY]
|
|
1061
|
+
);
|
|
1062
|
+
return row?.value !== "complete";
|
|
1063
|
+
} catch {
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1024
1067
|
function closeDb() {
|
|
1025
1068
|
if (db) {
|
|
1026
1069
|
db.close();
|
|
@@ -1131,16 +1174,18 @@ function cleanupWasmArtifacts(dbPath2) {
|
|
|
1131
1174
|
} catch {
|
|
1132
1175
|
}
|
|
1133
1176
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1177
|
+
var RETRIEVAL_SCHEMA_DDL = {
|
|
1178
|
+
/** messages + provenance tables + indexes (no FTS objects). */
|
|
1179
|
+
tables: `
|
|
1136
1180
|
CREATE TABLE IF NOT EXISTS messages (
|
|
1137
|
-
message_id
|
|
1138
|
-
session_id
|
|
1139
|
-
message_seq
|
|
1140
|
-
message_text
|
|
1141
|
-
project_id
|
|
1142
|
-
created_at
|
|
1143
|
-
message_role
|
|
1181
|
+
message_id TEXT PRIMARY KEY,
|
|
1182
|
+
session_id TEXT NOT NULL,
|
|
1183
|
+
message_seq INTEGER NOT NULL,
|
|
1184
|
+
message_text TEXT NOT NULL,
|
|
1185
|
+
project_id TEXT,
|
|
1186
|
+
created_at INTEGER NOT NULL,
|
|
1187
|
+
message_role TEXT,
|
|
1188
|
+
retrieval_class TEXT NOT NULL DEFAULT 'hot',
|
|
1144
1189
|
UNIQUE(session_id, message_id)
|
|
1145
1190
|
);
|
|
1146
1191
|
|
|
@@ -1152,68 +1197,134 @@ function ensureSchema(db3) {
|
|
|
1152
1197
|
-- embed drain and every Stop-hook catch-up. This index serves the ORDER BY so
|
|
1153
1198
|
-- the planner walks it and early-terminates at LIMIT instead.
|
|
1154
1199
|
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
|
|
1155
|
-
|
|
1156
|
-
|
|
1200
|
+
|
|
1201
|
+
-- session_provenance \u2014 durable per-session classification evidence:
|
|
1202
|
+
-- canonical id, vendor, root-vs-agent kind, parent thread, hook/agent
|
|
1203
|
+
-- metadata, and the transcript path \u2192 canonical id mapping that lets
|
|
1204
|
+
-- later T1/mtime scans resolve a child to ONE identity.
|
|
1205
|
+
CREATE TABLE IF NOT EXISTS session_provenance (
|
|
1206
|
+
session_id TEXT PRIMARY KEY,
|
|
1207
|
+
vendor TEXT NOT NULL,
|
|
1208
|
+
kind TEXT NOT NULL,
|
|
1209
|
+
parent_session_id TEXT,
|
|
1210
|
+
agent_depth INTEGER,
|
|
1211
|
+
agent_meta TEXT,
|
|
1212
|
+
transcript_path TEXT,
|
|
1213
|
+
updated_at INTEGER NOT NULL
|
|
1214
|
+
);
|
|
1215
|
+
CREATE INDEX IF NOT EXISTS idx_provenance_path ON session_provenance(transcript_path);
|
|
1216
|
+
|
|
1217
|
+
-- session_aliases \u2014 alternate identifiers (e.g. a hook agent_id that
|
|
1218
|
+
-- differs from the child rollout's session-meta UUID) \u2192 canonical id.
|
|
1219
|
+
CREATE TABLE IF NOT EXISTS session_aliases (
|
|
1220
|
+
alias_id TEXT PRIMARY KEY,
|
|
1221
|
+
session_id TEXT NOT NULL,
|
|
1222
|
+
source TEXT,
|
|
1223
|
+
created_at INTEGER NOT NULL
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
1227
|
+
key TEXT PRIMARY KEY,
|
|
1228
|
+
value TEXT NOT NULL
|
|
1229
|
+
);
|
|
1230
|
+
`,
|
|
1231
|
+
/** Filtered external-content view + FTS + four-state triggers + vocab. */
|
|
1232
|
+
fts: `
|
|
1233
|
+
-- Filtered external-content source: FTS5 rebuild/integrity-check read
|
|
1234
|
+
-- THIS view, so 'rebuild' repopulates only hot rows and the (rank-1)
|
|
1235
|
+
-- integrity check compares against the filtered corpus.
|
|
1236
|
+
CREATE VIEW IF NOT EXISTS searchable_messages AS
|
|
1237
|
+
SELECT rowid, message_text FROM messages WHERE retrieval_class = 'hot';
|
|
1238
|
+
|
|
1157
1239
|
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
1158
1240
|
message_text,
|
|
1159
|
-
content=
|
|
1241
|
+
content=searchable_messages,
|
|
1160
1242
|
content_rowid=rowid,
|
|
1161
1243
|
tokenize='porter unicode61'
|
|
1162
1244
|
);
|
|
1163
1245
|
|
|
1164
|
-
|
|
1246
|
+
-- Four-state trigger behavior:
|
|
1247
|
+
-- insert hot \u2192 add | insert agent \u2192 no-op
|
|
1248
|
+
-- delete hot \u2192 delete | delete agent \u2192 no-op
|
|
1249
|
+
-- update hot\u2192hot \u2192 delete old + add new
|
|
1250
|
+
-- update hot\u2192agent \u2192 delete old only
|
|
1251
|
+
-- update agent\u2192hot \u2192 add new only
|
|
1252
|
+
-- update agent\u2192agent \u2192 no-op
|
|
1253
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_ai AFTER INSERT ON messages
|
|
1254
|
+
WHEN new.retrieval_class = 'hot'
|
|
1255
|
+
BEGIN
|
|
1165
1256
|
INSERT INTO messages_fts(rowid, message_text) VALUES (new.rowid, new.message_text);
|
|
1166
1257
|
END;
|
|
1167
1258
|
|
|
1168
|
-
CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages
|
|
1259
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages
|
|
1260
|
+
WHEN old.retrieval_class = 'hot'
|
|
1261
|
+
BEGIN
|
|
1169
1262
|
INSERT INTO messages_fts(messages_fts, rowid, message_text)
|
|
1170
1263
|
VALUES ('delete', old.rowid, old.message_text);
|
|
1171
1264
|
END;
|
|
1172
1265
|
|
|
1173
1266
|
CREATE TRIGGER IF NOT EXISTS messages_fts_au AFTER UPDATE ON messages BEGIN
|
|
1174
1267
|
INSERT INTO messages_fts(messages_fts, rowid, message_text)
|
|
1175
|
-
|
|
1176
|
-
INSERT INTO messages_fts(rowid, message_text)
|
|
1268
|
+
SELECT 'delete', old.rowid, old.message_text WHERE old.retrieval_class = 'hot';
|
|
1269
|
+
INSERT INTO messages_fts(rowid, message_text)
|
|
1270
|
+
SELECT new.rowid, new.message_text WHERE new.retrieval_class = 'hot';
|
|
1177
1271
|
END;
|
|
1178
|
-
|
|
1179
|
-
db3.exec(`
|
|
1272
|
+
|
|
1180
1273
|
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts_vocab
|
|
1181
1274
|
USING fts5vocab(messages_fts, 'row');
|
|
1182
|
-
`
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1275
|
+
`
|
|
1276
|
+
};
|
|
1277
|
+
function ensureSchema(db3) {
|
|
1278
|
+
db3.exec("BEGIN IMMEDIATE");
|
|
1279
|
+
try {
|
|
1280
|
+
db3.exec(RETRIEVAL_SCHEMA_DDL.tables);
|
|
1281
|
+
db3.exec(RETRIEVAL_SCHEMA_DDL.fts);
|
|
1282
|
+
db3.exec(`
|
|
1283
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS _stem USING fts5(
|
|
1284
|
+
t, tokenize='porter unicode61'
|
|
1285
|
+
);
|
|
1286
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS _stem_vocab
|
|
1287
|
+
USING fts5vocab(_stem, 'row');
|
|
1288
|
+
`);
|
|
1289
|
+
db3.exec(`
|
|
1290
|
+
CREATE TABLE IF NOT EXISTS message_vectors (
|
|
1291
|
+
message_id TEXT PRIMARY KEY REFERENCES messages(message_id) ON DELETE CASCADE,
|
|
1292
|
+
embedding_q8 BLOB NOT NULL,
|
|
1293
|
+
norm REAL NOT NULL,
|
|
1294
|
+
quant_scale REAL NOT NULL,
|
|
1295
|
+
embed_version INTEGER NOT NULL DEFAULT 1
|
|
1296
|
+
);
|
|
1297
|
+
`);
|
|
1298
|
+
const hasEmbedVersion = () => db3.all(`PRAGMA table_info(message_vectors)`).some((c) => c.name === "embed_version");
|
|
1299
|
+
if (!hasEmbedVersion()) {
|
|
1300
|
+
try {
|
|
1301
|
+
db3.exec(`ALTER TABLE message_vectors ADD COLUMN embed_version INTEGER NOT NULL DEFAULT 1`);
|
|
1302
|
+
} catch (e) {
|
|
1303
|
+
if (!hasEmbedVersion())
|
|
1304
|
+
throw e;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
db3.exec(`
|
|
1308
|
+
CREATE TABLE IF NOT EXISTS ingest_watermark (
|
|
1309
|
+
transcript_path TEXT PRIMARY KEY,
|
|
1310
|
+
last_mtime INTEGER NOT NULL,
|
|
1311
|
+
last_size INTEGER NOT NULL,
|
|
1312
|
+
vendor TEXT NOT NULL
|
|
1313
|
+
);
|
|
1314
|
+
CREATE INDEX IF NOT EXISTS idx_watermark_vendor ON ingest_watermark(vendor);
|
|
1315
|
+
`);
|
|
1316
|
+
db3.exec(`
|
|
1317
|
+
INSERT OR IGNORE INTO schema_meta(key, value)
|
|
1318
|
+
VALUES ('${RETRIEVAL_MIGRATION_KEY}', 'complete');
|
|
1319
|
+
`);
|
|
1320
|
+
db3.exec("COMMIT");
|
|
1321
|
+
} catch (e) {
|
|
1201
1322
|
try {
|
|
1202
|
-
db3.exec(
|
|
1203
|
-
} catch
|
|
1204
|
-
if (!hasEmbedVersion())
|
|
1205
|
-
throw e;
|
|
1323
|
+
db3.exec("ROLLBACK");
|
|
1324
|
+
} catch {
|
|
1206
1325
|
}
|
|
1326
|
+
throw e;
|
|
1207
1327
|
}
|
|
1208
|
-
db3.exec(`
|
|
1209
|
-
CREATE TABLE IF NOT EXISTS ingest_watermark (
|
|
1210
|
-
transcript_path TEXT PRIMARY KEY,
|
|
1211
|
-
last_mtime INTEGER NOT NULL,
|
|
1212
|
-
last_size INTEGER NOT NULL,
|
|
1213
|
-
vendor TEXT NOT NULL
|
|
1214
|
-
);
|
|
1215
|
-
CREATE INDEX IF NOT EXISTS idx_watermark_vendor ON ingest_watermark(vendor);
|
|
1216
|
-
`);
|
|
1217
1328
|
}
|
|
1218
1329
|
|
|
1219
1330
|
// src/recall/message-store.ts
|
|
@@ -1236,7 +1347,7 @@ function ensureDir2() {
|
|
|
1236
1347
|
}
|
|
1237
1348
|
}
|
|
1238
1349
|
function insertMessages(messages, opts) {
|
|
1239
|
-
if (messages.length === 0 && !opts?.replaceSessionId)
|
|
1350
|
+
if (messages.length === 0 && !opts?.replaceSessionId && !opts?.provenance)
|
|
1240
1351
|
return;
|
|
1241
1352
|
ensureDir2();
|
|
1242
1353
|
const d = db2();
|
|
@@ -1252,8 +1363,8 @@ function insertMessages(messages, opts) {
|
|
|
1252
1363
|
}
|
|
1253
1364
|
const stmt = d.prepare(
|
|
1254
1365
|
`INSERT OR IGNORE INTO messages
|
|
1255
|
-
(message_id, session_id, message_seq, message_text, project_id, created_at, message_role)
|
|
1256
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
1366
|
+
(message_id, session_id, message_seq, message_text, project_id, created_at, message_role, retrieval_class)
|
|
1367
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1257
1368
|
);
|
|
1258
1369
|
for (const m of messages) {
|
|
1259
1370
|
stmt.run([
|
|
@@ -1263,9 +1374,55 @@ function insertMessages(messages, opts) {
|
|
|
1263
1374
|
m.message_text,
|
|
1264
1375
|
m.project_id,
|
|
1265
1376
|
m.created_at,
|
|
1266
|
-
m.message_role
|
|
1377
|
+
m.message_role,
|
|
1378
|
+
m.retrieval_class ?? "hot"
|
|
1267
1379
|
]);
|
|
1268
1380
|
}
|
|
1381
|
+
if (opts?.provenance) {
|
|
1382
|
+
const p = opts.provenance;
|
|
1383
|
+
d.run(
|
|
1384
|
+
`INSERT INTO session_provenance
|
|
1385
|
+
(session_id, vendor, kind, parent_session_id, agent_depth, agent_meta, transcript_path, updated_at)
|
|
1386
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1387
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
1388
|
+
vendor = excluded.vendor,
|
|
1389
|
+
kind = excluded.kind,
|
|
1390
|
+
parent_session_id = COALESCE(excluded.parent_session_id, session_provenance.parent_session_id),
|
|
1391
|
+
agent_depth = COALESCE(excluded.agent_depth, session_provenance.agent_depth),
|
|
1392
|
+
agent_meta = COALESCE(excluded.agent_meta, session_provenance.agent_meta),
|
|
1393
|
+
transcript_path = COALESCE(excluded.transcript_path, session_provenance.transcript_path),
|
|
1394
|
+
updated_at = excluded.updated_at`,
|
|
1395
|
+
[
|
|
1396
|
+
p.sessionId,
|
|
1397
|
+
p.vendor,
|
|
1398
|
+
p.kind,
|
|
1399
|
+
p.parentSessionId,
|
|
1400
|
+
p.agentDepth,
|
|
1401
|
+
p.agentMeta ? JSON.stringify(p.agentMeta) : null,
|
|
1402
|
+
p.transcriptPath,
|
|
1403
|
+
Date.now()
|
|
1404
|
+
]
|
|
1405
|
+
);
|
|
1406
|
+
if (p.kind === "agent") {
|
|
1407
|
+
d.run(
|
|
1408
|
+
`UPDATE messages SET retrieval_class = 'agent'
|
|
1409
|
+
WHERE session_id = ? AND retrieval_class != 'agent'`,
|
|
1410
|
+
[p.sessionId]
|
|
1411
|
+
);
|
|
1412
|
+
d.run(
|
|
1413
|
+
`DELETE FROM message_vectors WHERE message_id IN
|
|
1414
|
+
(SELECT message_id FROM messages WHERE session_id = ?)`,
|
|
1415
|
+
[p.sessionId]
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
for (const a of opts?.aliases ?? []) {
|
|
1420
|
+
d.run(
|
|
1421
|
+
`INSERT OR REPLACE INTO session_aliases (alias_id, session_id, source, created_at)
|
|
1422
|
+
VALUES (?, ?, ?, ?)`,
|
|
1423
|
+
[a.aliasId, a.sessionId, a.source, Date.now()]
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1269
1426
|
d.exec("COMMIT");
|
|
1270
1427
|
} catch (e) {
|
|
1271
1428
|
d.exec("ROLLBACK");
|
|
@@ -1273,6 +1430,260 @@ function insertMessages(messages, opts) {
|
|
|
1273
1430
|
}
|
|
1274
1431
|
}
|
|
1275
1432
|
|
|
1433
|
+
// src/recall/session-classifier.ts
|
|
1434
|
+
init_paths();
|
|
1435
|
+
init_log();
|
|
1436
|
+
|
|
1437
|
+
// src/adapters/codex/codex-jsonl-reader.ts
|
|
1438
|
+
var fs = __toESM(require("fs"));
|
|
1439
|
+
init_log();
|
|
1440
|
+
function parseCodexJsonlFile(filepath) {
|
|
1441
|
+
try {
|
|
1442
|
+
const content = fs.readFileSync(filepath, "utf-8");
|
|
1443
|
+
const lines = content.split("\n");
|
|
1444
|
+
const records = [];
|
|
1445
|
+
for (const line of lines) {
|
|
1446
|
+
const trimmed = line.trim();
|
|
1447
|
+
if (!trimmed)
|
|
1448
|
+
continue;
|
|
1449
|
+
try {
|
|
1450
|
+
const record = JSON.parse(trimmed);
|
|
1451
|
+
records.push(record);
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
log({ level: "warn", source: "codex-jsonl-reader", summary: `Skipping unparseable line: ${err.message}` });
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return records;
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
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 [];
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
function extractCodexSessionMeta(filepath) {
|
|
1463
|
+
let fd = null;
|
|
1464
|
+
try {
|
|
1465
|
+
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)
|
|
1469
|
+
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;
|
|
1473
|
+
const record = JSON.parse(firstLine.trim());
|
|
1474
|
+
if (record.type !== "session_meta")
|
|
1475
|
+
return null;
|
|
1476
|
+
const payload = record.payload;
|
|
1477
|
+
return {
|
|
1478
|
+
id: payload.id,
|
|
1479
|
+
cwd: payload.cwd,
|
|
1480
|
+
cli_version: payload.cli_version,
|
|
1481
|
+
git: payload.git,
|
|
1482
|
+
...payload.source && typeof payload.source === "object" ? { source: payload.source } : {}
|
|
1483
|
+
};
|
|
1484
|
+
} catch {
|
|
1485
|
+
return null;
|
|
1486
|
+
} finally {
|
|
1487
|
+
if (fd !== null) {
|
|
1488
|
+
try {
|
|
1489
|
+
fs.closeSync(fd);
|
|
1490
|
+
} catch {
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
var READ_BUFFER_SIZE = 64 * 1024;
|
|
1496
|
+
var TURN_CONTENT_CAP = 8 * 1024;
|
|
1497
|
+
|
|
1498
|
+
// src/recall/session-classifier.ts
|
|
1499
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1500
|
+
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;
|
|
1501
|
+
function normalize(p) {
|
|
1502
|
+
return p.replace(/\\/g, "/");
|
|
1503
|
+
}
|
|
1504
|
+
function basenameNoExt(p) {
|
|
1505
|
+
return normalize(p).split("/").pop().replace(/\.jsonl$/i, "");
|
|
1506
|
+
}
|
|
1507
|
+
function lookupStoredProvenance(transcriptPath) {
|
|
1508
|
+
try {
|
|
1509
|
+
const row = getDb(dbPath()).get(
|
|
1510
|
+
`SELECT session_id, kind, parent_session_id FROM session_provenance
|
|
1511
|
+
WHERE transcript_path = ? LIMIT 1`,
|
|
1512
|
+
[normalize(transcriptPath)]
|
|
1513
|
+
);
|
|
1514
|
+
return row ?? null;
|
|
1515
|
+
} catch {
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
function claudeSubagentParent(normPath) {
|
|
1520
|
+
const segments = normPath.split("/");
|
|
1521
|
+
const idx = segments.lastIndexOf("subagents");
|
|
1522
|
+
if (idx <= 0)
|
|
1523
|
+
return null;
|
|
1524
|
+
const parent = segments[idx - 1];
|
|
1525
|
+
return parent || null;
|
|
1526
|
+
}
|
|
1527
|
+
function classifySession(args) {
|
|
1528
|
+
const { sessionId, transcriptPath, vendor, hook } = args;
|
|
1529
|
+
const normPath = normalize(transcriptPath);
|
|
1530
|
+
const base = basenameNoExt(transcriptPath);
|
|
1531
|
+
const stored = lookupStoredProvenance(transcriptPath);
|
|
1532
|
+
if (stored && !(stored.kind !== "agent" && hook?.isSubagent)) {
|
|
1533
|
+
return {
|
|
1534
|
+
kind: stored.kind === "agent" ? "agent" : "root",
|
|
1535
|
+
canonicalSessionId: stored.session_id,
|
|
1536
|
+
parentSessionId: stored.parent_session_id,
|
|
1537
|
+
agentDepth: null,
|
|
1538
|
+
agentMeta: null,
|
|
1539
|
+
aliases: aliasCandidates(stored.session_id, sessionId, hook),
|
|
1540
|
+
evidence: "stored"
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
if (vendor === "claude")
|
|
1544
|
+
return classifyClaude(sessionId, normPath, base, hook);
|
|
1545
|
+
return classifyCodex(sessionId, transcriptPath, base, hook);
|
|
1546
|
+
}
|
|
1547
|
+
function aliasCandidates(canonical, proposed, hook) {
|
|
1548
|
+
const out = /* @__PURE__ */ new Set();
|
|
1549
|
+
if (hook?.agentId && hook.agentId !== canonical)
|
|
1550
|
+
out.add(hook.agentId);
|
|
1551
|
+
if (proposed && proposed !== canonical)
|
|
1552
|
+
out.add(proposed);
|
|
1553
|
+
return [...out];
|
|
1554
|
+
}
|
|
1555
|
+
function classifyClaude(sessionId, normPath, base, hook) {
|
|
1556
|
+
if (normPath.includes("/subagents/") && base.startsWith("agent-")) {
|
|
1557
|
+
return {
|
|
1558
|
+
kind: "agent",
|
|
1559
|
+
canonicalSessionId: base,
|
|
1560
|
+
parentSessionId: claudeSubagentParent(normPath) ?? hook?.payloadSessionId ?? null,
|
|
1561
|
+
agentDepth: null,
|
|
1562
|
+
agentMeta: hook?.agentId ? { hookAgentId: hook.agentId } : null,
|
|
1563
|
+
aliases: aliasCandidates(base, sessionId, hook),
|
|
1564
|
+
evidence: "claude-path"
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
if (base.startsWith("agent-") || sessionId.startsWith("agent-")) {
|
|
1568
|
+
const canonical = base.startsWith("agent-") ? base : sessionId;
|
|
1569
|
+
return {
|
|
1570
|
+
kind: "agent",
|
|
1571
|
+
canonicalSessionId: canonical,
|
|
1572
|
+
parentSessionId: hook?.payloadSessionId ?? null,
|
|
1573
|
+
agentDepth: null,
|
|
1574
|
+
agentMeta: hook?.agentId ? { hookAgentId: hook.agentId } : null,
|
|
1575
|
+
aliases: aliasCandidates(canonical, sessionId, hook),
|
|
1576
|
+
evidence: "claude-name"
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
if (hook?.isSubagent) {
|
|
1580
|
+
return {
|
|
1581
|
+
kind: "agent",
|
|
1582
|
+
canonicalSessionId: base || sessionId,
|
|
1583
|
+
parentSessionId: hook.payloadSessionId ?? null,
|
|
1584
|
+
agentDepth: null,
|
|
1585
|
+
agentMeta: hook.agentId ? { hookAgentId: hook.agentId } : null,
|
|
1586
|
+
aliases: aliasCandidates(base || sessionId, sessionId, hook),
|
|
1587
|
+
evidence: "hook"
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
return {
|
|
1591
|
+
kind: "root",
|
|
1592
|
+
canonicalSessionId: sessionId,
|
|
1593
|
+
parentSessionId: null,
|
|
1594
|
+
agentDepth: null,
|
|
1595
|
+
agentMeta: null,
|
|
1596
|
+
aliases: [],
|
|
1597
|
+
evidence: "default-root"
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function classifyCodex(sessionId, transcriptPath, base, hook) {
|
|
1601
|
+
const meta = extractCodexSessionMeta(transcriptPath);
|
|
1602
|
+
const filenameUuid = ROLLOUT_UUID_RE.exec(normalize(transcriptPath))?.[1] ?? null;
|
|
1603
|
+
const canonical = meta?.id && typeof meta.id === "string" && meta.id.length > 0 ? meta.id : filenameUuid ?? (UUID_RE.test(sessionId) ? sessionId : null);
|
|
1604
|
+
const sub = parseSubagentSource(meta?.source, transcriptPath);
|
|
1605
|
+
if (sub.malformed) {
|
|
1606
|
+
log({
|
|
1607
|
+
source: "recall:classify",
|
|
1608
|
+
level: "warn",
|
|
1609
|
+
summary: `codex session_meta.source looks subagent-like but is malformed \u2014 leaving session hot (conservative): ${transcriptPath}`
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
if (sub.isSubagent || hook?.isSubagent) {
|
|
1613
|
+
if (!canonical) {
|
|
1614
|
+
log({
|
|
1615
|
+
source: "recall:classify",
|
|
1616
|
+
level: "warn",
|
|
1617
|
+
summary: `codex subagent transcript has no derivable canonical id \u2014 skipping ingest (conservative): ${transcriptPath}`
|
|
1618
|
+
});
|
|
1619
|
+
return {
|
|
1620
|
+
kind: "agent",
|
|
1621
|
+
canonicalSessionId: base || sessionId,
|
|
1622
|
+
parentSessionId: sub.parentThreadId ?? hook?.payloadSessionId ?? null,
|
|
1623
|
+
agentDepth: sub.depth,
|
|
1624
|
+
agentMeta: sub.meta,
|
|
1625
|
+
aliases: [],
|
|
1626
|
+
evidence: sub.isSubagent ? "codex-meta" : "hook",
|
|
1627
|
+
unresolvable: true
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
return {
|
|
1631
|
+
kind: "agent",
|
|
1632
|
+
canonicalSessionId: canonical,
|
|
1633
|
+
parentSessionId: sub.parentThreadId ?? hook?.payloadSessionId ?? null,
|
|
1634
|
+
agentDepth: sub.depth,
|
|
1635
|
+
agentMeta: mergeMeta(sub.meta, hook?.agentId),
|
|
1636
|
+
aliases: aliasCandidates(canonical, sessionId, hook),
|
|
1637
|
+
evidence: sub.isSubagent ? "codex-meta" : "hook"
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
return {
|
|
1641
|
+
kind: "root",
|
|
1642
|
+
canonicalSessionId: canonical ?? sessionId,
|
|
1643
|
+
parentSessionId: null,
|
|
1644
|
+
agentDepth: null,
|
|
1645
|
+
agentMeta: null,
|
|
1646
|
+
aliases: canonical && canonical !== sessionId ? [sessionId] : [],
|
|
1647
|
+
evidence: "default-root"
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
function mergeMeta(meta, hookAgentId) {
|
|
1651
|
+
if (!hookAgentId)
|
|
1652
|
+
return meta;
|
|
1653
|
+
return { ...meta ?? {}, hookAgentId };
|
|
1654
|
+
}
|
|
1655
|
+
function parseSubagentSource(source, _transcriptPath) {
|
|
1656
|
+
const none = { isSubagent: false, malformed: false, parentThreadId: null, depth: null, meta: null };
|
|
1657
|
+
if (!source || typeof source !== "object")
|
|
1658
|
+
return none;
|
|
1659
|
+
const subRaw = source.subagent;
|
|
1660
|
+
const typeTag = source.type;
|
|
1661
|
+
const looksSubagent = subRaw !== void 0 || typeTag === "subagent";
|
|
1662
|
+
if (!looksSubagent)
|
|
1663
|
+
return none;
|
|
1664
|
+
if (subRaw !== void 0 && (typeof subRaw !== "object" || subRaw === null)) {
|
|
1665
|
+
return { ...none, malformed: true };
|
|
1666
|
+
}
|
|
1667
|
+
const sub = subRaw ?? {};
|
|
1668
|
+
const spawnRaw = sub.thread_spawn ?? source.thread_spawn;
|
|
1669
|
+
const spawn2 = spawnRaw && typeof spawnRaw === "object" ? spawnRaw : {};
|
|
1670
|
+
const parentThreadId = typeof spawn2.parent_thread_id === "string" && spawn2.parent_thread_id ? spawn2.parent_thread_id : null;
|
|
1671
|
+
const depth = typeof spawn2.depth === "number" && Number.isFinite(spawn2.depth) ? spawn2.depth : null;
|
|
1672
|
+
const meta = {};
|
|
1673
|
+
for (const k of ["agent_path", "agent_type", "agent_name", "path", "type", "name"]) {
|
|
1674
|
+
const v = spawn2[k] ?? sub[k];
|
|
1675
|
+
if (typeof v === "string" || typeof v === "number")
|
|
1676
|
+
meta[k] = v;
|
|
1677
|
+
}
|
|
1678
|
+
return {
|
|
1679
|
+
isSubagent: true,
|
|
1680
|
+
malformed: false,
|
|
1681
|
+
parentThreadId,
|
|
1682
|
+
depth,
|
|
1683
|
+
meta: Object.keys(meta).length > 0 ? meta : null
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1276
1687
|
// src/recall/message-ingest.ts
|
|
1277
1688
|
init_paths();
|
|
1278
1689
|
|
|
@@ -1291,13 +1702,13 @@ function normalizePath(p) {
|
|
|
1291
1702
|
init_log();
|
|
1292
1703
|
|
|
1293
1704
|
// src/adapters/claude/jsonl-reader.ts
|
|
1294
|
-
var
|
|
1705
|
+
var fs2 = __toESM(require("fs"));
|
|
1295
1706
|
init_log();
|
|
1296
|
-
var
|
|
1297
|
-
var
|
|
1707
|
+
var READ_BUFFER_SIZE2 = 64 * 1024;
|
|
1708
|
+
var TURN_CONTENT_CAP2 = 8 * 1024;
|
|
1298
1709
|
function parseJsonlFile(filepath) {
|
|
1299
1710
|
try {
|
|
1300
|
-
const content =
|
|
1711
|
+
const content = fs2.readFileSync(filepath, "utf-8");
|
|
1301
1712
|
const lines = content.split("\n");
|
|
1302
1713
|
const entries = [];
|
|
1303
1714
|
for (const line of lines) {
|
|
@@ -1486,37 +1897,6 @@ function adaptClaudeEntries(rawEntries) {
|
|
|
1486
1897
|
return rawEntries.map(adaptClaudeEntry).filter((entry) => entry !== null);
|
|
1487
1898
|
}
|
|
1488
1899
|
|
|
1489
|
-
// src/adapters/codex/codex-jsonl-reader.ts
|
|
1490
|
-
var fs2 = __toESM(require("fs"));
|
|
1491
|
-
init_log();
|
|
1492
|
-
var path = __toESM(require("path"));
|
|
1493
|
-
var os = __toESM(require("os"));
|
|
1494
|
-
var CODEX_SESSIONS_DIR = path.join(os.homedir(), ".codex", "sessions");
|
|
1495
|
-
function parseCodexJsonlFile(filepath) {
|
|
1496
|
-
try {
|
|
1497
|
-
const content = fs2.readFileSync(filepath, "utf-8");
|
|
1498
|
-
const lines = content.split("\n");
|
|
1499
|
-
const records = [];
|
|
1500
|
-
for (const line of lines) {
|
|
1501
|
-
const trimmed = line.trim();
|
|
1502
|
-
if (!trimmed)
|
|
1503
|
-
continue;
|
|
1504
|
-
try {
|
|
1505
|
-
const record = JSON.parse(trimmed);
|
|
1506
|
-
records.push(record);
|
|
1507
|
-
} catch (err) {
|
|
1508
|
-
log({ level: "warn", source: "codex-jsonl-reader", summary: `Skipping unparseable line: ${err.message}` });
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
return records;
|
|
1512
|
-
} catch (error) {
|
|
1513
|
-
log({ level: "error", source: "codex-jsonl-reader", summary: `Failed to read ${filepath}: ${error instanceof Error ? error.message : String(error)}`, data: { filepath, error: String(error) } });
|
|
1514
|
-
return [];
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
var READ_BUFFER_SIZE2 = 64 * 1024;
|
|
1518
|
-
var TURN_CONTENT_CAP2 = 8 * 1024;
|
|
1519
|
-
|
|
1520
1900
|
// src/adapters/codex/codex-jsonl-adapter.ts
|
|
1521
1901
|
function adaptCodexJsonlRecords(records, sessionId) {
|
|
1522
1902
|
const outputIndex = /* @__PURE__ */ new Map();
|
|
@@ -2151,6 +2531,36 @@ function entriesCwd(entries) {
|
|
|
2151
2531
|
return void 0;
|
|
2152
2532
|
}
|
|
2153
2533
|
async function ingestSessionMessages(sessionId, transcriptPath, vendor, options) {
|
|
2534
|
+
const classification = classifySession({
|
|
2535
|
+
sessionId,
|
|
2536
|
+
transcriptPath,
|
|
2537
|
+
vendor,
|
|
2538
|
+
...options?.hook ? { hook: options.hook } : {}
|
|
2539
|
+
});
|
|
2540
|
+
if (classification.unresolvable) {
|
|
2541
|
+
return {
|
|
2542
|
+
sessionId,
|
|
2543
|
+
chunksCreated: 0,
|
|
2544
|
+
skipped: true,
|
|
2545
|
+
error: "subagent transcript has no derivable canonical identity \u2014 skipped (conservative)"
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
const canonicalId = classification.canonicalSessionId;
|
|
2549
|
+
const retrievalClass = classification.kind === "agent" ? "agent" : "hot";
|
|
2550
|
+
const provenance = {
|
|
2551
|
+
sessionId: canonicalId,
|
|
2552
|
+
vendor,
|
|
2553
|
+
kind: classification.kind,
|
|
2554
|
+
parentSessionId: classification.parentSessionId,
|
|
2555
|
+
agentDepth: classification.agentDepth,
|
|
2556
|
+
agentMeta: classification.agentMeta,
|
|
2557
|
+
transcriptPath: transcriptPath.replace(/\\/g, "/")
|
|
2558
|
+
};
|
|
2559
|
+
const aliases = classification.aliases.map((aliasId) => ({
|
|
2560
|
+
aliasId,
|
|
2561
|
+
sessionId: canonicalId,
|
|
2562
|
+
source: options?.hook?.agentId === aliasId ? "hook-agent-id" : "ingest-request-id"
|
|
2563
|
+
}));
|
|
2154
2564
|
let rawEntries;
|
|
2155
2565
|
try {
|
|
2156
2566
|
if (vendor === "claude") {
|
|
@@ -2158,18 +2568,25 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
|
|
|
2158
2568
|
rawEntries = adaptClaudeEntries(raw);
|
|
2159
2569
|
} else {
|
|
2160
2570
|
const envelopes = parseCodexJsonlFile(transcriptPath);
|
|
2161
|
-
rawEntries = adaptCodexJsonlRecords(envelopes,
|
|
2571
|
+
rawEntries = adaptCodexJsonlRecords(envelopes, canonicalId);
|
|
2162
2572
|
}
|
|
2163
2573
|
} catch (err) {
|
|
2164
2574
|
return {
|
|
2165
|
-
sessionId,
|
|
2575
|
+
sessionId: canonicalId,
|
|
2166
2576
|
chunksCreated: 0,
|
|
2167
2577
|
skipped: false,
|
|
2168
|
-
error: `Failed to load session: ${err instanceof Error ? err.message : String(err)}
|
|
2578
|
+
error: `Failed to load session: ${err instanceof Error ? err.message : String(err)}`,
|
|
2579
|
+
retrievalClass
|
|
2169
2580
|
};
|
|
2170
2581
|
}
|
|
2171
2582
|
if (rawEntries.length === 0) {
|
|
2172
|
-
|
|
2583
|
+
if (options?.force) {
|
|
2584
|
+
try {
|
|
2585
|
+
insertMessages([], { replaceSessionId: canonicalId, provenance, aliases });
|
|
2586
|
+
} catch {
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
return { sessionId: canonicalId, chunksCreated: 0, skipped: true, retrievalClass };
|
|
2173
2590
|
}
|
|
2174
2591
|
const rawProjectId = options?.projectId ?? entriesCwd(rawEntries);
|
|
2175
2592
|
const projectId = rawProjectId ? normalizePath(rawProjectId) : null;
|
|
@@ -2186,31 +2603,44 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
|
|
|
2186
2603
|
const createdAt = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
|
|
2187
2604
|
records.push({
|
|
2188
2605
|
message_id: entry.uuid,
|
|
2189
|
-
session_id:
|
|
2606
|
+
session_id: canonicalId,
|
|
2190
2607
|
message_seq: i,
|
|
2191
2608
|
message_text: text,
|
|
2192
2609
|
project_id: projectId,
|
|
2193
2610
|
created_at: createdAt,
|
|
2194
|
-
message_role: entry.message?.role ?? entry.type ?? null
|
|
2611
|
+
message_role: entry.message?.role ?? entry.type ?? null,
|
|
2612
|
+
retrieval_class: retrievalClass
|
|
2195
2613
|
});
|
|
2196
2614
|
}
|
|
2197
2615
|
if (records.length === 0) {
|
|
2198
|
-
|
|
2616
|
+
if (options?.force) {
|
|
2617
|
+
try {
|
|
2618
|
+
insertMessages([], { replaceSessionId: canonicalId, provenance, aliases });
|
|
2619
|
+
} catch {
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return { sessionId: canonicalId, chunksCreated: 0, skipped: true, retrievalClass };
|
|
2199
2623
|
}
|
|
2200
2624
|
try {
|
|
2201
|
-
insertMessages(records,
|
|
2625
|
+
insertMessages(records, {
|
|
2626
|
+
...options?.force ? { replaceSessionId: canonicalId } : {},
|
|
2627
|
+
provenance,
|
|
2628
|
+
aliases
|
|
2629
|
+
});
|
|
2202
2630
|
} catch (err) {
|
|
2203
2631
|
return {
|
|
2204
|
-
sessionId,
|
|
2632
|
+
sessionId: canonicalId,
|
|
2205
2633
|
chunksCreated: 0,
|
|
2206
2634
|
skipped: false,
|
|
2207
|
-
error: `DB insert failed: ${err instanceof Error ? err.message : String(err)}
|
|
2635
|
+
error: `DB insert failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2636
|
+
retrievalClass
|
|
2208
2637
|
};
|
|
2209
2638
|
}
|
|
2210
2639
|
return {
|
|
2211
|
-
sessionId,
|
|
2640
|
+
sessionId: canonicalId,
|
|
2212
2641
|
chunksCreated: records.length,
|
|
2213
|
-
skipped: false
|
|
2642
|
+
skipped: false,
|
|
2643
|
+
retrievalClass
|
|
2214
2644
|
};
|
|
2215
2645
|
}
|
|
2216
2646
|
|
|
@@ -2219,9 +2649,35 @@ init_paths();
|
|
|
2219
2649
|
var import_fs = require("fs");
|
|
2220
2650
|
var import_child_process = require("child_process");
|
|
2221
2651
|
var import_path = require("path");
|
|
2222
|
-
function vendorForTranscript(transcriptPath) {
|
|
2652
|
+
function vendorForTranscript(transcriptPath, codexHome = process.env["CODEX_HOME"]) {
|
|
2223
2653
|
const p = transcriptPath.replace(/\\/g, "/");
|
|
2224
|
-
|
|
2654
|
+
const root = codexHome?.replace(/\\/g, "/").replace(/\/$/, "");
|
|
2655
|
+
const underCodexHome = root ? p === root || p.startsWith(`${root}/`) : false;
|
|
2656
|
+
return p.includes("/.codex/") || underCodexHome ? "codex" : "claude";
|
|
2657
|
+
}
|
|
2658
|
+
function resolveIngestTarget(payload) {
|
|
2659
|
+
if (payload.agent_transcript_path) {
|
|
2660
|
+
const transcriptPath = payload.agent_transcript_path;
|
|
2661
|
+
const base = transcriptPath.replace(/\\/g, "/").split("/").pop().replace(/\.jsonl$/i, "");
|
|
2662
|
+
if (!base)
|
|
2663
|
+
return null;
|
|
2664
|
+
return {
|
|
2665
|
+
sessionId: base,
|
|
2666
|
+
transcriptPath,
|
|
2667
|
+
hook: {
|
|
2668
|
+
...payload.session_id ? { payloadSessionId: payload.session_id } : {},
|
|
2669
|
+
...payload.agent_id ? { agentId: payload.agent_id } : {},
|
|
2670
|
+
isSubagent: true
|
|
2671
|
+
}
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
if (!payload.session_id || !payload.transcript_path)
|
|
2675
|
+
return null;
|
|
2676
|
+
return {
|
|
2677
|
+
sessionId: payload.session_id,
|
|
2678
|
+
transcriptPath: payload.transcript_path,
|
|
2679
|
+
hook: { payloadSessionId: payload.session_id, isSubagent: false }
|
|
2680
|
+
};
|
|
2225
2681
|
}
|
|
2226
2682
|
function logStopHook(line) {
|
|
2227
2683
|
try {
|
|
@@ -2240,40 +2696,47 @@ async function runStopHook() {
|
|
|
2240
2696
|
} catch {
|
|
2241
2697
|
process.exit(0);
|
|
2242
2698
|
}
|
|
2243
|
-
if (!payload?.session_id || !payload?.transcript_path)
|
|
2244
|
-
process.exit(0);
|
|
2245
2699
|
if (payload.stop_hook_active)
|
|
2246
2700
|
process.exit(0);
|
|
2701
|
+
const target = resolveIngestTarget(payload);
|
|
2702
|
+
if (!target)
|
|
2703
|
+
process.exit(0);
|
|
2704
|
+
let ingestedClass;
|
|
2705
|
+
let canonicalId = target.sessionId;
|
|
2247
2706
|
try {
|
|
2248
2707
|
getDb(dbPath()).exec("PRAGMA busy_timeout = 5000;");
|
|
2249
|
-
const vendor = vendorForTranscript(
|
|
2708
|
+
const vendor = vendorForTranscript(target.transcriptPath);
|
|
2250
2709
|
const result = await ingestSessionMessages(
|
|
2251
|
-
|
|
2252
|
-
|
|
2710
|
+
target.sessionId,
|
|
2711
|
+
target.transcriptPath,
|
|
2253
2712
|
vendor,
|
|
2254
|
-
{ projectId: payload.cwd ?? void 0 }
|
|
2713
|
+
{ projectId: payload.cwd ?? void 0, hook: target.hook }
|
|
2255
2714
|
);
|
|
2715
|
+
ingestedClass = result?.retrievalClass;
|
|
2716
|
+
canonicalId = result?.sessionId ?? target.sessionId;
|
|
2256
2717
|
if (result?.error) {
|
|
2257
2718
|
logStopHook(
|
|
2258
|
-
`${(/* @__PURE__ */ new Date()).toISOString()} ingest-failed sid=${
|
|
2719
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} ingest-failed sid=${target.sessionId} err=${result.error}
|
|
2259
2720
|
`
|
|
2260
2721
|
);
|
|
2261
2722
|
}
|
|
2262
2723
|
} catch (e) {
|
|
2263
2724
|
logStopHook(
|
|
2264
|
-
`${(/* @__PURE__ */ new Date()).toISOString()} ingest-failed sid=${
|
|
2725
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} ingest-failed sid=${target.sessionId} err=${e.message}
|
|
2265
2726
|
`
|
|
2266
2727
|
);
|
|
2267
2728
|
}
|
|
2268
|
-
(
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2729
|
+
if (!target.hook.isSubagent && ingestedClass !== "agent") {
|
|
2730
|
+
(0, import_child_process.spawn)(
|
|
2731
|
+
process.execPath,
|
|
2732
|
+
[(0, import_path.join)(binDir(), "embed-pending.js"), canonicalId],
|
|
2733
|
+
{
|
|
2734
|
+
detached: true,
|
|
2735
|
+
stdio: "ignore",
|
|
2736
|
+
windowsHide: true
|
|
2737
|
+
}
|
|
2738
|
+
).unref();
|
|
2739
|
+
}
|
|
2277
2740
|
process.exit(0);
|
|
2278
2741
|
}
|
|
2279
2742
|
if (typeof require !== "undefined" && typeof module !== "undefined" && require.main === module) {
|
|
@@ -2281,5 +2744,6 @@ if (typeof require !== "undefined" && typeof module !== "undefined" && require.m
|
|
|
2281
2744
|
}
|
|
2282
2745
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2283
2746
|
0 && (module.exports = {
|
|
2747
|
+
resolveIngestTarget,
|
|
2284
2748
|
vendorForTranscript
|
|
2285
2749
|
});
|