agentel 0.2.6 → 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 +260 -79
- package/docs/code-reference.md +130 -42
- package/docs/history-source-handling.md +685 -153
- package/docs/release.md +35 -8
- package/npm-shrinkwrap.json +478 -0
- package/package.json +20 -4
- package/scripts/postinstall.js +156 -0
- package/src/archive.js +1342 -50
- package/src/canonical-events.js +346 -35
- package/src/cli.js +8835 -843
- package/src/collector.js +42 -4
- package/src/config.js +26 -4
- package/src/diffs.js +156 -0
- package/src/doctor.js +48 -5
- package/src/importers/claude.js +51 -4
- package/src/importers/copilot.js +385 -0
- package/src/importers/cursor-recovery.js +22 -0
- package/src/importers/factory.js +396 -0
- package/src/importers/gemini.js +41 -1
- package/src/importers/grok.js +367 -0
- package/src/importers/pi.js +422 -0
- package/src/importers/providers.js +64 -5
- package/src/importers.js +6429 -747
- package/src/mcp.js +1 -0
- package/src/memory-sources.js +671 -0
- package/src/memory-store.js +0 -0
- package/src/parser-versions.js +13 -0
- package/src/pricing.js +84 -0
- package/src/search.js +641 -215
- package/src/session-store.js +405 -0
- package/src/source-watch.js +293 -0
- package/src/sources.js +60 -11
- package/src/supervisor.js +197 -9
- package/src/sync.js +6 -0
- package/src/unavailable-sources.js +358 -0
- package/src/web-export-instructions.js +6 -4
package/src/search.js
CHANGED
|
@@ -4,21 +4,32 @@ const fs = require("fs");
|
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const { spawnSync } = require("child_process");
|
|
7
|
-
const { ensureConversationMarkdown, listSessions, readEvents, readTranscript, sessionHistoryTime } = require("./archive");
|
|
7
|
+
const { ensureConversationMarkdown, listSessions, listSessionsSnapshot, readEvents, readTranscript, sessionHistoryTime } = require("./archive");
|
|
8
8
|
const { EVENT_KINDS, renderEventText } = require("./canonical-events");
|
|
9
9
|
const { loadConfig } = require("./config");
|
|
10
10
|
const { paths, ensureDir, readJson, writeJson } = require("./paths");
|
|
11
11
|
const { canonicalRepo } = require("./repo");
|
|
12
12
|
|
|
13
|
-
const INDEX_VERSION =
|
|
13
|
+
const INDEX_VERSION = 8;
|
|
14
14
|
const INDEX_STALE_CHECK_TTL_MS = 5000;
|
|
15
|
-
const
|
|
16
|
-
const SQLITE_BUILD_BATCH_SIZE = 100;
|
|
15
|
+
const SQLITE_BUILD_BATCH_SIZE = 500;
|
|
17
16
|
const RIPGREP_SEARCH_TIMEOUT_MS = 8000;
|
|
18
17
|
const RIPGREP_BATCH_FILE_COUNT = 200;
|
|
19
18
|
const MARKDOWN_MATCHES_PER_FILE = 3;
|
|
20
19
|
const FTS_SEARCH_BATCH_SIZE = 250;
|
|
21
20
|
const FTS_MAX_SCAN_ROWS = 5000;
|
|
21
|
+
// Most queries find `limit` distinct sessions within the first few hundred
|
|
22
|
+
// ranked rows — broader terms used to hit FTS_MAX_SCAN_ROWS unconditionally,
|
|
23
|
+
// which forced FTS5 to read ~5000 rows from the 469MB index even when the
|
|
24
|
+
// answer was in the first 200. Try a cheap primary scan first and only
|
|
25
|
+
// re-query at the wide cap if dedupe leaves fewer than `limit` sessions.
|
|
26
|
+
const FTS_PRIMARY_SCAN_MULTIPLIER = 100;
|
|
27
|
+
// Tool *output* (Bash stdout, file dumps, test logs) is high-volume and
|
|
28
|
+
// low-signal: the useful part (error strings, the head of a diff, the start
|
|
29
|
+
// of a stack trace) is front-loaded. We still index tool.completed events so
|
|
30
|
+
// those head matches are findable, but cap the indexed/excerpt text so a
|
|
31
|
+
// 50k-line log doesn't add dozens of chunk docs and inflate every BM25 scan.
|
|
32
|
+
const TOOL_COMPLETED_INDEX_MAX_CHARS = 600;
|
|
22
33
|
const _indexCache = {
|
|
23
34
|
path: "",
|
|
24
35
|
mtimeMs: 0,
|
|
@@ -33,20 +44,83 @@ const _ftsCache = {
|
|
|
33
44
|
checkedAtMs: 0,
|
|
34
45
|
available: false
|
|
35
46
|
};
|
|
47
|
+
// Read-only better-sqlite3 connection cache for the FTS sidecar. Reopened when
|
|
48
|
+
// the file inode changes (atomic rename during rebuild). Prepared statements
|
|
49
|
+
// are cached on the connection by SQL text.
|
|
50
|
+
const _ftsReadConn = {
|
|
51
|
+
path: "",
|
|
52
|
+
mtimeMs: 0,
|
|
53
|
+
size: 0,
|
|
54
|
+
ino: 0,
|
|
55
|
+
db: null,
|
|
56
|
+
prepared: null
|
|
57
|
+
};
|
|
58
|
+
let _betterSqlite3 = null;
|
|
59
|
+
let _betterSqlite3Loaded = false;
|
|
60
|
+
let _betterSqlite3LoadError = null;
|
|
61
|
+
// Latched true once `new Database()` throws at runtime (e.g. ABI mismatch
|
|
62
|
+
// after Node version change). Once latched, loadBetterSqlite3 returns null
|
|
63
|
+
// permanently so callers take the spawn fallback instead of repeatedly
|
|
64
|
+
// crashing in the constructor.
|
|
65
|
+
let _betterSqlite3Broken = false;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Lazily load better-sqlite3. Returns the constructor or null when the native
|
|
69
|
+
* binding is unavailable; call sites then fall back to the legacy `sqlite3`
|
|
70
|
+
* subprocess path. The load attempt is cached so missing optional builds do
|
|
71
|
+
* not re-try `require()` on every query.
|
|
72
|
+
*
|
|
73
|
+
* Two failure modes are handled here:
|
|
74
|
+
* 1. `require("better-sqlite3")` itself throws (no native binding shipped).
|
|
75
|
+
* 2. The require succeeds but `new Database()` later throws because the
|
|
76
|
+
* native .node file was built against a different Node ABI (e.g. nvm
|
|
77
|
+
* flipped versions). The lower-level openFts*Db helpers call
|
|
78
|
+
* `markBetterSqlite3Broken()` when that happens, after which we
|
|
79
|
+
* permanently return null here so callers consistently take the spawn
|
|
80
|
+
* fallback instead of repeatedly hitting the same constructor crash.
|
|
81
|
+
*/
|
|
82
|
+
function loadBetterSqlite3() {
|
|
83
|
+
if (_betterSqlite3Broken) return null;
|
|
84
|
+
if (_betterSqlite3Loaded) return _betterSqlite3;
|
|
85
|
+
_betterSqlite3Loaded = true;
|
|
86
|
+
try {
|
|
87
|
+
_betterSqlite3 = require("better-sqlite3");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
_betterSqlite3 = null;
|
|
90
|
+
_betterSqlite3LoadError = error;
|
|
91
|
+
}
|
|
92
|
+
return _betterSqlite3;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function betterSqlite3LoadError() {
|
|
96
|
+
return _betterSqlite3LoadError;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function markBetterSqlite3Broken(error) {
|
|
100
|
+
_betterSqlite3Broken = true;
|
|
101
|
+
if (error && !_betterSqlite3LoadError) _betterSqlite3LoadError = error;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resetBetterSqlite3State() {
|
|
105
|
+
// Test-only: clear the load+broken latches so a test can simulate fresh
|
|
106
|
+
// module load with different `require` behavior. Not part of any public
|
|
107
|
+
// API contract.
|
|
108
|
+
_betterSqlite3 = null;
|
|
109
|
+
_betterSqlite3Loaded = false;
|
|
110
|
+
_betterSqlite3LoadError = null;
|
|
111
|
+
_betterSqlite3Broken = false;
|
|
112
|
+
}
|
|
36
113
|
|
|
37
114
|
function buildIndex(env = process.env) {
|
|
38
|
-
const
|
|
115
|
+
const snapshot = listSessionsSnapshot(env);
|
|
116
|
+
const sessions = snapshot.sessions;
|
|
39
117
|
const docs = [];
|
|
40
118
|
const postings = Object.create(null);
|
|
41
119
|
const df = Object.create(null);
|
|
42
120
|
let totalLength = 0;
|
|
43
121
|
|
|
44
122
|
for (const session of sessions) {
|
|
45
|
-
|
|
46
|
-
const events = readEvents(session);
|
|
47
|
-
const eventDocs = events.length ? docsForEvents(session, events) : [];
|
|
48
|
-
if (!eventDocs.length) ensureConversationMarkdown(session, env);
|
|
49
|
-
const sourceDocs = eventDocs.length ? eventDocs : docsForTranscript(session, readTranscript(session.transcriptPath));
|
|
123
|
+
const sourceDocs = canonicalDocsForSession(session);
|
|
50
124
|
for (const sourceDoc of sourceDocs) {
|
|
51
125
|
const indexText = normalizeIndexText(sourceDoc.text);
|
|
52
126
|
if (!indexText) continue;
|
|
@@ -76,6 +150,7 @@ function buildIndex(env = process.env) {
|
|
|
76
150
|
const index = {
|
|
77
151
|
version: INDEX_VERSION,
|
|
78
152
|
builtAt: new Date().toISOString(),
|
|
153
|
+
sessionFingerprint: snapshot.fingerprint,
|
|
79
154
|
docCount: docs.length,
|
|
80
155
|
avgDocLength: docs.length ? totalLength / docs.length : 0,
|
|
81
156
|
df,
|
|
@@ -104,6 +179,7 @@ function summarizeIndex(index) {
|
|
|
104
179
|
return {
|
|
105
180
|
version: index.version,
|
|
106
181
|
builtAt: index.builtAt,
|
|
182
|
+
sessionFingerprint: index.sessionFingerprint,
|
|
107
183
|
docCount: index.docCount,
|
|
108
184
|
avgDocLength: index.avgDocLength,
|
|
109
185
|
summaryOnly: true
|
|
@@ -120,6 +196,7 @@ function readIndexSummary(indexPath) {
|
|
|
120
196
|
const summary = {
|
|
121
197
|
version: readJsonHeaderNumber(header, "version"),
|
|
122
198
|
builtAt: readJsonHeaderString(header, "builtAt"),
|
|
199
|
+
sessionFingerprint: readJsonHeaderString(header, "sessionFingerprint"),
|
|
123
200
|
docCount: readJsonHeaderNumber(header, "docCount"),
|
|
124
201
|
avgDocLength: readJsonHeaderNumber(header, "avgDocLength"),
|
|
125
202
|
summaryOnly: true
|
|
@@ -147,59 +224,69 @@ function escapeRegExp(value) {
|
|
|
147
224
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
148
225
|
}
|
|
149
226
|
|
|
227
|
+
const FTS_SCHEMA_SQL = [
|
|
228
|
+
"CREATE TABLE meta(key TEXT PRIMARY KEY, value TEXT NOT NULL);",
|
|
229
|
+
"CREATE TABLE docs(",
|
|
230
|
+
" rowid INTEGER PRIMARY KEY,",
|
|
231
|
+
" doc_id TEXT,",
|
|
232
|
+
" session_id TEXT,",
|
|
233
|
+
" provider TEXT,",
|
|
234
|
+
" source_type TEXT,",
|
|
235
|
+
" conversation_kind TEXT,",
|
|
236
|
+
" is_subagent INTEGER,",
|
|
237
|
+
" repo_canonical TEXT,",
|
|
238
|
+
" repo_display TEXT,",
|
|
239
|
+
" scope_canonical TEXT,",
|
|
240
|
+
" storage_scope TEXT,",
|
|
241
|
+
" cwd TEXT,",
|
|
242
|
+
" title TEXT,",
|
|
243
|
+
" started_at TEXT,",
|
|
244
|
+
" occurred_at TEXT,",
|
|
245
|
+
" role TEXT,",
|
|
246
|
+
" event_id TEXT,",
|
|
247
|
+
" event_kind TEXT,",
|
|
248
|
+
" message_index INTEGER,",
|
|
249
|
+
" path TEXT,",
|
|
250
|
+
" matched_text TEXT",
|
|
251
|
+
");",
|
|
252
|
+
"CREATE VIRTUAL TABLE docs_fts USING fts5(text, tokenize='unicode61', prefix='2 3 4');"
|
|
253
|
+
].join("\n");
|
|
254
|
+
|
|
255
|
+
function cleanupFtsTmpFiles(tmpPath) {
|
|
256
|
+
for (const file of [tmpPath, `${tmpPath}-journal`, `${tmpPath}-wal`, `${tmpPath}-shm`]) {
|
|
257
|
+
try {
|
|
258
|
+
fs.rmSync(file, { force: true });
|
|
259
|
+
} catch {
|
|
260
|
+
// Best effort cleanup before rebuilding the sidecar index.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
150
265
|
function buildFtsIndex(index, env = process.env) {
|
|
151
266
|
const ftsPath = paths(env).ftsIndex;
|
|
152
267
|
const tmpPath = `${ftsPath}.${process.pid}.tmp`;
|
|
268
|
+
let handle = null;
|
|
153
269
|
try {
|
|
154
270
|
ensureDir(path.dirname(ftsPath));
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
" doc_id TEXT,",
|
|
169
|
-
" session_id TEXT,",
|
|
170
|
-
" provider TEXT,",
|
|
171
|
-
" source_type TEXT,",
|
|
172
|
-
" repo_canonical TEXT,",
|
|
173
|
-
" repo_display TEXT,",
|
|
174
|
-
" scope_canonical TEXT,",
|
|
175
|
-
" cwd TEXT,",
|
|
176
|
-
" title TEXT,",
|
|
177
|
-
" started_at TEXT,",
|
|
178
|
-
" occurred_at TEXT,",
|
|
179
|
-
" role TEXT,",
|
|
180
|
-
" event_id TEXT,",
|
|
181
|
-
" event_kind TEXT,",
|
|
182
|
-
" message_index INTEGER,",
|
|
183
|
-
" path TEXT,",
|
|
184
|
-
" matched_text TEXT",
|
|
185
|
-
");",
|
|
186
|
-
"CREATE VIRTUAL TABLE docs_fts USING fts5(text, tokenize='unicode61', prefix='2 3 4');",
|
|
187
|
-
`INSERT INTO meta(key, value) VALUES ('version', ${sqliteString(String(INDEX_VERSION))});`,
|
|
188
|
-
`INSERT INTO meta(key, value) VALUES ('builtAt', ${sqliteString(index.builtAt || "")});`,
|
|
189
|
-
`INSERT INTO meta(key, value) VALUES ('docCount', ${sqliteString(String(index.docCount || 0))});`
|
|
190
|
-
].join("\n"));
|
|
191
|
-
|
|
192
|
-
insertFtsDocs(tmpPath, index.docs || [], 1);
|
|
193
|
-
runSqliteScript(tmpPath, "INSERT INTO docs_fts(docs_fts) VALUES('optimize');");
|
|
271
|
+
cleanupFtsTmpFiles(tmpPath);
|
|
272
|
+
handle = openFtsBuildDb(tmpPath);
|
|
273
|
+
execFtsBuildSql(handle, FTS_SCHEMA_SQL);
|
|
274
|
+
insertFtsMetaRows(handle, [
|
|
275
|
+
{ key: "version", value: String(INDEX_VERSION) },
|
|
276
|
+
{ key: "builtAt", value: index.builtAt || "" },
|
|
277
|
+
{ key: "sessionFingerprint", value: index.sessionFingerprint || "" },
|
|
278
|
+
{ key: "docCount", value: String(index.docCount || 0) }
|
|
279
|
+
]);
|
|
280
|
+
insertFtsDocs(handle, tmpPath, index.docs || [], 1);
|
|
281
|
+
execFtsBuildSql(handle, "INSERT INTO docs_fts(docs_fts) VALUES('optimize');");
|
|
282
|
+
closeFtsBuildDb(handle);
|
|
283
|
+
handle = null;
|
|
194
284
|
fs.renameSync(tmpPath, ftsPath);
|
|
195
285
|
rememberFtsCache(ftsPath, true);
|
|
196
286
|
return true;
|
|
197
287
|
} catch {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
} catch {
|
|
201
|
-
// Ignore optional FTS cleanup failure.
|
|
202
|
-
}
|
|
288
|
+
if (handle) closeFtsBuildDb(handle);
|
|
289
|
+
cleanupFtsTmpFiles(tmpPath);
|
|
203
290
|
try {
|
|
204
291
|
fs.rmSync(ftsPath, { force: true });
|
|
205
292
|
} catch {
|
|
@@ -211,56 +298,27 @@ function buildFtsIndex(index, env = process.env) {
|
|
|
211
298
|
}
|
|
212
299
|
|
|
213
300
|
function buildFtsIndexSummary(env = process.env) {
|
|
301
|
+
const snapshot = listSessionsSnapshot(env);
|
|
214
302
|
const ftsPath = paths(env).ftsIndex;
|
|
215
303
|
const tmpPath = `${ftsPath}.${process.pid}.tmp`;
|
|
216
304
|
const builtAt = new Date().toISOString();
|
|
217
305
|
let docCount = 0;
|
|
218
306
|
let totalLength = 0;
|
|
307
|
+
let handle = null;
|
|
219
308
|
try {
|
|
220
309
|
ensureDir(path.dirname(ftsPath));
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
"PRAGMA journal_mode=OFF;",
|
|
230
|
-
"PRAGMA synchronous=OFF;",
|
|
231
|
-
"CREATE TABLE meta(key TEXT PRIMARY KEY, value TEXT NOT NULL);",
|
|
232
|
-
"CREATE TABLE docs(",
|
|
233
|
-
" rowid INTEGER PRIMARY KEY,",
|
|
234
|
-
" doc_id TEXT,",
|
|
235
|
-
" session_id TEXT,",
|
|
236
|
-
" provider TEXT,",
|
|
237
|
-
" source_type TEXT,",
|
|
238
|
-
" repo_canonical TEXT,",
|
|
239
|
-
" repo_display TEXT,",
|
|
240
|
-
" scope_canonical TEXT,",
|
|
241
|
-
" cwd TEXT,",
|
|
242
|
-
" title TEXT,",
|
|
243
|
-
" started_at TEXT,",
|
|
244
|
-
" occurred_at TEXT,",
|
|
245
|
-
" role TEXT,",
|
|
246
|
-
" event_id TEXT,",
|
|
247
|
-
" event_kind TEXT,",
|
|
248
|
-
" message_index INTEGER,",
|
|
249
|
-
" path TEXT,",
|
|
250
|
-
" matched_text TEXT",
|
|
251
|
-
");",
|
|
252
|
-
"CREATE VIRTUAL TABLE docs_fts USING fts5(text, tokenize='unicode61', prefix='2 3 4');",
|
|
253
|
-
`INSERT INTO meta(key, value) VALUES ('version', ${sqliteString(String(INDEX_VERSION))});`,
|
|
254
|
-
`INSERT INTO meta(key, value) VALUES ('builtAt', ${sqliteString(builtAt)});`
|
|
255
|
-
].join("\n"));
|
|
310
|
+
cleanupFtsTmpFiles(tmpPath);
|
|
311
|
+
handle = openFtsBuildDb(tmpPath);
|
|
312
|
+
execFtsBuildSql(handle, FTS_SCHEMA_SQL);
|
|
313
|
+
insertFtsMetaRows(handle, [
|
|
314
|
+
{ key: "version", value: String(INDEX_VERSION) },
|
|
315
|
+
{ key: "builtAt", value: builtAt },
|
|
316
|
+
{ key: "sessionFingerprint", value: snapshot.fingerprint || "" }
|
|
317
|
+
]);
|
|
256
318
|
|
|
257
319
|
let batch = [];
|
|
258
|
-
for (const session of
|
|
259
|
-
|
|
260
|
-
const events = readEvents(session);
|
|
261
|
-
const eventDocs = events.length ? docsForEvents(session, events) : [];
|
|
262
|
-
if (!eventDocs.length) ensureConversationMarkdown(session, env);
|
|
263
|
-
const sourceDocs = eventDocs.length ? eventDocs : docsForTranscript(session, readTranscript(session.transcriptPath));
|
|
320
|
+
for (const session of snapshot.sessions) {
|
|
321
|
+
const sourceDocs = canonicalDocsForSession(session);
|
|
264
322
|
for (const sourceDoc of sourceDocs) {
|
|
265
323
|
const indexText = normalizeIndexText(sourceDoc.text);
|
|
266
324
|
if (!indexText) continue;
|
|
@@ -278,25 +336,22 @@ function buildFtsIndexSummary(env = process.env) {
|
|
|
278
336
|
length: tokens.length
|
|
279
337
|
});
|
|
280
338
|
if (batch.length >= SQLITE_BUILD_BATCH_SIZE) {
|
|
281
|
-
insertFtsDocs(tmpPath, batch, docCount - batch.length + 1);
|
|
339
|
+
insertFtsDocs(handle, tmpPath, batch, docCount - batch.length + 1);
|
|
282
340
|
batch = [];
|
|
283
341
|
}
|
|
284
342
|
}
|
|
285
343
|
}
|
|
286
344
|
}
|
|
287
|
-
if (batch.length) insertFtsDocs(tmpPath, batch, docCount - batch.length + 1);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
345
|
+
if (batch.length) insertFtsDocs(handle, tmpPath, batch, docCount - batch.length + 1);
|
|
346
|
+
insertFtsMetaRows(handle, [{ key: "docCount", value: String(docCount) }]);
|
|
347
|
+
execFtsBuildSql(handle, "INSERT INTO docs_fts(docs_fts) VALUES('optimize');");
|
|
348
|
+
closeFtsBuildDb(handle);
|
|
349
|
+
handle = null;
|
|
292
350
|
fs.renameSync(tmpPath, ftsPath);
|
|
293
351
|
rememberFtsCache(ftsPath, true);
|
|
294
352
|
} catch (error) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
} catch {
|
|
298
|
-
// Ignore optional FTS cleanup failure.
|
|
299
|
-
}
|
|
353
|
+
if (handle) closeFtsBuildDb(handle);
|
|
354
|
+
cleanupFtsTmpFiles(tmpPath);
|
|
300
355
|
try {
|
|
301
356
|
fs.rmSync(ftsPath, { force: true });
|
|
302
357
|
} catch {
|
|
@@ -308,68 +363,272 @@ function buildFtsIndexSummary(env = process.env) {
|
|
|
308
363
|
return {
|
|
309
364
|
version: INDEX_VERSION,
|
|
310
365
|
builtAt,
|
|
366
|
+
sessionFingerprint: snapshot.fingerprint,
|
|
311
367
|
docCount,
|
|
312
368
|
avgDocLength: docCount ? totalLength / docCount : 0,
|
|
313
369
|
summaryOnly: true
|
|
314
370
|
};
|
|
315
371
|
}
|
|
316
372
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
373
|
+
const FTS_DOCS_INSERT_SQL =
|
|
374
|
+
"INSERT INTO docs(rowid, doc_id, session_id, provider, source_type, conversation_kind, is_subagent, repo_canonical, repo_display, scope_canonical, storage_scope, cwd, title, started_at, occurred_at, role, event_id, event_kind, message_index, path, matched_text) " +
|
|
375
|
+
"VALUES (@rowid, @doc_id, @session_id, @provider, @source_type, @conversation_kind, @is_subagent, @repo_canonical, @repo_display, @scope_canonical, @storage_scope, @cwd, @title, @started_at, @occurred_at, @role, @event_id, @event_kind, @message_index, @path, @matched_text)";
|
|
376
|
+
const FTS_FTS_INSERT_SQL = "INSERT INTO docs_fts(rowid, text) VALUES (@rowid, @text)";
|
|
377
|
+
|
|
378
|
+
function docInsertParams(doc, rowid) {
|
|
379
|
+
return {
|
|
380
|
+
rowid,
|
|
381
|
+
doc_id: doc.id || "",
|
|
382
|
+
session_id: doc.sessionId || "",
|
|
383
|
+
provider: doc.provider || "",
|
|
384
|
+
source_type: doc.sourceType || "",
|
|
385
|
+
conversation_kind: doc.conversationKind || "",
|
|
386
|
+
// Precompute the subagent flag once at index time using the same JS
|
|
387
|
+
// predicate the rest of the codebase uses. Querying an integer column is
|
|
388
|
+
// ~50x faster than `NOT GLOB '*_subagent'`, which scans every row's
|
|
389
|
+
// string char-by-char because of the leading wildcard.
|
|
390
|
+
is_subagent: isSubagentSession(doc) ? 1 : 0,
|
|
391
|
+
repo_canonical: doc.repoCanonical || "",
|
|
392
|
+
repo_display: doc.repoDisplay || "",
|
|
393
|
+
scope_canonical: doc.scopeCanonical || "",
|
|
394
|
+
storage_scope: doc.storageScope || "",
|
|
395
|
+
cwd: doc.cwd || "",
|
|
396
|
+
title: doc.title || "",
|
|
397
|
+
started_at: doc.startedAt || "",
|
|
398
|
+
occurred_at: doc.occurredAt || "",
|
|
399
|
+
role: doc.role || "",
|
|
400
|
+
event_id: doc.eventId || "",
|
|
401
|
+
event_kind: doc.eventKind || "",
|
|
402
|
+
message_index: Number.isFinite(Number(doc.messageIndex)) ? Number(doc.messageIndex) : null,
|
|
403
|
+
path: doc.path || "",
|
|
404
|
+
matched_text: doc.matchedText || ""
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function insertFtsMetaRows(handle, rows) {
|
|
409
|
+
if (handle.kind === "native") {
|
|
410
|
+
const stmt = handle.db.prepare("INSERT INTO meta(key, value) VALUES (?, ?)");
|
|
411
|
+
const insertMany = handle.db.transaction((items) => {
|
|
412
|
+
for (const row of items) stmt.run(row.key, row.value);
|
|
413
|
+
});
|
|
414
|
+
insertMany(rows);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const sql = rows
|
|
418
|
+
.map((row) => `INSERT INTO meta(key, value) VALUES ('${String(row.key).replace(/'/g, "''")}', '${String(row.value).replace(/'/g, "''")}');`)
|
|
419
|
+
.join("\n");
|
|
420
|
+
legacyRunSqliteScript(handle.dbPath, sql);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function insertFtsDocs(handleOrPath, dbPathHint, docs, rowidStart = 1) {
|
|
424
|
+
// Backwards-compatible call shape: tests/external callers may pass
|
|
425
|
+
// (dbPath, docs, rowidStart). When the first argument looks like a path
|
|
426
|
+
// we open a short-lived build connection just for this batch.
|
|
427
|
+
let handle = handleOrPath && typeof handleOrPath === "object" && handleOrPath.kind ? handleOrPath : null;
|
|
428
|
+
let docsArg = docs;
|
|
429
|
+
let rowidArg = rowidStart;
|
|
430
|
+
let openedHere = false;
|
|
431
|
+
if (!handle) {
|
|
432
|
+
handle = openFtsBuildDb(handleOrPath);
|
|
433
|
+
openedHere = true;
|
|
434
|
+
docsArg = dbPathHint;
|
|
435
|
+
rowidArg = docs ?? 1;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
if (handle.kind === "native") {
|
|
439
|
+
const docStmt = handle.db.prepare(FTS_DOCS_INSERT_SQL);
|
|
440
|
+
const ftsStmt = handle.db.prepare(FTS_FTS_INSERT_SQL);
|
|
441
|
+
const insertMany = handle.db.transaction((items) => {
|
|
442
|
+
for (let offset = 0; offset < items.length; offset++) {
|
|
443
|
+
const rowid = rowidArg + offset;
|
|
444
|
+
const doc = items[offset];
|
|
445
|
+
docStmt.run(docInsertParams(doc, rowid));
|
|
446
|
+
ftsStmt.run({ rowid, text: doc.text || "" });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
// Chunk to avoid pathological transaction sizes; better-sqlite3 handles
|
|
450
|
+
// ~10k inserts/transaction comfortably but we keep batches bounded for
|
|
451
|
+
// memory and progress observability.
|
|
452
|
+
for (let start = 0; start < docsArg.length; start += SQLITE_BUILD_BATCH_SIZE) {
|
|
453
|
+
const slice = docsArg.slice(start, start + SQLITE_BUILD_BATCH_SIZE);
|
|
454
|
+
insertMany(slice);
|
|
455
|
+
rowidArg += slice.length;
|
|
456
|
+
}
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
// Legacy spawn path. Same text-script semantics as before, just without
|
|
460
|
+
// the helper rename.
|
|
461
|
+
for (let start = 0; start < docsArg.length; start += SQLITE_BUILD_BATCH_SIZE) {
|
|
462
|
+
const statements = ["BEGIN;"];
|
|
463
|
+
const batch = docsArg.slice(start, start + SQLITE_BUILD_BATCH_SIZE);
|
|
464
|
+
for (let offset = 0; offset < batch.length; offset++) {
|
|
465
|
+
const rowid = rowidArg + start + offset;
|
|
466
|
+
const doc = batch[offset];
|
|
467
|
+
const sqlString = (value) => `'${String(value == null ? "" : value).replace(/'/g, "''")}'`;
|
|
468
|
+
statements.push(
|
|
469
|
+
"INSERT INTO docs(rowid, doc_id, session_id, provider, source_type, conversation_kind, is_subagent, repo_canonical, repo_display, scope_canonical, storage_scope, cwd, title, started_at, occurred_at, role, event_id, event_kind, message_index, path, matched_text) VALUES (" +
|
|
470
|
+
[
|
|
471
|
+
rowid,
|
|
472
|
+
sqlString(doc.id || ""),
|
|
473
|
+
sqlString(doc.sessionId || ""),
|
|
474
|
+
sqlString(doc.provider || ""),
|
|
475
|
+
sqlString(doc.sourceType || ""),
|
|
476
|
+
sqlString(doc.conversationKind || ""),
|
|
477
|
+
isSubagentSession(doc) ? 1 : 0,
|
|
478
|
+
sqlString(doc.repoCanonical || ""),
|
|
479
|
+
sqlString(doc.repoDisplay || ""),
|
|
480
|
+
sqlString(doc.scopeCanonical || ""),
|
|
481
|
+
sqlString(doc.storageScope || ""),
|
|
482
|
+
sqlString(doc.cwd || ""),
|
|
483
|
+
sqlString(doc.title || ""),
|
|
484
|
+
sqlString(doc.startedAt || ""),
|
|
485
|
+
sqlString(doc.occurredAt || ""),
|
|
486
|
+
sqlString(doc.role || ""),
|
|
487
|
+
sqlString(doc.eventId || ""),
|
|
488
|
+
sqlString(doc.eventKind || ""),
|
|
489
|
+
Number.isFinite(Number(doc.messageIndex)) ? Number(doc.messageIndex) : "NULL",
|
|
490
|
+
sqlString(doc.path || ""),
|
|
491
|
+
sqlString(doc.matchedText || "")
|
|
492
|
+
].join(", ") +
|
|
493
|
+
");"
|
|
494
|
+
);
|
|
495
|
+
statements.push(`INSERT INTO docs_fts(rowid, text) VALUES (${rowid}, '${String(doc.text || "").replace(/'/g, "''")}');`);
|
|
496
|
+
}
|
|
497
|
+
statements.push("COMMIT;");
|
|
498
|
+
legacyRunSqliteScript(handle.dbPath, statements.join("\n"));
|
|
349
499
|
}
|
|
350
|
-
|
|
351
|
-
|
|
500
|
+
} finally {
|
|
501
|
+
if (openedHere) closeFtsBuildDb(handle);
|
|
352
502
|
}
|
|
353
503
|
}
|
|
354
504
|
|
|
355
|
-
|
|
505
|
+
/**
|
|
506
|
+
* Open a fresh write connection for index builds. Build paths run inside
|
|
507
|
+
* spawned child processes (`buildIndexInChild`) so the connection is
|
|
508
|
+
* short-lived and not cached.
|
|
509
|
+
*/
|
|
510
|
+
function openFtsBuildDb(dbPath) {
|
|
511
|
+
const Database = loadBetterSqlite3();
|
|
512
|
+
if (Database) {
|
|
513
|
+
// `new Database()` can throw on ABI mismatch (different Node version
|
|
514
|
+
// than the one better-sqlite3 was compiled for). Without this guard the
|
|
515
|
+
// exception propagates up to buildFtsIndex's catch block, which then
|
|
516
|
+
// deletes the FTS index file — hard-breaking search until next reindex.
|
|
517
|
+
// Falling back to the spawn path preserves search instead.
|
|
518
|
+
try {
|
|
519
|
+
const db = new Database(dbPath);
|
|
520
|
+
db.pragma("journal_mode = OFF");
|
|
521
|
+
db.pragma("synchronous = OFF");
|
|
522
|
+
db.pragma("temp_store = MEMORY");
|
|
523
|
+
db.pragma("locking_mode = EXCLUSIVE");
|
|
524
|
+
return { kind: "native", db };
|
|
525
|
+
} catch (error) {
|
|
526
|
+
markBetterSqlite3Broken(error);
|
|
527
|
+
// Fall through to spawn handle.
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return { kind: "spawn", dbPath };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function closeFtsBuildDb(handle) {
|
|
534
|
+
if (handle?.kind === "native") {
|
|
535
|
+
try { handle.db.close(); } catch { /* ignore */ }
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function execFtsBuildSql(handle, sql) {
|
|
540
|
+
if (handle.kind === "native") {
|
|
541
|
+
handle.db.exec(sql);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
legacyRunSqliteScript(handle.dbPath, sql);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Open or reuse a read-only connection for the FTS sidecar. The connection is
|
|
549
|
+
* invalidated when the on-disk file's inode/size/mtime changes — the index
|
|
550
|
+
* rebuild atomically renames a fresh DB into place, so the inode flips and
|
|
551
|
+
* any stale prepared statements are dropped.
|
|
552
|
+
*/
|
|
553
|
+
function openFtsReadDb(ftsPath, stat) {
|
|
554
|
+
const Database = loadBetterSqlite3();
|
|
555
|
+
if (!Database) return null;
|
|
556
|
+
if (
|
|
557
|
+
_ftsReadConn.db &&
|
|
558
|
+
_ftsReadConn.path === ftsPath &&
|
|
559
|
+
_ftsReadConn.ino === stat.ino &&
|
|
560
|
+
_ftsReadConn.mtimeMs === stat.mtimeMs &&
|
|
561
|
+
_ftsReadConn.size === stat.size
|
|
562
|
+
) {
|
|
563
|
+
return _ftsReadConn;
|
|
564
|
+
}
|
|
565
|
+
closeFtsReadDb();
|
|
566
|
+
let db;
|
|
567
|
+
try {
|
|
568
|
+
db = new Database(ftsPath, { readonly: true, fileMustExist: true });
|
|
569
|
+
db.pragma("query_only = ON");
|
|
570
|
+
} catch (error) {
|
|
571
|
+
// ABI-mismatch crashes look like ordinary throws here. Latch the module
|
|
572
|
+
// as broken so other readers/builders take the spawn path immediately
|
|
573
|
+
// instead of repeatedly hitting the same crash.
|
|
574
|
+
markBetterSqlite3Broken(error);
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
_ftsReadConn.path = ftsPath;
|
|
578
|
+
_ftsReadConn.ino = stat.ino;
|
|
579
|
+
_ftsReadConn.mtimeMs = stat.mtimeMs;
|
|
580
|
+
_ftsReadConn.size = stat.size;
|
|
581
|
+
_ftsReadConn.db = db;
|
|
582
|
+
_ftsReadConn.prepared = new Map();
|
|
583
|
+
return _ftsReadConn;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function closeFtsReadDb() {
|
|
587
|
+
if (_ftsReadConn.db) {
|
|
588
|
+
try { _ftsReadConn.db.close(); } catch { /* ignore */ }
|
|
589
|
+
}
|
|
590
|
+
_ftsReadConn.path = "";
|
|
591
|
+
_ftsReadConn.ino = 0;
|
|
592
|
+
_ftsReadConn.mtimeMs = 0;
|
|
593
|
+
_ftsReadConn.size = 0;
|
|
594
|
+
_ftsReadConn.db = null;
|
|
595
|
+
_ftsReadConn.prepared = null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function prepareFtsStatement(conn, sql) {
|
|
599
|
+
let stmt = conn.prepared.get(sql);
|
|
600
|
+
if (!stmt) {
|
|
601
|
+
stmt = conn.db.prepare(sql);
|
|
602
|
+
conn.prepared.set(sql, stmt);
|
|
603
|
+
}
|
|
604
|
+
return stmt;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Last-resort fallback: spawn the system `sqlite3` binary. Only used when the
|
|
609
|
+
* better-sqlite3 native binding could not be loaded (older Node, missing build
|
|
610
|
+
* toolchain). Pricing this path stays in line with the pre-PR-1 behavior so
|
|
611
|
+
* users without a toolchain are not regressed.
|
|
612
|
+
*/
|
|
613
|
+
function legacyRunSqliteScript(dbPath, script) {
|
|
356
614
|
const result = spawnSync("sqlite3", [dbPath], {
|
|
357
615
|
argv0: "agentlog-sqlite",
|
|
358
616
|
input: script,
|
|
359
617
|
encoding: "utf8",
|
|
360
618
|
maxBuffer: 1024 * 1024 * 20,
|
|
361
|
-
timeout:
|
|
619
|
+
timeout: 5000
|
|
362
620
|
});
|
|
363
621
|
if (result.error) throw result.error;
|
|
364
622
|
if (result.status !== 0) throw new Error(String(result.stderr || result.stdout || "sqlite3 failed").trim());
|
|
365
623
|
}
|
|
366
624
|
|
|
367
|
-
function
|
|
368
|
-
const
|
|
625
|
+
function legacySqliteJson(dbPath, query, params = []) {
|
|
626
|
+
const inlined = inlineSqlParams(query, params);
|
|
627
|
+
const result = spawnSync("sqlite3", [dbPath, "-json", inlined], {
|
|
369
628
|
argv0: "agentlog-sqlite",
|
|
370
629
|
encoding: "utf8",
|
|
371
630
|
maxBuffer: 1024 * 1024 * 20,
|
|
372
|
-
timeout:
|
|
631
|
+
timeout: 5000
|
|
373
632
|
});
|
|
374
633
|
if (result.error || result.status !== 0) return null;
|
|
375
634
|
try {
|
|
@@ -379,8 +638,25 @@ function sqliteJson(dbPath, query) {
|
|
|
379
638
|
}
|
|
380
639
|
}
|
|
381
640
|
|
|
382
|
-
|
|
383
|
-
|
|
641
|
+
/**
|
|
642
|
+
* Substitute `?` placeholders for the legacy spawn fallback. Not used by the
|
|
643
|
+
* better-sqlite3 path — that path passes params directly to prepared
|
|
644
|
+
* statements. Quoting matches the prior `sqliteString` semantics.
|
|
645
|
+
*/
|
|
646
|
+
function inlineSqlParams(query, params) {
|
|
647
|
+
if (!params || !params.length) return query;
|
|
648
|
+
let index = 0;
|
|
649
|
+
return query.replace(/\?/g, () => {
|
|
650
|
+
if (index >= params.length) return "?";
|
|
651
|
+
const value = params[index++];
|
|
652
|
+
if (value == null) return "NULL";
|
|
653
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
654
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function escapeSqlLike(value) {
|
|
659
|
+
return String(value).replace(/[\\%_]/g, (match) => `\\${match}`);
|
|
384
660
|
}
|
|
385
661
|
|
|
386
662
|
function rememberFtsCache(ftsPath, available) {
|
|
@@ -395,6 +671,21 @@ function rememberFtsCache(ftsPath, available) {
|
|
|
395
671
|
_ftsCache.size = stat?.size || 0;
|
|
396
672
|
_ftsCache.checkedAtMs = Date.now();
|
|
397
673
|
_ftsCache.available = Boolean(available && stat);
|
|
674
|
+
if (!available || !stat) closeFtsReadDb();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function readFtsMeta(ftsPath, stat) {
|
|
678
|
+
const conn = openFtsReadDb(ftsPath, stat);
|
|
679
|
+
if (conn) {
|
|
680
|
+
try {
|
|
681
|
+
const stmt = prepareFtsStatement(conn, "SELECT key, value FROM meta WHERE key IN ('version', 'docCount', 'sessionFingerprint')");
|
|
682
|
+
return stmt.all();
|
|
683
|
+
} catch {
|
|
684
|
+
closeFtsReadDb();
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return legacySqliteJson(ftsPath, "SELECT key, value FROM meta WHERE key IN ('version', 'docCount', 'sessionFingerprint');");
|
|
398
689
|
}
|
|
399
690
|
|
|
400
691
|
function ftsIndexAvailable(env = process.env, options = {}) {
|
|
@@ -404,6 +695,7 @@ function ftsIndexAvailable(env = process.env, options = {}) {
|
|
|
404
695
|
stat = fs.statSync(ftsPath);
|
|
405
696
|
} catch (error) {
|
|
406
697
|
if (error.code !== "ENOENT") throw error;
|
|
698
|
+
closeFtsReadDb();
|
|
407
699
|
return false;
|
|
408
700
|
}
|
|
409
701
|
if (
|
|
@@ -415,7 +707,7 @@ function ftsIndexAvailable(env = process.env, options = {}) {
|
|
|
415
707
|
) {
|
|
416
708
|
return true;
|
|
417
709
|
}
|
|
418
|
-
const rows =
|
|
710
|
+
const rows = readFtsMeta(ftsPath, stat);
|
|
419
711
|
if (!rows || !rows.some((row) => row.key === "version" && Number(row.value) === INDEX_VERSION)) {
|
|
420
712
|
rememberFtsCache(ftsPath, false);
|
|
421
713
|
return false;
|
|
@@ -432,13 +724,20 @@ function docsForEvents(session, events) {
|
|
|
432
724
|
const indexedKinds = new Set([
|
|
433
725
|
EVENT_KINDS.PROMPT_SUBMITTED,
|
|
434
726
|
EVENT_KINDS.RESPONSE_GENERATED,
|
|
435
|
-
EVENT_KINDS.TOOL_CALLED
|
|
727
|
+
EVENT_KINDS.TOOL_CALLED,
|
|
728
|
+
EVENT_KINDS.TOOL_COMPLETED
|
|
436
729
|
]);
|
|
437
730
|
return events
|
|
438
731
|
.filter((event) => indexedKinds.has(event.kind))
|
|
439
732
|
.map((event) => {
|
|
440
|
-
|
|
733
|
+
let renderedText = renderEventText(event);
|
|
441
734
|
if (!String(renderedText || "").trim()) return null;
|
|
735
|
+
// Cap tool output to its head only. The first ~600 chars hold the
|
|
736
|
+
// error message / first lines of a diff / start of a stack trace;
|
|
737
|
+
// the tail is verbose machine output with near-zero recall value.
|
|
738
|
+
if (event.kind === EVENT_KINDS.TOOL_COMPLETED && renderedText.length > TOOL_COMPLETED_INDEX_MAX_CHARS) {
|
|
739
|
+
renderedText = `${renderedText.slice(0, TOOL_COMPLETED_INDEX_MAX_CHARS)} [truncated]`;
|
|
740
|
+
}
|
|
442
741
|
return {
|
|
443
742
|
id: event.eventId,
|
|
444
743
|
eventId: event.eventId,
|
|
@@ -447,9 +746,11 @@ function docsForEvents(session, events) {
|
|
|
447
746
|
sessionId: session.sessionId,
|
|
448
747
|
provider: event.provider || session.provider,
|
|
449
748
|
sourceType: event.sourceType || session.sourceType || "",
|
|
749
|
+
conversationKind: session.conversationKind || "",
|
|
450
750
|
repoCanonical: event.repoCanonical || session.repoCanonical || "",
|
|
451
751
|
repoDisplay: displayRepoLabel(session),
|
|
452
752
|
scopeCanonical: event.scopeCanonical || session.scopeCanonical || "",
|
|
753
|
+
storageScope: session.storageScope || "",
|
|
453
754
|
cwd: session.cwd || "",
|
|
454
755
|
title: session.title || event.indexed?.title || "",
|
|
455
756
|
eventTitle: event.indexed?.title || "",
|
|
@@ -465,6 +766,11 @@ function docsForEvents(session, events) {
|
|
|
465
766
|
.filter(Boolean);
|
|
466
767
|
}
|
|
467
768
|
|
|
769
|
+
function canonicalDocsForSession(session) {
|
|
770
|
+
const events = readEvents(session);
|
|
771
|
+
return events.length ? docsForEvents(session, events) : [];
|
|
772
|
+
}
|
|
773
|
+
|
|
468
774
|
function docsForTranscript(session, messages) {
|
|
469
775
|
const docs = [];
|
|
470
776
|
for (const message of messages) {
|
|
@@ -473,9 +779,11 @@ function docsForTranscript(session, messages) {
|
|
|
473
779
|
sessionId: session.sessionId,
|
|
474
780
|
provider: session.provider,
|
|
475
781
|
sourceType: session.sourceType || "",
|
|
782
|
+
conversationKind: session.conversationKind || "",
|
|
476
783
|
repoCanonical: session.repoCanonical || "",
|
|
477
784
|
repoDisplay: displayRepoLabel(session),
|
|
478
785
|
scopeCanonical: session.scopeCanonical || "",
|
|
786
|
+
storageScope: session.storageScope || "",
|
|
479
787
|
cwd: session.cwd || "",
|
|
480
788
|
title: session.title || "",
|
|
481
789
|
startedAt: session.startedAt,
|
|
@@ -539,11 +847,12 @@ function rememberIndexCache(indexPath, index) {
|
|
|
539
847
|
}
|
|
540
848
|
|
|
541
849
|
function searchPastSessions(query, options = {}, env = process.env) {
|
|
850
|
+
const allowMarkdownFallback = Boolean(options.markdownFallback || options.legacyMarkdownFallback || options.allowMarkdownFallback);
|
|
542
851
|
try {
|
|
543
852
|
const eventResults = searchIndexedSessions(query, options, env);
|
|
544
|
-
if (eventResults.length || options.skipMarkdownFallback) return eventResults;
|
|
853
|
+
if (eventResults.length || !allowMarkdownFallback || options.skipMarkdownFallback) return eventResults;
|
|
545
854
|
} catch {
|
|
546
|
-
if (options.skipMarkdownFallback) return [];
|
|
855
|
+
if (!allowMarkdownFallback || options.skipMarkdownFallback) return [];
|
|
547
856
|
// Fall through to the legacy markdown path below.
|
|
548
857
|
}
|
|
549
858
|
return searchMarkdownSessions(query, options, env);
|
|
@@ -553,6 +862,7 @@ function searchMarkdownSessions(query, options = {}, env = process.env) {
|
|
|
553
862
|
const limit = Math.max(1, Math.min(Number(options.limit || 10), 50));
|
|
554
863
|
const maxMatches = Math.max(limit * 8, 40);
|
|
555
864
|
const includeWebChats = Boolean(options.includeWebChats);
|
|
865
|
+
const includeSubagents = Boolean(options.includeSubagents || options.include_subagents);
|
|
556
866
|
const filter = normalizeSessionFilter(options);
|
|
557
867
|
const repo = filter.repo || inferCallingRepo(options.cwd || process.cwd());
|
|
558
868
|
const since = parseSinceFilter(options.since);
|
|
@@ -561,7 +871,7 @@ function searchMarkdownSessions(query, options = {}, env = process.env) {
|
|
|
561
871
|
if (!queryTokens.length && !phrase) return [];
|
|
562
872
|
|
|
563
873
|
const sessions = listSessions(env).filter((session) => {
|
|
564
|
-
if (!matchesSessionFilter(session, { ...filter, includeWebChats, since })) return false;
|
|
874
|
+
if (!matchesSessionFilter(session, { ...filter, includeWebChats, includeSubagents, since })) return false;
|
|
565
875
|
const conversationPath = ensureConversationMarkdown(session, env);
|
|
566
876
|
const searchPath = conversationPath || session.transcriptPath;
|
|
567
877
|
session._searchPath = searchPath;
|
|
@@ -619,22 +929,23 @@ function searchMarkdownSessions(query, options = {}, env = process.env) {
|
|
|
619
929
|
function searchIndexedSessions(query, options = {}, env = process.env) {
|
|
620
930
|
const limit = Math.max(1, Math.min(Number(options.limit || 10), 50));
|
|
621
931
|
const includeWebChats = Boolean(options.includeWebChats);
|
|
932
|
+
const includeSubagents = Boolean(options.includeSubagents || options.include_subagents);
|
|
622
933
|
const filter = normalizeSessionFilter(options);
|
|
623
934
|
const repo = filter.repo || inferCallingRepo(options.cwd || process.cwd());
|
|
624
935
|
const since = parseSinceFilter(options.since);
|
|
625
936
|
const queryTokens = tokenize(query);
|
|
626
937
|
const phrase = String(query || "").trim().toLowerCase();
|
|
627
938
|
if (!queryTokens.length && !phrase) return [];
|
|
628
|
-
const ftsResults = searchFtsSessions(query, queryTokens, { limit, includeWebChats, filter, repo, since, options }, env);
|
|
939
|
+
const ftsResults = searchFtsSessions(query, queryTokens, { limit, includeWebChats, includeSubagents, filter, repo, since, options }, env);
|
|
629
940
|
if (ftsResults) return ftsResults;
|
|
630
|
-
if (options.skipJsonIndex) return [];
|
|
941
|
+
if (options.skipJsonIndex || !(options.legacyJsonIndex || options.allowJsonIndex)) return [];
|
|
631
942
|
const index = loadIndex(env, { noRebuild: Boolean(options.noRebuild) });
|
|
632
943
|
if (!index) return [];
|
|
633
944
|
|
|
634
945
|
const scored = [];
|
|
635
946
|
const candidates = candidateDocsForQuery(index, queryTokens, phrase);
|
|
636
947
|
for (const { doc, docIndex } of candidates) {
|
|
637
|
-
if (!matchesSessionFilter(doc, { ...filter, includeWebChats, since })) continue;
|
|
948
|
+
if (!matchesSessionFilter(doc, { ...filter, includeWebChats, includeSubagents, since })) continue;
|
|
638
949
|
|
|
639
950
|
let score = bm25Score(doc, queryTokens, index, docIndex, candidates.termFrequencies);
|
|
640
951
|
if (phrase && doc.text.toLowerCase().includes(phrase)) score += 2.5;
|
|
@@ -668,34 +979,43 @@ function searchIndexedSessions(query, options = {}, env = process.env) {
|
|
|
668
979
|
}));
|
|
669
980
|
}
|
|
670
981
|
|
|
982
|
+
function dedupeFtsRowsBySession(rows, context) {
|
|
983
|
+
const bySession = new Map();
|
|
984
|
+
for (const row of rows) {
|
|
985
|
+
const doc = ftsRowToDoc(row);
|
|
986
|
+
if (!matchesSessionFilter(doc, { ...context.filter, includeWebChats: context.includeWebChats, includeSubagents: context.includeSubagents, since: context.since })) continue;
|
|
987
|
+
if (!context.options.repo && context.repo && doc.repoCanonical === context.repo) {
|
|
988
|
+
row.rank = Number(row.rank || 0) - 0.05;
|
|
989
|
+
}
|
|
990
|
+
if (!bySession.has(doc.sessionId)) bySession.set(doc.sessionId, { doc, row });
|
|
991
|
+
if (bySession.size >= context.limit) break;
|
|
992
|
+
}
|
|
993
|
+
return bySession;
|
|
994
|
+
}
|
|
995
|
+
|
|
671
996
|
function searchFtsSessions(query, queryTokens, context, env = process.env) {
|
|
672
997
|
const ftsPath = paths(env).ftsIndex;
|
|
673
998
|
if (!ftsIndexAvailable(env, { noStaleCheck: Boolean(context.options.noRebuild || context.options.allowStaleFts) })) return null;
|
|
674
999
|
const matchQuery = ftsMatchQuery(query);
|
|
675
1000
|
if (!matchQuery) return [];
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1001
|
+
// Single bounded prefix in rank order — FTS5 with bm25 + OFFSET is quadratic,
|
|
1002
|
+
// so we pull one shot and dedupe in JS. Two-tier scan: primary is small and
|
|
1003
|
+
// fast (limit*100, typically 1000), wide cap is only used if dedupe couldn't
|
|
1004
|
+
// fill `limit` distinct sessions from the primary.
|
|
1005
|
+
const primaryScanLimit = Math.max(context.limit * FTS_PRIMARY_SCAN_MULTIPLIER, FTS_SEARCH_BATCH_SIZE);
|
|
1006
|
+
const wideScanLimit = Math.max(primaryScanLimit, Math.min(FTS_MAX_SCAN_ROWS, context.limit * 500));
|
|
1007
|
+
|
|
1008
|
+
let rows = ftsSearchRows(ftsPath, matchQuery, { limit: primaryScanLimit, offset: 0, context });
|
|
1009
|
+
if (!rows) return null;
|
|
1010
|
+
let bySession = dedupeFtsRowsBySession(rows, context);
|
|
1011
|
+
if (bySession.size < context.limit && primaryScanLimit < wideScanLimit) {
|
|
1012
|
+
// Under-filled (e.g. broad term where many top-ranked docs collapse onto
|
|
1013
|
+
// the same session, or filters reject most of the primary set). Re-query
|
|
1014
|
+
// at the wide cap. SQLite re-evaluates the MATCH but the surrounding
|
|
1015
|
+
// prepared statement + OS page cache make the second pass cheap.
|
|
1016
|
+
rows = ftsSearchRows(ftsPath, matchQuery, { limit: wideScanLimit, offset: 0, context });
|
|
686
1017
|
if (!rows) return null;
|
|
687
|
-
|
|
688
|
-
offset += rows.length;
|
|
689
|
-
for (const row of rows) {
|
|
690
|
-
const doc = ftsRowToDoc(row);
|
|
691
|
-
if (!matchesSessionFilter(doc, { ...context.filter, includeWebChats: context.includeWebChats, since: context.since })) continue;
|
|
692
|
-
if (!context.options.repo && context.repo && doc.repoCanonical === context.repo) {
|
|
693
|
-
row.rank = Number(row.rank || 0) - 0.05;
|
|
694
|
-
}
|
|
695
|
-
if (!bySession.has(doc.sessionId)) bySession.set(doc.sessionId, { doc, row });
|
|
696
|
-
if (bySession.size >= context.limit) break;
|
|
697
|
-
}
|
|
698
|
-
if (rows.length < batchSize) break;
|
|
1018
|
+
bySession = dedupeFtsRowsBySession(rows, context);
|
|
699
1019
|
}
|
|
700
1020
|
return [...bySession.values()].slice(0, context.limit).map(({ doc, row }) => ({
|
|
701
1021
|
session_id: doc.sessionId,
|
|
@@ -719,31 +1039,75 @@ function searchFtsSessions(query, queryTokens, context, env = process.env) {
|
|
|
719
1039
|
}
|
|
720
1040
|
|
|
721
1041
|
function ftsSearchRows(ftsPath, matchQuery, options) {
|
|
722
|
-
const clauses = [`docs_fts MATCH ${sqliteString(matchQuery)}`];
|
|
723
1042
|
const filter = options.context?.filter || {};
|
|
724
|
-
if (filter.provider) clauses.push(`d.provider = ${sqliteString(filter.provider)}`);
|
|
725
1043
|
const sourceTypes = filter.sourceTypes?.length ? filter.sourceTypes : filter.sourceType ? [filter.sourceType] : [];
|
|
726
|
-
|
|
1044
|
+
const params = [matchQuery];
|
|
1045
|
+
const clauses = ["docs_fts MATCH ?"];
|
|
1046
|
+
if (filter.provider) {
|
|
1047
|
+
clauses.push("d.provider = ?");
|
|
1048
|
+
params.push(filter.provider);
|
|
1049
|
+
}
|
|
1050
|
+
if (sourceTypes.length) {
|
|
1051
|
+
clauses.push(`d.source_type IN (${sourceTypes.map(() => "?").join(", ")})`);
|
|
1052
|
+
for (const value of sourceTypes) params.push(value);
|
|
1053
|
+
}
|
|
1054
|
+
if (!options.context?.includeSubagents) {
|
|
1055
|
+
// Precomputed integer flag (set in docInsertParams). Equivalent to the
|
|
1056
|
+
// old `NOT GLOB '*_subagent'` but a 1-byte compare instead of a
|
|
1057
|
+
// leading-wildcard string scan over every FTS-matched row.
|
|
1058
|
+
clauses.push("(d.is_subagent IS NULL OR d.is_subagent = 0)");
|
|
1059
|
+
}
|
|
727
1060
|
if (options.context?.since) {
|
|
728
|
-
const since =
|
|
729
|
-
clauses.push(
|
|
1061
|
+
const since = options.context.since.toISOString();
|
|
1062
|
+
clauses.push("(d.started_at >= ? OR (d.started_at = '' AND d.occurred_at >= ?))");
|
|
1063
|
+
params.push(since, since);
|
|
1064
|
+
}
|
|
1065
|
+
if (filter.repo) {
|
|
1066
|
+
const repo = String(filter.repo).toLowerCase();
|
|
1067
|
+
const repoLike = `%${escapeSqlLike(repo)}%`;
|
|
1068
|
+
const repoColumns = ["d.repo_canonical", "d.repo_display", "d.scope_canonical", "d.cwd"];
|
|
1069
|
+
clauses.push(
|
|
1070
|
+
"(" +
|
|
1071
|
+
repoColumns.map((column) => `(LOWER(${column}) = ? OR LOWER(${column}) LIKE ? ESCAPE '\\')`).join(" OR ") +
|
|
1072
|
+
")"
|
|
1073
|
+
);
|
|
1074
|
+
for (const column of repoColumns) {
|
|
1075
|
+
void column;
|
|
1076
|
+
params.push(repo, repoLike);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
const limit = Math.max(1, Number(options.limit) || FTS_SEARCH_BATCH_SIZE);
|
|
1080
|
+
const offset = Math.max(0, Number(options.offset) || 0);
|
|
1081
|
+
// LIMIT/OFFSET stay parameterized so a single prepared-statement shape
|
|
1082
|
+
// covers every page; the filter shape varies by query so its SQL is built
|
|
1083
|
+
// per call but the resulting prepared statement is cached on the connection.
|
|
1084
|
+
const sql = [
|
|
1085
|
+
"SELECT",
|
|
1086
|
+
" d.doc_id, d.session_id, d.provider, d.source_type, d.conversation_kind, d.is_subagent, d.repo_canonical, d.repo_display,",
|
|
1087
|
+
" d.scope_canonical, d.storage_scope, d.cwd, d.title, d.started_at, d.occurred_at, d.role,",
|
|
1088
|
+
" d.event_id, d.event_kind, d.message_index, d.path, d.matched_text,",
|
|
1089
|
+
" snippet(docs_fts, 0, '', '', '...', 32) AS excerpt,",
|
|
1090
|
+
" bm25(docs_fts) AS rank",
|
|
1091
|
+
"FROM docs_fts",
|
|
1092
|
+
"JOIN docs d ON d.rowid = docs_fts.rowid",
|
|
1093
|
+
`WHERE ${clauses.join(" AND ")}`,
|
|
1094
|
+
"ORDER BY rank ASC, d.occurred_at DESC, d.started_at DESC",
|
|
1095
|
+
"LIMIT ? OFFSET ?"
|
|
1096
|
+
].join("\n");
|
|
1097
|
+
params.push(limit, offset);
|
|
1098
|
+
|
|
1099
|
+
let stat;
|
|
1100
|
+
try { stat = fs.statSync(ftsPath); } catch { return null; }
|
|
1101
|
+
const conn = openFtsReadDb(ftsPath, stat);
|
|
1102
|
+
if (conn) {
|
|
1103
|
+
try {
|
|
1104
|
+
return prepareFtsStatement(conn, sql).all(...params);
|
|
1105
|
+
} catch {
|
|
1106
|
+
closeFtsReadDb();
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
730
1109
|
}
|
|
731
|
-
return
|
|
732
|
-
ftsPath,
|
|
733
|
-
[
|
|
734
|
-
"SELECT",
|
|
735
|
-
" d.doc_id, d.session_id, d.provider, d.source_type, d.repo_canonical, d.repo_display,",
|
|
736
|
-
" d.scope_canonical, d.cwd, d.title, d.started_at, d.occurred_at, d.role,",
|
|
737
|
-
" d.event_id, d.event_kind, d.message_index, d.path, d.matched_text,",
|
|
738
|
-
" snippet(docs_fts, 0, '', '', '...', 32) AS excerpt,",
|
|
739
|
-
" bm25(docs_fts) AS rank",
|
|
740
|
-
"FROM docs_fts",
|
|
741
|
-
"JOIN docs d ON d.rowid = docs_fts.rowid",
|
|
742
|
-
`WHERE ${clauses.join(" AND ")}`,
|
|
743
|
-
"ORDER BY rank ASC, d.occurred_at DESC, d.started_at DESC",
|
|
744
|
-
`LIMIT ${Math.max(1, Number(options.limit) || FTS_SEARCH_BATCH_SIZE)} OFFSET ${Math.max(0, Number(options.offset) || 0)};`
|
|
745
|
-
].join("\n")
|
|
746
|
-
);
|
|
1110
|
+
return legacySqliteJson(ftsPath, sql, params);
|
|
747
1111
|
}
|
|
748
1112
|
|
|
749
1113
|
function ftsRowToDoc(row) {
|
|
@@ -752,9 +1116,12 @@ function ftsRowToDoc(row) {
|
|
|
752
1116
|
sessionId: row.session_id || "",
|
|
753
1117
|
provider: row.provider || "",
|
|
754
1118
|
sourceType: row.source_type || "",
|
|
1119
|
+
conversationKind: row.conversation_kind || "",
|
|
1120
|
+
isSubagent: row.is_subagent == null ? undefined : Number(row.is_subagent),
|
|
755
1121
|
repoCanonical: row.repo_canonical || "",
|
|
756
1122
|
repoDisplay: row.repo_display || "",
|
|
757
1123
|
scopeCanonical: row.scope_canonical || "",
|
|
1124
|
+
storageScope: row.storage_scope || "",
|
|
758
1125
|
cwd: row.cwd || "",
|
|
759
1126
|
title: row.title || "",
|
|
760
1127
|
startedAt: row.started_at || "",
|
|
@@ -980,11 +1347,16 @@ function inferCallingRepo(cwd) {
|
|
|
980
1347
|
}
|
|
981
1348
|
|
|
982
1349
|
function listHistorySessions(options = {}, env = process.env) {
|
|
1350
|
+
return listHistorySessionsFromSessions(listSessions(env), options);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function listHistorySessionsFromSessions(sessions, options = {}) {
|
|
983
1354
|
const since = parseSinceFilter(options.since);
|
|
984
1355
|
const includeWebChats = options.includeWebChats !== false;
|
|
1356
|
+
const includeSubagents = Boolean(options.includeSubagents || options.include_subagents);
|
|
985
1357
|
const filter = normalizeSessionFilter(options);
|
|
986
|
-
return
|
|
987
|
-
.filter((session) => matchesSessionFilter(session, { ...filter, includeWebChats, since }))
|
|
1358
|
+
return (Array.isArray(sessions) ? sessions : [])
|
|
1359
|
+
.filter((session) => matchesSessionFilter(session, { ...filter, includeWebChats, includeSubagents, since }))
|
|
988
1360
|
.map(historySessionSummary)
|
|
989
1361
|
.sort((a, b) => String(historySessionSortTime(b)).localeCompare(String(historySessionSortTime(a))));
|
|
990
1362
|
}
|
|
@@ -1005,6 +1377,7 @@ function historySessionSummary(session) {
|
|
|
1005
1377
|
chat_display_name: session.chatDisplayName || undefined,
|
|
1006
1378
|
chat_project_path: session.chatProjectPath || undefined,
|
|
1007
1379
|
conversation_kind: session.conversationKind || undefined,
|
|
1380
|
+
parent_composer_id: session.parentComposerId || undefined,
|
|
1008
1381
|
pinned: Boolean(session.pinned) || undefined,
|
|
1009
1382
|
cwd: session.cwd || undefined,
|
|
1010
1383
|
title: session.title || undefined,
|
|
@@ -1017,9 +1390,25 @@ function historySessionSummary(session) {
|
|
|
1017
1390
|
usage: session.usage || undefined,
|
|
1018
1391
|
estimatedUsage: session.estimatedUsage || undefined,
|
|
1019
1392
|
models: session.models || undefined,
|
|
1393
|
+
toolUsage: session.toolUsage || undefined,
|
|
1394
|
+
outputTokenWork: session.outputTokenWork || undefined,
|
|
1395
|
+
outcomes: session.outcomes || undefined,
|
|
1020
1396
|
cursorCommandTypeCounts: session.cursorCommandTypeCounts || undefined,
|
|
1397
|
+
codexTurnContext: session.sessionSummary?.codexTurnContext || undefined,
|
|
1398
|
+
sidecarEffort: session.sessionSummary?.claudeCodeSidecar?.effort || undefined,
|
|
1399
|
+
claudeTasks: session.sessionSummary?.claudeTasks
|
|
1400
|
+
? { count: session.sessionSummary.claudeTasks.count, completed: session.sessionSummary.claudeTasks.completed }
|
|
1401
|
+
: undefined,
|
|
1402
|
+
cursorFileHistory: session.sessionSummary?.cursorFileHistory
|
|
1403
|
+
? { snapshots: session.sessionSummary.cursorFileHistory.snapshots, bytes: session.sessionSummary.cursorFileHistory.bytes }
|
|
1404
|
+
: undefined,
|
|
1405
|
+
artifactCount: Array.isArray(session.artifacts) && session.artifacts.length ? session.artifacts.length : undefined,
|
|
1406
|
+
artifactBytes: Array.isArray(session.artifacts) && session.artifacts.length
|
|
1407
|
+
? session.artifacts.reduce((sum, item) => sum + (Number(item?.bytes) || 0), 0)
|
|
1408
|
+
: undefined,
|
|
1021
1409
|
conversation: session.conversationPath,
|
|
1022
|
-
transcript: session.transcriptPath
|
|
1410
|
+
transcript: session.transcriptPath,
|
|
1411
|
+
events: session.eventPath
|
|
1023
1412
|
};
|
|
1024
1413
|
}
|
|
1025
1414
|
|
|
@@ -1042,6 +1431,7 @@ function displayRepoLabel(session) {
|
|
|
1042
1431
|
function displayScopeLabel(scope) {
|
|
1043
1432
|
return {
|
|
1044
1433
|
"claude-desktop/uncategorized": "Claude Desktop (uncategorized)",
|
|
1434
|
+
"claude-cowork/uncategorized": "Claude Cowork (uncategorized)",
|
|
1045
1435
|
"claude-code-desktop/uncategorized": "Claude Code Desktop (uncategorized)"
|
|
1046
1436
|
}[scope] || scope;
|
|
1047
1437
|
}
|
|
@@ -1077,6 +1467,10 @@ function listRecentSessions(limit = 20, options = {}, env = process.env) {
|
|
|
1077
1467
|
return listHistorySessions(options, env).slice(0, limit);
|
|
1078
1468
|
}
|
|
1079
1469
|
|
|
1470
|
+
function listRecentSessionsFromSessions(sessions, limit = 20, options = {}) {
|
|
1471
|
+
return listHistorySessionsFromSessions(sessions, options).slice(0, limit);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1080
1474
|
function normalizeSessionFilter(options = {}) {
|
|
1081
1475
|
const providerInput = options.provider || options.source || "";
|
|
1082
1476
|
const normalizedProvider = normalizeProviderFilter(providerInput);
|
|
@@ -1097,6 +1491,8 @@ function normalizeProviderFilter(value) {
|
|
|
1097
1491
|
claude_cli: { provider: "claude_code" },
|
|
1098
1492
|
claude_desktop: { provider: "claude_desktop" },
|
|
1099
1493
|
claude_code_desktop: { provider: "claude_desktop", sourceType: "claude-code-desktop-metadata", sourceTypes: ["claude-code-desktop-metadata"] },
|
|
1494
|
+
claude_cowork: { provider: "claude_desktop", sourceType: "claude-workspace-desktop", sourceTypes: ["claude-workspace-desktop"] },
|
|
1495
|
+
claude_cowork_desktop: { provider: "claude_desktop", sourceType: "claude-workspace-desktop", sourceTypes: ["claude-workspace-desktop"] },
|
|
1100
1496
|
claude_workspace: { provider: "claude_desktop", sourceType: "claude-workspace-desktop", sourceTypes: ["claude-workspace-desktop"] },
|
|
1101
1497
|
claude_workspace_desktop: { provider: "claude_desktop", sourceType: "claude-workspace-desktop", sourceTypes: ["claude-workspace-desktop"] },
|
|
1102
1498
|
claude_sdk: { provider: "claude_sdk" },
|
|
@@ -1113,10 +1509,21 @@ function normalizeProviderFilter(value) {
|
|
|
1113
1509
|
aider: { provider: "aider", sourceType: "aider-chat-history", sourceTypes: ["aider-chat-history"] },
|
|
1114
1510
|
devin: { provider: "devin" },
|
|
1115
1511
|
devin_cli: { provider: "devin", sourceType: "devin-cli-history", sourceTypes: ["devin-cli-history"] },
|
|
1512
|
+
devin_desktop: { provider: "devin", sourceType: "devin-desktop-acp-events", sourceTypes: ["devin-desktop-acp-events"] },
|
|
1116
1513
|
gemini: { provider: "gemini_cli" },
|
|
1117
1514
|
gemini_cli: { provider: "gemini_cli" },
|
|
1118
1515
|
windsurf: { provider: "windsurf" },
|
|
1119
1516
|
antigravity: { provider: "antigravity" },
|
|
1517
|
+
antigravity_cli: { provider: "antigravity_cli" },
|
|
1518
|
+
antigravity_ide: { provider: "antigravity_ide" },
|
|
1519
|
+
copilot: { provider: "copilot" },
|
|
1520
|
+
copilot_cli: { provider: "copilot" },
|
|
1521
|
+
factory: { provider: "factory" },
|
|
1522
|
+
factory_droid: { provider: "factory" },
|
|
1523
|
+
droid: { provider: "factory" },
|
|
1524
|
+
grok: { provider: "grok" },
|
|
1525
|
+
grok_build: { provider: "grok" },
|
|
1526
|
+
pi: { provider: "pi" },
|
|
1120
1527
|
chatgpt: { provider: "chatgpt" },
|
|
1121
1528
|
claude_web: { provider: "claude_web" },
|
|
1122
1529
|
claude_ai: { provider: "claude_web" }
|
|
@@ -1128,6 +1535,7 @@ function matchesSessionFilter(session, filter) {
|
|
|
1128
1535
|
if (filter.provider && session.provider !== filter.provider) return false;
|
|
1129
1536
|
const sourceTypes = filter.sourceTypes?.length ? filter.sourceTypes : filter.sourceType ? [filter.sourceType] : [];
|
|
1130
1537
|
if (sourceTypes.length && !sourceTypes.includes(session.sourceType)) return false;
|
|
1538
|
+
if (!filter.includeSubagents && isSubagentSession(session)) return false;
|
|
1131
1539
|
if (!filter.includeWebChats && session.scopeCanonical && session.storageScope !== "local") return false;
|
|
1132
1540
|
if (filter.repo && !matchesRepoFilter(session, filter.repo)) return false;
|
|
1133
1541
|
if (filter.since) {
|
|
@@ -1138,6 +1546,10 @@ function matchesSessionFilter(session, filter) {
|
|
|
1138
1546
|
return true;
|
|
1139
1547
|
}
|
|
1140
1548
|
|
|
1549
|
+
function isSubagentSession(session) {
|
|
1550
|
+
return /_subagent$/.test(String(session?.conversationKind || session?.conversation_kind || ""));
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1141
1553
|
function matchesRepoFilter(session, repoFilter) {
|
|
1142
1554
|
const wanted = String(repoFilter || "").toLowerCase();
|
|
1143
1555
|
if (!wanted) return true;
|
|
@@ -1202,22 +1614,24 @@ function rebuildIndexSummary(env = process.env, options = {}) {
|
|
|
1202
1614
|
}
|
|
1203
1615
|
|
|
1204
1616
|
function indexIsStale(indexPath, env = process.env) {
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1617
|
+
const expected = listSessionsSnapshot(env).fingerprint;
|
|
1618
|
+
const actual = indexSessionFingerprint(indexPath, env);
|
|
1619
|
+
return !actual || actual !== expected;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function indexSessionFingerprint(indexPath, env = process.env) {
|
|
1623
|
+
const ftsPath = paths(env).ftsIndex;
|
|
1624
|
+
if (path.resolve(indexPath) === path.resolve(ftsPath)) {
|
|
1625
|
+
let stat;
|
|
1212
1626
|
try {
|
|
1213
|
-
|
|
1214
|
-
if (fs.statSync(session.metadataPath).mtimeMs > indexStat.mtimeMs) return true;
|
|
1215
|
-
if (session.eventPath && fs.existsSync(session.eventPath) && fs.statSync(session.eventPath).mtimeMs > indexStat.mtimeMs) return true;
|
|
1627
|
+
stat = fs.statSync(ftsPath);
|
|
1216
1628
|
} catch {
|
|
1217
|
-
return
|
|
1629
|
+
return "";
|
|
1218
1630
|
}
|
|
1631
|
+
const rows = readFtsMeta(ftsPath, stat);
|
|
1632
|
+
return rows?.find((row) => row.key === "sessionFingerprint")?.value || "";
|
|
1219
1633
|
}
|
|
1220
|
-
return
|
|
1634
|
+
return readIndexSummary(indexPath)?.sessionFingerprint || "";
|
|
1221
1635
|
}
|
|
1222
1636
|
|
|
1223
1637
|
function parseSinceFilter(value) {
|
|
@@ -1258,11 +1672,23 @@ module.exports = {
|
|
|
1258
1672
|
buildIndexSummary,
|
|
1259
1673
|
chunkText,
|
|
1260
1674
|
listHistorySessions,
|
|
1675
|
+
listHistorySessionsFromSessions,
|
|
1261
1676
|
listRecentSessions,
|
|
1677
|
+
listRecentSessionsFromSessions,
|
|
1262
1678
|
loadIndex,
|
|
1263
1679
|
readIndexSummary,
|
|
1680
|
+
rebuildIndexSummary,
|
|
1264
1681
|
reindexIfNeeded,
|
|
1265
1682
|
sessionHistoryTime,
|
|
1266
1683
|
searchPastSessions,
|
|
1267
|
-
tokenize
|
|
1684
|
+
tokenize,
|
|
1685
|
+
// Test-only handles so we can simulate native-binding failures without
|
|
1686
|
+
// touching the on-disk .node file. Not part of any public contract.
|
|
1687
|
+
_searchInternals: {
|
|
1688
|
+
openFtsBuildDb,
|
|
1689
|
+
openFtsReadDb,
|
|
1690
|
+
loadBetterSqlite3,
|
|
1691
|
+
markBetterSqlite3Broken,
|
|
1692
|
+
resetBetterSqlite3State
|
|
1693
|
+
}
|
|
1268
1694
|
};
|