crispy-recall 0.2.0 → 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 -169
- package/THIRD-PARTY-LICENSES +43 -145
- package/dist/SKILL.md.template +7 -0
- package/dist/embed-pending.js +742 -204
- package/dist/recall.js +3029 -941
- package/dist/statusline.js +180 -0
- package/dist/stop-hook.js +611 -144
- package/package.json +3 -2
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);
|
|
@@ -976,13 +977,25 @@ var BindingLoadError = class extends Error {
|
|
|
976
977
|
constructor(dbPath2, cause) {
|
|
977
978
|
super(
|
|
978
979
|
`recall: failed to load the better-sqlite3 native binding while opening ${dbPath2}: ${cause.message}
|
|
979
|
-
The binding is ABI-locked to the Node it was built for
|
|
980
|
+
The native SQLite binding is ABI-locked to the Node it was built for. This is usually one of:
|
|
981
|
+
\u2022 recall is running under a different Node than the one npm installed it with (e.g. Homebrew node vs nvm) \u2014 reinstall with \`npm install -g crispy-recall\` using the node on your PATH, then \`recall install\`.
|
|
982
|
+
\u2022 no prebuilt binary matched your Node version and it could not compile \u2014 use Node 22 LTS or 24+ (Node 23 has no prebuilt SQLite binding), and on macOS make sure Xcode Command Line Tools are installed (\`xcode-select --install\`).
|
|
983
|
+
Run \`recall doctor\` for details.`
|
|
980
984
|
);
|
|
981
985
|
this.dbPath = dbPath2;
|
|
982
986
|
this.cause = cause;
|
|
983
987
|
this.name = "BindingLoadError";
|
|
984
988
|
}
|
|
985
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
|
+
};
|
|
986
999
|
function isBindingLoadError(e) {
|
|
987
1000
|
if (!e || typeof e !== "object")
|
|
988
1001
|
return false;
|
|
@@ -997,7 +1010,7 @@ function isBindingLoadError(e) {
|
|
|
997
1010
|
}
|
|
998
1011
|
var db = null;
|
|
999
1012
|
var currentDbPath = null;
|
|
1000
|
-
function getDb(dbPath2) {
|
|
1013
|
+
function getDb(dbPath2, opts) {
|
|
1001
1014
|
if (db && currentDbPath === dbPath2)
|
|
1002
1015
|
return db;
|
|
1003
1016
|
if (db) {
|
|
@@ -1012,12 +1025,45 @@ function getDb(dbPath2) {
|
|
|
1012
1025
|
raw.close();
|
|
1013
1026
|
throw e;
|
|
1014
1027
|
}
|
|
1015
|
-
|
|
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;
|
|
1016
1040
|
currentDbPath = dbPath2;
|
|
1017
1041
|
ensureSchema(db);
|
|
1018
1042
|
log({ source: "db", level: "info", summary: `DB: initialized at ${dbPath2}` });
|
|
1019
1043
|
return db;
|
|
1020
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
|
+
}
|
|
1021
1067
|
function closeDb() {
|
|
1022
1068
|
if (db) {
|
|
1023
1069
|
db.close();
|
|
@@ -1128,16 +1174,18 @@ function cleanupWasmArtifacts(dbPath2) {
|
|
|
1128
1174
|
} catch {
|
|
1129
1175
|
}
|
|
1130
1176
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1177
|
+
var RETRIEVAL_SCHEMA_DDL = {
|
|
1178
|
+
/** messages + provenance tables + indexes (no FTS objects). */
|
|
1179
|
+
tables: `
|
|
1133
1180
|
CREATE TABLE IF NOT EXISTS messages (
|
|
1134
|
-
message_id
|
|
1135
|
-
session_id
|
|
1136
|
-
message_seq
|
|
1137
|
-
message_text
|
|
1138
|
-
project_id
|
|
1139
|
-
created_at
|
|
1140
|
-
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',
|
|
1141
1189
|
UNIQUE(session_id, message_id)
|
|
1142
1190
|
);
|
|
1143
1191
|
|
|
@@ -1149,68 +1197,134 @@ function ensureSchema(db3) {
|
|
|
1149
1197
|
-- embed drain and every Stop-hook catch-up. This index serves the ORDER BY so
|
|
1150
1198
|
-- the planner walks it and early-terminates at LIMIT instead.
|
|
1151
1199
|
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
+
|
|
1154
1239
|
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
1155
1240
|
message_text,
|
|
1156
|
-
content=
|
|
1241
|
+
content=searchable_messages,
|
|
1157
1242
|
content_rowid=rowid,
|
|
1158
1243
|
tokenize='porter unicode61'
|
|
1159
1244
|
);
|
|
1160
1245
|
|
|
1161
|
-
|
|
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
|
|
1162
1256
|
INSERT INTO messages_fts(rowid, message_text) VALUES (new.rowid, new.message_text);
|
|
1163
1257
|
END;
|
|
1164
1258
|
|
|
1165
|
-
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
|
|
1166
1262
|
INSERT INTO messages_fts(messages_fts, rowid, message_text)
|
|
1167
1263
|
VALUES ('delete', old.rowid, old.message_text);
|
|
1168
1264
|
END;
|
|
1169
1265
|
|
|
1170
1266
|
CREATE TRIGGER IF NOT EXISTS messages_fts_au AFTER UPDATE ON messages BEGIN
|
|
1171
1267
|
INSERT INTO messages_fts(messages_fts, rowid, message_text)
|
|
1172
|
-
|
|
1173
|
-
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';
|
|
1174
1271
|
END;
|
|
1175
|
-
|
|
1176
|
-
db3.exec(`
|
|
1272
|
+
|
|
1177
1273
|
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts_vocab
|
|
1178
1274
|
USING fts5vocab(messages_fts, 'row');
|
|
1179
|
-
`
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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) {
|
|
1198
1322
|
try {
|
|
1199
|
-
db3.exec(
|
|
1200
|
-
} catch
|
|
1201
|
-
if (!hasEmbedVersion())
|
|
1202
|
-
throw e;
|
|
1323
|
+
db3.exec("ROLLBACK");
|
|
1324
|
+
} catch {
|
|
1203
1325
|
}
|
|
1326
|
+
throw e;
|
|
1204
1327
|
}
|
|
1205
|
-
db3.exec(`
|
|
1206
|
-
CREATE TABLE IF NOT EXISTS ingest_watermark (
|
|
1207
|
-
transcript_path TEXT PRIMARY KEY,
|
|
1208
|
-
last_mtime INTEGER NOT NULL,
|
|
1209
|
-
last_size INTEGER NOT NULL,
|
|
1210
|
-
vendor TEXT NOT NULL
|
|
1211
|
-
);
|
|
1212
|
-
CREATE INDEX IF NOT EXISTS idx_watermark_vendor ON ingest_watermark(vendor);
|
|
1213
|
-
`);
|
|
1214
1328
|
}
|
|
1215
1329
|
|
|
1216
1330
|
// src/recall/message-store.ts
|
|
@@ -1233,7 +1347,7 @@ function ensureDir2() {
|
|
|
1233
1347
|
}
|
|
1234
1348
|
}
|
|
1235
1349
|
function insertMessages(messages, opts) {
|
|
1236
|
-
if (messages.length === 0 && !opts?.replaceSessionId)
|
|
1350
|
+
if (messages.length === 0 && !opts?.replaceSessionId && !opts?.provenance)
|
|
1237
1351
|
return;
|
|
1238
1352
|
ensureDir2();
|
|
1239
1353
|
const d = db2();
|
|
@@ -1249,8 +1363,8 @@ function insertMessages(messages, opts) {
|
|
|
1249
1363
|
}
|
|
1250
1364
|
const stmt = d.prepare(
|
|
1251
1365
|
`INSERT OR IGNORE INTO messages
|
|
1252
|
-
(message_id, session_id, message_seq, message_text, project_id, created_at, message_role)
|
|
1253
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
1366
|
+
(message_id, session_id, message_seq, message_text, project_id, created_at, message_role, retrieval_class)
|
|
1367
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1254
1368
|
);
|
|
1255
1369
|
for (const m of messages) {
|
|
1256
1370
|
stmt.run([
|
|
@@ -1260,9 +1374,55 @@ function insertMessages(messages, opts) {
|
|
|
1260
1374
|
m.message_text,
|
|
1261
1375
|
m.project_id,
|
|
1262
1376
|
m.created_at,
|
|
1263
|
-
m.message_role
|
|
1377
|
+
m.message_role,
|
|
1378
|
+
m.retrieval_class ?? "hot"
|
|
1264
1379
|
]);
|
|
1265
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
|
+
}
|
|
1266
1426
|
d.exec("COMMIT");
|
|
1267
1427
|
} catch (e) {
|
|
1268
1428
|
d.exec("ROLLBACK");
|
|
@@ -1270,6 +1430,260 @@ function insertMessages(messages, opts) {
|
|
|
1270
1430
|
}
|
|
1271
1431
|
}
|
|
1272
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
|
+
|
|
1273
1687
|
// src/recall/message-ingest.ts
|
|
1274
1688
|
init_paths();
|
|
1275
1689
|
|
|
@@ -1288,13 +1702,13 @@ function normalizePath(p) {
|
|
|
1288
1702
|
init_log();
|
|
1289
1703
|
|
|
1290
1704
|
// src/adapters/claude/jsonl-reader.ts
|
|
1291
|
-
var
|
|
1705
|
+
var fs2 = __toESM(require("fs"));
|
|
1292
1706
|
init_log();
|
|
1293
|
-
var
|
|
1294
|
-
var
|
|
1707
|
+
var READ_BUFFER_SIZE2 = 64 * 1024;
|
|
1708
|
+
var TURN_CONTENT_CAP2 = 8 * 1024;
|
|
1295
1709
|
function parseJsonlFile(filepath) {
|
|
1296
1710
|
try {
|
|
1297
|
-
const content =
|
|
1711
|
+
const content = fs2.readFileSync(filepath, "utf-8");
|
|
1298
1712
|
const lines = content.split("\n");
|
|
1299
1713
|
const entries = [];
|
|
1300
1714
|
for (const line of lines) {
|
|
@@ -1483,37 +1897,6 @@ function adaptClaudeEntries(rawEntries) {
|
|
|
1483
1897
|
return rawEntries.map(adaptClaudeEntry).filter((entry) => entry !== null);
|
|
1484
1898
|
}
|
|
1485
1899
|
|
|
1486
|
-
// src/adapters/codex/codex-jsonl-reader.ts
|
|
1487
|
-
var fs2 = __toESM(require("fs"));
|
|
1488
|
-
init_log();
|
|
1489
|
-
var path = __toESM(require("path"));
|
|
1490
|
-
var os = __toESM(require("os"));
|
|
1491
|
-
var CODEX_SESSIONS_DIR = path.join(os.homedir(), ".codex", "sessions");
|
|
1492
|
-
function parseCodexJsonlFile(filepath) {
|
|
1493
|
-
try {
|
|
1494
|
-
const content = fs2.readFileSync(filepath, "utf-8");
|
|
1495
|
-
const lines = content.split("\n");
|
|
1496
|
-
const records = [];
|
|
1497
|
-
for (const line of lines) {
|
|
1498
|
-
const trimmed = line.trim();
|
|
1499
|
-
if (!trimmed)
|
|
1500
|
-
continue;
|
|
1501
|
-
try {
|
|
1502
|
-
const record = JSON.parse(trimmed);
|
|
1503
|
-
records.push(record);
|
|
1504
|
-
} catch (err) {
|
|
1505
|
-
log({ level: "warn", source: "codex-jsonl-reader", summary: `Skipping unparseable line: ${err.message}` });
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
return records;
|
|
1509
|
-
} catch (error) {
|
|
1510
|
-
log({ level: "error", source: "codex-jsonl-reader", summary: `Failed to read ${filepath}: ${error instanceof Error ? error.message : String(error)}`, data: { filepath, error: String(error) } });
|
|
1511
|
-
return [];
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
var READ_BUFFER_SIZE2 = 64 * 1024;
|
|
1515
|
-
var TURN_CONTENT_CAP2 = 8 * 1024;
|
|
1516
|
-
|
|
1517
1900
|
// src/adapters/codex/codex-jsonl-adapter.ts
|
|
1518
1901
|
function adaptCodexJsonlRecords(records, sessionId) {
|
|
1519
1902
|
const outputIndex = /* @__PURE__ */ new Map();
|
|
@@ -2148,6 +2531,36 @@ function entriesCwd(entries) {
|
|
|
2148
2531
|
return void 0;
|
|
2149
2532
|
}
|
|
2150
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
|
+
}));
|
|
2151
2564
|
let rawEntries;
|
|
2152
2565
|
try {
|
|
2153
2566
|
if (vendor === "claude") {
|
|
@@ -2155,18 +2568,25 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
|
|
|
2155
2568
|
rawEntries = adaptClaudeEntries(raw);
|
|
2156
2569
|
} else {
|
|
2157
2570
|
const envelopes = parseCodexJsonlFile(transcriptPath);
|
|
2158
|
-
rawEntries = adaptCodexJsonlRecords(envelopes,
|
|
2571
|
+
rawEntries = adaptCodexJsonlRecords(envelopes, canonicalId);
|
|
2159
2572
|
}
|
|
2160
2573
|
} catch (err) {
|
|
2161
2574
|
return {
|
|
2162
|
-
sessionId,
|
|
2575
|
+
sessionId: canonicalId,
|
|
2163
2576
|
chunksCreated: 0,
|
|
2164
2577
|
skipped: false,
|
|
2165
|
-
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
|
|
2166
2580
|
};
|
|
2167
2581
|
}
|
|
2168
2582
|
if (rawEntries.length === 0) {
|
|
2169
|
-
|
|
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 };
|
|
2170
2590
|
}
|
|
2171
2591
|
const rawProjectId = options?.projectId ?? entriesCwd(rawEntries);
|
|
2172
2592
|
const projectId = rawProjectId ? normalizePath(rawProjectId) : null;
|
|
@@ -2183,31 +2603,44 @@ async function ingestSessionMessages(sessionId, transcriptPath, vendor, options)
|
|
|
2183
2603
|
const createdAt = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
|
|
2184
2604
|
records.push({
|
|
2185
2605
|
message_id: entry.uuid,
|
|
2186
|
-
session_id:
|
|
2606
|
+
session_id: canonicalId,
|
|
2187
2607
|
message_seq: i,
|
|
2188
2608
|
message_text: text,
|
|
2189
2609
|
project_id: projectId,
|
|
2190
2610
|
created_at: createdAt,
|
|
2191
|
-
message_role: entry.message?.role ?? entry.type ?? null
|
|
2611
|
+
message_role: entry.message?.role ?? entry.type ?? null,
|
|
2612
|
+
retrieval_class: retrievalClass
|
|
2192
2613
|
});
|
|
2193
2614
|
}
|
|
2194
2615
|
if (records.length === 0) {
|
|
2195
|
-
|
|
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 };
|
|
2196
2623
|
}
|
|
2197
2624
|
try {
|
|
2198
|
-
insertMessages(records,
|
|
2625
|
+
insertMessages(records, {
|
|
2626
|
+
...options?.force ? { replaceSessionId: canonicalId } : {},
|
|
2627
|
+
provenance,
|
|
2628
|
+
aliases
|
|
2629
|
+
});
|
|
2199
2630
|
} catch (err) {
|
|
2200
2631
|
return {
|
|
2201
|
-
sessionId,
|
|
2632
|
+
sessionId: canonicalId,
|
|
2202
2633
|
chunksCreated: 0,
|
|
2203
2634
|
skipped: false,
|
|
2204
|
-
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
|
|
2205
2637
|
};
|
|
2206
2638
|
}
|
|
2207
2639
|
return {
|
|
2208
|
-
sessionId,
|
|
2640
|
+
sessionId: canonicalId,
|
|
2209
2641
|
chunksCreated: records.length,
|
|
2210
|
-
skipped: false
|
|
2642
|
+
skipped: false,
|
|
2643
|
+
retrievalClass
|
|
2211
2644
|
};
|
|
2212
2645
|
}
|
|
2213
2646
|
|
|
@@ -2216,9 +2649,35 @@ init_paths();
|
|
|
2216
2649
|
var import_fs = require("fs");
|
|
2217
2650
|
var import_child_process = require("child_process");
|
|
2218
2651
|
var import_path = require("path");
|
|
2219
|
-
function vendorForTranscript(transcriptPath) {
|
|
2652
|
+
function vendorForTranscript(transcriptPath, codexHome = process.env["CODEX_HOME"]) {
|
|
2220
2653
|
const p = transcriptPath.replace(/\\/g, "/");
|
|
2221
|
-
|
|
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
|
+
};
|
|
2222
2681
|
}
|
|
2223
2682
|
function logStopHook(line) {
|
|
2224
2683
|
try {
|
|
@@ -2237,40 +2696,47 @@ async function runStopHook() {
|
|
|
2237
2696
|
} catch {
|
|
2238
2697
|
process.exit(0);
|
|
2239
2698
|
}
|
|
2240
|
-
if (!payload?.session_id || !payload?.transcript_path)
|
|
2241
|
-
process.exit(0);
|
|
2242
2699
|
if (payload.stop_hook_active)
|
|
2243
2700
|
process.exit(0);
|
|
2701
|
+
const target = resolveIngestTarget(payload);
|
|
2702
|
+
if (!target)
|
|
2703
|
+
process.exit(0);
|
|
2704
|
+
let ingestedClass;
|
|
2705
|
+
let canonicalId = target.sessionId;
|
|
2244
2706
|
try {
|
|
2245
2707
|
getDb(dbPath()).exec("PRAGMA busy_timeout = 5000;");
|
|
2246
|
-
const vendor = vendorForTranscript(
|
|
2708
|
+
const vendor = vendorForTranscript(target.transcriptPath);
|
|
2247
2709
|
const result = await ingestSessionMessages(
|
|
2248
|
-
|
|
2249
|
-
|
|
2710
|
+
target.sessionId,
|
|
2711
|
+
target.transcriptPath,
|
|
2250
2712
|
vendor,
|
|
2251
|
-
{ projectId: payload.cwd ?? void 0 }
|
|
2713
|
+
{ projectId: payload.cwd ?? void 0, hook: target.hook }
|
|
2252
2714
|
);
|
|
2715
|
+
ingestedClass = result?.retrievalClass;
|
|
2716
|
+
canonicalId = result?.sessionId ?? target.sessionId;
|
|
2253
2717
|
if (result?.error) {
|
|
2254
2718
|
logStopHook(
|
|
2255
|
-
`${(/* @__PURE__ */ new Date()).toISOString()} ingest-failed sid=${
|
|
2719
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} ingest-failed sid=${target.sessionId} err=${result.error}
|
|
2256
2720
|
`
|
|
2257
2721
|
);
|
|
2258
2722
|
}
|
|
2259
2723
|
} catch (e) {
|
|
2260
2724
|
logStopHook(
|
|
2261
|
-
`${(/* @__PURE__ */ new Date()).toISOString()} ingest-failed sid=${
|
|
2725
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} ingest-failed sid=${target.sessionId} err=${e.message}
|
|
2262
2726
|
`
|
|
2263
2727
|
);
|
|
2264
2728
|
}
|
|
2265
|
-
(
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
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
|
+
}
|
|
2274
2740
|
process.exit(0);
|
|
2275
2741
|
}
|
|
2276
2742
|
if (typeof require !== "undefined" && typeof module !== "undefined" && require.main === module) {
|
|
@@ -2278,5 +2744,6 @@ if (typeof require !== "undefined" && typeof module !== "undefined" && require.m
|
|
|
2278
2744
|
}
|
|
2279
2745
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2280
2746
|
0 && (module.exports = {
|
|
2747
|
+
resolveIngestTarget,
|
|
2281
2748
|
vendorForTranscript
|
|
2282
2749
|
});
|