agentel 0.2.8 → 0.3.1
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 +238 -68
- package/docs/code-reference.md +165 -37
- package/docs/history-source-handling.md +555 -124
- package/docs/release.md +35 -8
- package/npm-shrinkwrap.json +478 -0
- package/package.json +18 -5
- package/scripts/postinstall.js +156 -0
- package/src/archive.js +1176 -65
- package/src/canonical-events.js +346 -35
- package/src/cli.js +7801 -874
- package/src/collector.js +42 -4
- package/src/config.js +51 -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 +39 -0
- 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 +4524 -383
- 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 +256 -70
- package/src/session-store.js +405 -0
- package/src/slack-notify.js +732 -0
- package/src/source-watch.js +293 -0
- package/src/sources.js +60 -11
- package/src/supervisor.js +231 -7
- package/src/sync.js +6 -0
- package/src/unavailable-sources.js +358 -0
package/src/search.js
CHANGED
|
@@ -4,13 +4,13 @@ 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
15
|
const SQLITE_BUILD_BATCH_SIZE = 500;
|
|
16
16
|
const RIPGREP_SEARCH_TIMEOUT_MS = 8000;
|
|
@@ -18,6 +18,18 @@ const RIPGREP_BATCH_FILE_COUNT = 200;
|
|
|
18
18
|
const MARKDOWN_MATCHES_PER_FILE = 3;
|
|
19
19
|
const FTS_SEARCH_BATCH_SIZE = 250;
|
|
20
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;
|
|
21
33
|
const _indexCache = {
|
|
22
34
|
path: "",
|
|
23
35
|
mtimeMs: 0,
|
|
@@ -46,14 +58,29 @@ const _ftsReadConn = {
|
|
|
46
58
|
let _betterSqlite3 = null;
|
|
47
59
|
let _betterSqlite3Loaded = false;
|
|
48
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;
|
|
49
66
|
|
|
50
67
|
/**
|
|
51
68
|
* Lazily load better-sqlite3. Returns the constructor or null when the native
|
|
52
69
|
* binding is unavailable; call sites then fall back to the legacy `sqlite3`
|
|
53
70
|
* subprocess path. The load attempt is cached so missing optional builds do
|
|
54
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.
|
|
55
81
|
*/
|
|
56
82
|
function loadBetterSqlite3() {
|
|
83
|
+
if (_betterSqlite3Broken) return null;
|
|
57
84
|
if (_betterSqlite3Loaded) return _betterSqlite3;
|
|
58
85
|
_betterSqlite3Loaded = true;
|
|
59
86
|
try {
|
|
@@ -69,19 +96,31 @@ function betterSqlite3LoadError() {
|
|
|
69
96
|
return _betterSqlite3LoadError;
|
|
70
97
|
}
|
|
71
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
|
+
}
|
|
113
|
+
|
|
72
114
|
function buildIndex(env = process.env) {
|
|
73
|
-
const
|
|
115
|
+
const snapshot = listSessionsSnapshot(env);
|
|
116
|
+
const sessions = snapshot.sessions;
|
|
74
117
|
const docs = [];
|
|
75
118
|
const postings = Object.create(null);
|
|
76
119
|
const df = Object.create(null);
|
|
77
120
|
let totalLength = 0;
|
|
78
121
|
|
|
79
122
|
for (const session of sessions) {
|
|
80
|
-
|
|
81
|
-
const events = readEvents(session);
|
|
82
|
-
const eventDocs = events.length ? docsForEvents(session, events) : [];
|
|
83
|
-
if (!eventDocs.length) ensureConversationMarkdown(session, env);
|
|
84
|
-
const sourceDocs = eventDocs.length ? eventDocs : docsForTranscript(session, readTranscript(session.transcriptPath));
|
|
123
|
+
const sourceDocs = canonicalDocsForSession(session);
|
|
85
124
|
for (const sourceDoc of sourceDocs) {
|
|
86
125
|
const indexText = normalizeIndexText(sourceDoc.text);
|
|
87
126
|
if (!indexText) continue;
|
|
@@ -111,6 +150,7 @@ function buildIndex(env = process.env) {
|
|
|
111
150
|
const index = {
|
|
112
151
|
version: INDEX_VERSION,
|
|
113
152
|
builtAt: new Date().toISOString(),
|
|
153
|
+
sessionFingerprint: snapshot.fingerprint,
|
|
114
154
|
docCount: docs.length,
|
|
115
155
|
avgDocLength: docs.length ? totalLength / docs.length : 0,
|
|
116
156
|
df,
|
|
@@ -139,6 +179,7 @@ function summarizeIndex(index) {
|
|
|
139
179
|
return {
|
|
140
180
|
version: index.version,
|
|
141
181
|
builtAt: index.builtAt,
|
|
182
|
+
sessionFingerprint: index.sessionFingerprint,
|
|
142
183
|
docCount: index.docCount,
|
|
143
184
|
avgDocLength: index.avgDocLength,
|
|
144
185
|
summaryOnly: true
|
|
@@ -155,6 +196,7 @@ function readIndexSummary(indexPath) {
|
|
|
155
196
|
const summary = {
|
|
156
197
|
version: readJsonHeaderNumber(header, "version"),
|
|
157
198
|
builtAt: readJsonHeaderString(header, "builtAt"),
|
|
199
|
+
sessionFingerprint: readJsonHeaderString(header, "sessionFingerprint"),
|
|
158
200
|
docCount: readJsonHeaderNumber(header, "docCount"),
|
|
159
201
|
avgDocLength: readJsonHeaderNumber(header, "avgDocLength"),
|
|
160
202
|
summaryOnly: true
|
|
@@ -190,9 +232,12 @@ const FTS_SCHEMA_SQL = [
|
|
|
190
232
|
" session_id TEXT,",
|
|
191
233
|
" provider TEXT,",
|
|
192
234
|
" source_type TEXT,",
|
|
235
|
+
" conversation_kind TEXT,",
|
|
236
|
+
" is_subagent INTEGER,",
|
|
193
237
|
" repo_canonical TEXT,",
|
|
194
238
|
" repo_display TEXT,",
|
|
195
239
|
" scope_canonical TEXT,",
|
|
240
|
+
" storage_scope TEXT,",
|
|
196
241
|
" cwd TEXT,",
|
|
197
242
|
" title TEXT,",
|
|
198
243
|
" started_at TEXT,",
|
|
@@ -229,6 +274,7 @@ function buildFtsIndex(index, env = process.env) {
|
|
|
229
274
|
insertFtsMetaRows(handle, [
|
|
230
275
|
{ key: "version", value: String(INDEX_VERSION) },
|
|
231
276
|
{ key: "builtAt", value: index.builtAt || "" },
|
|
277
|
+
{ key: "sessionFingerprint", value: index.sessionFingerprint || "" },
|
|
232
278
|
{ key: "docCount", value: String(index.docCount || 0) }
|
|
233
279
|
]);
|
|
234
280
|
insertFtsDocs(handle, tmpPath, index.docs || [], 1);
|
|
@@ -252,6 +298,7 @@ function buildFtsIndex(index, env = process.env) {
|
|
|
252
298
|
}
|
|
253
299
|
|
|
254
300
|
function buildFtsIndexSummary(env = process.env) {
|
|
301
|
+
const snapshot = listSessionsSnapshot(env);
|
|
255
302
|
const ftsPath = paths(env).ftsIndex;
|
|
256
303
|
const tmpPath = `${ftsPath}.${process.pid}.tmp`;
|
|
257
304
|
const builtAt = new Date().toISOString();
|
|
@@ -265,16 +312,13 @@ function buildFtsIndexSummary(env = process.env) {
|
|
|
265
312
|
execFtsBuildSql(handle, FTS_SCHEMA_SQL);
|
|
266
313
|
insertFtsMetaRows(handle, [
|
|
267
314
|
{ key: "version", value: String(INDEX_VERSION) },
|
|
268
|
-
{ key: "builtAt", value: builtAt }
|
|
315
|
+
{ key: "builtAt", value: builtAt },
|
|
316
|
+
{ key: "sessionFingerprint", value: snapshot.fingerprint || "" }
|
|
269
317
|
]);
|
|
270
318
|
|
|
271
319
|
let batch = [];
|
|
272
|
-
for (const session of
|
|
273
|
-
|
|
274
|
-
const events = readEvents(session);
|
|
275
|
-
const eventDocs = events.length ? docsForEvents(session, events) : [];
|
|
276
|
-
if (!eventDocs.length) ensureConversationMarkdown(session, env);
|
|
277
|
-
const sourceDocs = eventDocs.length ? eventDocs : docsForTranscript(session, readTranscript(session.transcriptPath));
|
|
320
|
+
for (const session of snapshot.sessions) {
|
|
321
|
+
const sourceDocs = canonicalDocsForSession(session);
|
|
278
322
|
for (const sourceDoc of sourceDocs) {
|
|
279
323
|
const indexText = normalizeIndexText(sourceDoc.text);
|
|
280
324
|
if (!indexText) continue;
|
|
@@ -319,6 +363,7 @@ function buildFtsIndexSummary(env = process.env) {
|
|
|
319
363
|
return {
|
|
320
364
|
version: INDEX_VERSION,
|
|
321
365
|
builtAt,
|
|
366
|
+
sessionFingerprint: snapshot.fingerprint,
|
|
322
367
|
docCount,
|
|
323
368
|
avgDocLength: docCount ? totalLength / docCount : 0,
|
|
324
369
|
summaryOnly: true
|
|
@@ -326,8 +371,8 @@ function buildFtsIndexSummary(env = process.env) {
|
|
|
326
371
|
}
|
|
327
372
|
|
|
328
373
|
const FTS_DOCS_INSERT_SQL =
|
|
329
|
-
"INSERT INTO docs(rowid, doc_id, session_id, provider, source_type, repo_canonical, repo_display, scope_canonical, cwd, title, started_at, occurred_at, role, event_id, event_kind, message_index, path, matched_text) " +
|
|
330
|
-
"VALUES (@rowid, @doc_id, @session_id, @provider, @source_type, @repo_canonical, @repo_display, @scope_canonical, @cwd, @title, @started_at, @occurred_at, @role, @event_id, @event_kind, @message_index, @path, @matched_text)";
|
|
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)";
|
|
331
376
|
const FTS_FTS_INSERT_SQL = "INSERT INTO docs_fts(rowid, text) VALUES (@rowid, @text)";
|
|
332
377
|
|
|
333
378
|
function docInsertParams(doc, rowid) {
|
|
@@ -337,9 +382,16 @@ function docInsertParams(doc, rowid) {
|
|
|
337
382
|
session_id: doc.sessionId || "",
|
|
338
383
|
provider: doc.provider || "",
|
|
339
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,
|
|
340
391
|
repo_canonical: doc.repoCanonical || "",
|
|
341
392
|
repo_display: doc.repoDisplay || "",
|
|
342
393
|
scope_canonical: doc.scopeCanonical || "",
|
|
394
|
+
storage_scope: doc.storageScope || "",
|
|
343
395
|
cwd: doc.cwd || "",
|
|
344
396
|
title: doc.title || "",
|
|
345
397
|
started_at: doc.startedAt || "",
|
|
@@ -414,16 +466,19 @@ function insertFtsDocs(handleOrPath, dbPathHint, docs, rowidStart = 1) {
|
|
|
414
466
|
const doc = batch[offset];
|
|
415
467
|
const sqlString = (value) => `'${String(value == null ? "" : value).replace(/'/g, "''")}'`;
|
|
416
468
|
statements.push(
|
|
417
|
-
"INSERT INTO docs(rowid, doc_id, session_id, provider, source_type, repo_canonical, repo_display, scope_canonical, cwd, title, started_at, occurred_at, role, event_id, event_kind, message_index, path, matched_text) VALUES (" +
|
|
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 (" +
|
|
418
470
|
[
|
|
419
471
|
rowid,
|
|
420
472
|
sqlString(doc.id || ""),
|
|
421
473
|
sqlString(doc.sessionId || ""),
|
|
422
474
|
sqlString(doc.provider || ""),
|
|
423
475
|
sqlString(doc.sourceType || ""),
|
|
476
|
+
sqlString(doc.conversationKind || ""),
|
|
477
|
+
isSubagentSession(doc) ? 1 : 0,
|
|
424
478
|
sqlString(doc.repoCanonical || ""),
|
|
425
479
|
sqlString(doc.repoDisplay || ""),
|
|
426
480
|
sqlString(doc.scopeCanonical || ""),
|
|
481
|
+
sqlString(doc.storageScope || ""),
|
|
427
482
|
sqlString(doc.cwd || ""),
|
|
428
483
|
sqlString(doc.title || ""),
|
|
429
484
|
sqlString(doc.startedAt || ""),
|
|
@@ -455,12 +510,22 @@ function insertFtsDocs(handleOrPath, dbPathHint, docs, rowidStart = 1) {
|
|
|
455
510
|
function openFtsBuildDb(dbPath) {
|
|
456
511
|
const Database = loadBetterSqlite3();
|
|
457
512
|
if (Database) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
+
}
|
|
464
529
|
}
|
|
465
530
|
return { kind: "spawn", dbPath };
|
|
466
531
|
}
|
|
@@ -502,7 +567,11 @@ function openFtsReadDb(ftsPath, stat) {
|
|
|
502
567
|
try {
|
|
503
568
|
db = new Database(ftsPath, { readonly: true, fileMustExist: true });
|
|
504
569
|
db.pragma("query_only = ON");
|
|
505
|
-
} catch {
|
|
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);
|
|
506
575
|
return null;
|
|
507
576
|
}
|
|
508
577
|
_ftsReadConn.path = ftsPath;
|
|
@@ -586,6 +655,10 @@ function inlineSqlParams(query, params) {
|
|
|
586
655
|
});
|
|
587
656
|
}
|
|
588
657
|
|
|
658
|
+
function escapeSqlLike(value) {
|
|
659
|
+
return String(value).replace(/[\\%_]/g, (match) => `\\${match}`);
|
|
660
|
+
}
|
|
661
|
+
|
|
589
662
|
function rememberFtsCache(ftsPath, available) {
|
|
590
663
|
let stat = null;
|
|
591
664
|
try {
|
|
@@ -605,14 +678,14 @@ function readFtsMeta(ftsPath, stat) {
|
|
|
605
678
|
const conn = openFtsReadDb(ftsPath, stat);
|
|
606
679
|
if (conn) {
|
|
607
680
|
try {
|
|
608
|
-
const stmt = prepareFtsStatement(conn, "SELECT key, value FROM meta WHERE key IN ('version', 'docCount')");
|
|
681
|
+
const stmt = prepareFtsStatement(conn, "SELECT key, value FROM meta WHERE key IN ('version', 'docCount', 'sessionFingerprint')");
|
|
609
682
|
return stmt.all();
|
|
610
683
|
} catch {
|
|
611
684
|
closeFtsReadDb();
|
|
612
685
|
return null;
|
|
613
686
|
}
|
|
614
687
|
}
|
|
615
|
-
return legacySqliteJson(ftsPath, "SELECT key, value FROM meta WHERE key IN ('version', 'docCount');");
|
|
688
|
+
return legacySqliteJson(ftsPath, "SELECT key, value FROM meta WHERE key IN ('version', 'docCount', 'sessionFingerprint');");
|
|
616
689
|
}
|
|
617
690
|
|
|
618
691
|
function ftsIndexAvailable(env = process.env, options = {}) {
|
|
@@ -651,13 +724,20 @@ function docsForEvents(session, events) {
|
|
|
651
724
|
const indexedKinds = new Set([
|
|
652
725
|
EVENT_KINDS.PROMPT_SUBMITTED,
|
|
653
726
|
EVENT_KINDS.RESPONSE_GENERATED,
|
|
654
|
-
EVENT_KINDS.TOOL_CALLED
|
|
727
|
+
EVENT_KINDS.TOOL_CALLED,
|
|
728
|
+
EVENT_KINDS.TOOL_COMPLETED
|
|
655
729
|
]);
|
|
656
730
|
return events
|
|
657
731
|
.filter((event) => indexedKinds.has(event.kind))
|
|
658
732
|
.map((event) => {
|
|
659
|
-
|
|
733
|
+
let renderedText = renderEventText(event);
|
|
660
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
|
+
}
|
|
661
741
|
return {
|
|
662
742
|
id: event.eventId,
|
|
663
743
|
eventId: event.eventId,
|
|
@@ -666,9 +746,11 @@ function docsForEvents(session, events) {
|
|
|
666
746
|
sessionId: session.sessionId,
|
|
667
747
|
provider: event.provider || session.provider,
|
|
668
748
|
sourceType: event.sourceType || session.sourceType || "",
|
|
749
|
+
conversationKind: session.conversationKind || "",
|
|
669
750
|
repoCanonical: event.repoCanonical || session.repoCanonical || "",
|
|
670
751
|
repoDisplay: displayRepoLabel(session),
|
|
671
752
|
scopeCanonical: event.scopeCanonical || session.scopeCanonical || "",
|
|
753
|
+
storageScope: session.storageScope || "",
|
|
672
754
|
cwd: session.cwd || "",
|
|
673
755
|
title: session.title || event.indexed?.title || "",
|
|
674
756
|
eventTitle: event.indexed?.title || "",
|
|
@@ -684,6 +766,11 @@ function docsForEvents(session, events) {
|
|
|
684
766
|
.filter(Boolean);
|
|
685
767
|
}
|
|
686
768
|
|
|
769
|
+
function canonicalDocsForSession(session) {
|
|
770
|
+
const events = readEvents(session);
|
|
771
|
+
return events.length ? docsForEvents(session, events) : [];
|
|
772
|
+
}
|
|
773
|
+
|
|
687
774
|
function docsForTranscript(session, messages) {
|
|
688
775
|
const docs = [];
|
|
689
776
|
for (const message of messages) {
|
|
@@ -692,9 +779,11 @@ function docsForTranscript(session, messages) {
|
|
|
692
779
|
sessionId: session.sessionId,
|
|
693
780
|
provider: session.provider,
|
|
694
781
|
sourceType: session.sourceType || "",
|
|
782
|
+
conversationKind: session.conversationKind || "",
|
|
695
783
|
repoCanonical: session.repoCanonical || "",
|
|
696
784
|
repoDisplay: displayRepoLabel(session),
|
|
697
785
|
scopeCanonical: session.scopeCanonical || "",
|
|
786
|
+
storageScope: session.storageScope || "",
|
|
698
787
|
cwd: session.cwd || "",
|
|
699
788
|
title: session.title || "",
|
|
700
789
|
startedAt: session.startedAt,
|
|
@@ -758,11 +847,12 @@ function rememberIndexCache(indexPath, index) {
|
|
|
758
847
|
}
|
|
759
848
|
|
|
760
849
|
function searchPastSessions(query, options = {}, env = process.env) {
|
|
850
|
+
const allowMarkdownFallback = Boolean(options.markdownFallback || options.legacyMarkdownFallback || options.allowMarkdownFallback);
|
|
761
851
|
try {
|
|
762
852
|
const eventResults = searchIndexedSessions(query, options, env);
|
|
763
|
-
if (eventResults.length || options.skipMarkdownFallback) return eventResults;
|
|
853
|
+
if (eventResults.length || !allowMarkdownFallback || options.skipMarkdownFallback) return eventResults;
|
|
764
854
|
} catch {
|
|
765
|
-
if (options.skipMarkdownFallback) return [];
|
|
855
|
+
if (!allowMarkdownFallback || options.skipMarkdownFallback) return [];
|
|
766
856
|
// Fall through to the legacy markdown path below.
|
|
767
857
|
}
|
|
768
858
|
return searchMarkdownSessions(query, options, env);
|
|
@@ -772,6 +862,7 @@ function searchMarkdownSessions(query, options = {}, env = process.env) {
|
|
|
772
862
|
const limit = Math.max(1, Math.min(Number(options.limit || 10), 50));
|
|
773
863
|
const maxMatches = Math.max(limit * 8, 40);
|
|
774
864
|
const includeWebChats = Boolean(options.includeWebChats);
|
|
865
|
+
const includeSubagents = Boolean(options.includeSubagents || options.include_subagents);
|
|
775
866
|
const filter = normalizeSessionFilter(options);
|
|
776
867
|
const repo = filter.repo || inferCallingRepo(options.cwd || process.cwd());
|
|
777
868
|
const since = parseSinceFilter(options.since);
|
|
@@ -780,7 +871,7 @@ function searchMarkdownSessions(query, options = {}, env = process.env) {
|
|
|
780
871
|
if (!queryTokens.length && !phrase) return [];
|
|
781
872
|
|
|
782
873
|
const sessions = listSessions(env).filter((session) => {
|
|
783
|
-
if (!matchesSessionFilter(session, { ...filter, includeWebChats, since })) return false;
|
|
874
|
+
if (!matchesSessionFilter(session, { ...filter, includeWebChats, includeSubagents, since })) return false;
|
|
784
875
|
const conversationPath = ensureConversationMarkdown(session, env);
|
|
785
876
|
const searchPath = conversationPath || session.transcriptPath;
|
|
786
877
|
session._searchPath = searchPath;
|
|
@@ -838,22 +929,23 @@ function searchMarkdownSessions(query, options = {}, env = process.env) {
|
|
|
838
929
|
function searchIndexedSessions(query, options = {}, env = process.env) {
|
|
839
930
|
const limit = Math.max(1, Math.min(Number(options.limit || 10), 50));
|
|
840
931
|
const includeWebChats = Boolean(options.includeWebChats);
|
|
932
|
+
const includeSubagents = Boolean(options.includeSubagents || options.include_subagents);
|
|
841
933
|
const filter = normalizeSessionFilter(options);
|
|
842
934
|
const repo = filter.repo || inferCallingRepo(options.cwd || process.cwd());
|
|
843
935
|
const since = parseSinceFilter(options.since);
|
|
844
936
|
const queryTokens = tokenize(query);
|
|
845
937
|
const phrase = String(query || "").trim().toLowerCase();
|
|
846
938
|
if (!queryTokens.length && !phrase) return [];
|
|
847
|
-
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);
|
|
848
940
|
if (ftsResults) return ftsResults;
|
|
849
|
-
if (options.skipJsonIndex) return [];
|
|
941
|
+
if (options.skipJsonIndex || !(options.legacyJsonIndex || options.allowJsonIndex)) return [];
|
|
850
942
|
const index = loadIndex(env, { noRebuild: Boolean(options.noRebuild) });
|
|
851
943
|
if (!index) return [];
|
|
852
944
|
|
|
853
945
|
const scored = [];
|
|
854
946
|
const candidates = candidateDocsForQuery(index, queryTokens, phrase);
|
|
855
947
|
for (const { doc, docIndex } of candidates) {
|
|
856
|
-
if (!matchesSessionFilter(doc, { ...filter, includeWebChats, since })) continue;
|
|
948
|
+
if (!matchesSessionFilter(doc, { ...filter, includeWebChats, includeSubagents, since })) continue;
|
|
857
949
|
|
|
858
950
|
let score = bm25Score(doc, queryTokens, index, docIndex, candidates.termFrequencies);
|
|
859
951
|
if (phrase && doc.text.toLowerCase().includes(phrase)) score += 2.5;
|
|
@@ -887,32 +979,44 @@ function searchIndexedSessions(query, options = {}, env = process.env) {
|
|
|
887
979
|
}));
|
|
888
980
|
}
|
|
889
981
|
|
|
890
|
-
function
|
|
891
|
-
const ftsPath = paths(env).ftsIndex;
|
|
892
|
-
if (!ftsIndexAvailable(env, { noStaleCheck: Boolean(context.options.noRebuild || context.options.allowStaleFts) })) return null;
|
|
893
|
-
const matchQuery = ftsMatchQuery(query);
|
|
894
|
-
if (!matchQuery) return [];
|
|
895
|
-
// Pull a single bounded prefix in rank order rather than paging with growing
|
|
896
|
-
// OFFSET. FTS5 with bm25 ORDER BY recomputes the entire ranked set per
|
|
897
|
-
// OFFSET query, which is quadratic for broad terms (e.g. "react*" matching
|
|
898
|
-
// 1700+ docs across 7 sessions). One query + JS dedupe is O(N) and stays
|
|
899
|
-
// under FTS_MAX_SCAN_ROWS to bound memory for catastrophically broad terms.
|
|
900
|
-
const scanLimit = Math.max(
|
|
901
|
-
Math.max(context.limit * 20, FTS_SEARCH_BATCH_SIZE),
|
|
902
|
-
Math.min(FTS_MAX_SCAN_ROWS, context.limit * 500)
|
|
903
|
-
);
|
|
904
|
-
const rows = ftsSearchRows(ftsPath, matchQuery, { limit: scanLimit, offset: 0, context });
|
|
905
|
-
if (!rows) return null;
|
|
982
|
+
function dedupeFtsRowsBySession(rows, context) {
|
|
906
983
|
const bySession = new Map();
|
|
907
984
|
for (const row of rows) {
|
|
908
985
|
const doc = ftsRowToDoc(row);
|
|
909
|
-
if (!matchesSessionFilter(doc, { ...context.filter, includeWebChats: context.includeWebChats, since: context.since })) continue;
|
|
986
|
+
if (!matchesSessionFilter(doc, { ...context.filter, includeWebChats: context.includeWebChats, includeSubagents: context.includeSubagents, since: context.since })) continue;
|
|
910
987
|
if (!context.options.repo && context.repo && doc.repoCanonical === context.repo) {
|
|
911
988
|
row.rank = Number(row.rank || 0) - 0.05;
|
|
912
989
|
}
|
|
913
990
|
if (!bySession.has(doc.sessionId)) bySession.set(doc.sessionId, { doc, row });
|
|
914
991
|
if (bySession.size >= context.limit) break;
|
|
915
992
|
}
|
|
993
|
+
return bySession;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function searchFtsSessions(query, queryTokens, context, env = process.env) {
|
|
997
|
+
const ftsPath = paths(env).ftsIndex;
|
|
998
|
+
if (!ftsIndexAvailable(env, { noStaleCheck: Boolean(context.options.noRebuild || context.options.allowStaleFts) })) return null;
|
|
999
|
+
const matchQuery = ftsMatchQuery(query);
|
|
1000
|
+
if (!matchQuery) return [];
|
|
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 });
|
|
1017
|
+
if (!rows) return null;
|
|
1018
|
+
bySession = dedupeFtsRowsBySession(rows, context);
|
|
1019
|
+
}
|
|
916
1020
|
return [...bySession.values()].slice(0, context.limit).map(({ doc, row }) => ({
|
|
917
1021
|
session_id: doc.sessionId,
|
|
918
1022
|
provider: doc.provider,
|
|
@@ -947,11 +1051,31 @@ function ftsSearchRows(ftsPath, matchQuery, options) {
|
|
|
947
1051
|
clauses.push(`d.source_type IN (${sourceTypes.map(() => "?").join(", ")})`);
|
|
948
1052
|
for (const value of sourceTypes) params.push(value);
|
|
949
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
|
+
}
|
|
950
1060
|
if (options.context?.since) {
|
|
951
1061
|
const since = options.context.since.toISOString();
|
|
952
1062
|
clauses.push("(d.started_at >= ? OR (d.started_at = '' AND d.occurred_at >= ?))");
|
|
953
1063
|
params.push(since, since);
|
|
954
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
|
+
}
|
|
955
1079
|
const limit = Math.max(1, Number(options.limit) || FTS_SEARCH_BATCH_SIZE);
|
|
956
1080
|
const offset = Math.max(0, Number(options.offset) || 0);
|
|
957
1081
|
// LIMIT/OFFSET stay parameterized so a single prepared-statement shape
|
|
@@ -959,8 +1083,8 @@ function ftsSearchRows(ftsPath, matchQuery, options) {
|
|
|
959
1083
|
// per call but the resulting prepared statement is cached on the connection.
|
|
960
1084
|
const sql = [
|
|
961
1085
|
"SELECT",
|
|
962
|
-
" d.doc_id, d.session_id, d.provider, d.source_type, d.repo_canonical, d.repo_display,",
|
|
963
|
-
" d.scope_canonical, d.cwd, d.title, d.started_at, d.occurred_at, d.role,",
|
|
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,",
|
|
964
1088
|
" d.event_id, d.event_kind, d.message_index, d.path, d.matched_text,",
|
|
965
1089
|
" snippet(docs_fts, 0, '', '', '...', 32) AS excerpt,",
|
|
966
1090
|
" bm25(docs_fts) AS rank",
|
|
@@ -992,9 +1116,12 @@ function ftsRowToDoc(row) {
|
|
|
992
1116
|
sessionId: row.session_id || "",
|
|
993
1117
|
provider: row.provider || "",
|
|
994
1118
|
sourceType: row.source_type || "",
|
|
1119
|
+
conversationKind: row.conversation_kind || "",
|
|
1120
|
+
isSubagent: row.is_subagent == null ? undefined : Number(row.is_subagent),
|
|
995
1121
|
repoCanonical: row.repo_canonical || "",
|
|
996
1122
|
repoDisplay: row.repo_display || "",
|
|
997
1123
|
scopeCanonical: row.scope_canonical || "",
|
|
1124
|
+
storageScope: row.storage_scope || "",
|
|
998
1125
|
cwd: row.cwd || "",
|
|
999
1126
|
title: row.title || "",
|
|
1000
1127
|
startedAt: row.started_at || "",
|
|
@@ -1220,11 +1347,16 @@ function inferCallingRepo(cwd) {
|
|
|
1220
1347
|
}
|
|
1221
1348
|
|
|
1222
1349
|
function listHistorySessions(options = {}, env = process.env) {
|
|
1350
|
+
return listHistorySessionsFromSessions(listSessions(env), options);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function listHistorySessionsFromSessions(sessions, options = {}) {
|
|
1223
1354
|
const since = parseSinceFilter(options.since);
|
|
1224
1355
|
const includeWebChats = options.includeWebChats !== false;
|
|
1356
|
+
const includeSubagents = Boolean(options.includeSubagents || options.include_subagents);
|
|
1225
1357
|
const filter = normalizeSessionFilter(options);
|
|
1226
|
-
return
|
|
1227
|
-
.filter((session) => matchesSessionFilter(session, { ...filter, includeWebChats, since }))
|
|
1358
|
+
return (Array.isArray(sessions) ? sessions : [])
|
|
1359
|
+
.filter((session) => matchesSessionFilter(session, { ...filter, includeWebChats, includeSubagents, since }))
|
|
1228
1360
|
.map(historySessionSummary)
|
|
1229
1361
|
.sort((a, b) => String(historySessionSortTime(b)).localeCompare(String(historySessionSortTime(a))));
|
|
1230
1362
|
}
|
|
@@ -1245,6 +1377,7 @@ function historySessionSummary(session) {
|
|
|
1245
1377
|
chat_display_name: session.chatDisplayName || undefined,
|
|
1246
1378
|
chat_project_path: session.chatProjectPath || undefined,
|
|
1247
1379
|
conversation_kind: session.conversationKind || undefined,
|
|
1380
|
+
parent_composer_id: session.parentComposerId || undefined,
|
|
1248
1381
|
pinned: Boolean(session.pinned) || undefined,
|
|
1249
1382
|
cwd: session.cwd || undefined,
|
|
1250
1383
|
title: session.title || undefined,
|
|
@@ -1257,9 +1390,25 @@ function historySessionSummary(session) {
|
|
|
1257
1390
|
usage: session.usage || undefined,
|
|
1258
1391
|
estimatedUsage: session.estimatedUsage || undefined,
|
|
1259
1392
|
models: session.models || undefined,
|
|
1393
|
+
toolUsage: session.toolUsage || undefined,
|
|
1394
|
+
outputTokenWork: session.outputTokenWork || undefined,
|
|
1395
|
+
outcomes: session.outcomes || undefined,
|
|
1260
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,
|
|
1261
1409
|
conversation: session.conversationPath,
|
|
1262
|
-
transcript: session.transcriptPath
|
|
1410
|
+
transcript: session.transcriptPath,
|
|
1411
|
+
events: session.eventPath
|
|
1263
1412
|
};
|
|
1264
1413
|
}
|
|
1265
1414
|
|
|
@@ -1282,6 +1431,7 @@ function displayRepoLabel(session) {
|
|
|
1282
1431
|
function displayScopeLabel(scope) {
|
|
1283
1432
|
return {
|
|
1284
1433
|
"claude-desktop/uncategorized": "Claude Desktop (uncategorized)",
|
|
1434
|
+
"claude-cowork/uncategorized": "Claude Cowork (uncategorized)",
|
|
1285
1435
|
"claude-code-desktop/uncategorized": "Claude Code Desktop (uncategorized)"
|
|
1286
1436
|
}[scope] || scope;
|
|
1287
1437
|
}
|
|
@@ -1317,6 +1467,10 @@ function listRecentSessions(limit = 20, options = {}, env = process.env) {
|
|
|
1317
1467
|
return listHistorySessions(options, env).slice(0, limit);
|
|
1318
1468
|
}
|
|
1319
1469
|
|
|
1470
|
+
function listRecentSessionsFromSessions(sessions, limit = 20, options = {}) {
|
|
1471
|
+
return listHistorySessionsFromSessions(sessions, options).slice(0, limit);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1320
1474
|
function normalizeSessionFilter(options = {}) {
|
|
1321
1475
|
const providerInput = options.provider || options.source || "";
|
|
1322
1476
|
const normalizedProvider = normalizeProviderFilter(providerInput);
|
|
@@ -1337,6 +1491,8 @@ function normalizeProviderFilter(value) {
|
|
|
1337
1491
|
claude_cli: { provider: "claude_code" },
|
|
1338
1492
|
claude_desktop: { provider: "claude_desktop" },
|
|
1339
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"] },
|
|
1340
1496
|
claude_workspace: { provider: "claude_desktop", sourceType: "claude-workspace-desktop", sourceTypes: ["claude-workspace-desktop"] },
|
|
1341
1497
|
claude_workspace_desktop: { provider: "claude_desktop", sourceType: "claude-workspace-desktop", sourceTypes: ["claude-workspace-desktop"] },
|
|
1342
1498
|
claude_sdk: { provider: "claude_sdk" },
|
|
@@ -1353,10 +1509,21 @@ function normalizeProviderFilter(value) {
|
|
|
1353
1509
|
aider: { provider: "aider", sourceType: "aider-chat-history", sourceTypes: ["aider-chat-history"] },
|
|
1354
1510
|
devin: { provider: "devin" },
|
|
1355
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"] },
|
|
1356
1513
|
gemini: { provider: "gemini_cli" },
|
|
1357
1514
|
gemini_cli: { provider: "gemini_cli" },
|
|
1358
1515
|
windsurf: { provider: "windsurf" },
|
|
1359
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" },
|
|
1360
1527
|
chatgpt: { provider: "chatgpt" },
|
|
1361
1528
|
claude_web: { provider: "claude_web" },
|
|
1362
1529
|
claude_ai: { provider: "claude_web" }
|
|
@@ -1368,6 +1535,7 @@ function matchesSessionFilter(session, filter) {
|
|
|
1368
1535
|
if (filter.provider && session.provider !== filter.provider) return false;
|
|
1369
1536
|
const sourceTypes = filter.sourceTypes?.length ? filter.sourceTypes : filter.sourceType ? [filter.sourceType] : [];
|
|
1370
1537
|
if (sourceTypes.length && !sourceTypes.includes(session.sourceType)) return false;
|
|
1538
|
+
if (!filter.includeSubagents && isSubagentSession(session)) return false;
|
|
1371
1539
|
if (!filter.includeWebChats && session.scopeCanonical && session.storageScope !== "local") return false;
|
|
1372
1540
|
if (filter.repo && !matchesRepoFilter(session, filter.repo)) return false;
|
|
1373
1541
|
if (filter.since) {
|
|
@@ -1378,6 +1546,10 @@ function matchesSessionFilter(session, filter) {
|
|
|
1378
1546
|
return true;
|
|
1379
1547
|
}
|
|
1380
1548
|
|
|
1549
|
+
function isSubagentSession(session) {
|
|
1550
|
+
return /_subagent$/.test(String(session?.conversationKind || session?.conversation_kind || ""));
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1381
1553
|
function matchesRepoFilter(session, repoFilter) {
|
|
1382
1554
|
const wanted = String(repoFilter || "").toLowerCase();
|
|
1383
1555
|
if (!wanted) return true;
|
|
@@ -1442,22 +1614,24 @@ function rebuildIndexSummary(env = process.env, options = {}) {
|
|
|
1442
1614
|
}
|
|
1443
1615
|
|
|
1444
1616
|
function indexIsStale(indexPath, env = process.env) {
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
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;
|
|
1452
1626
|
try {
|
|
1453
|
-
|
|
1454
|
-
if (fs.statSync(session.metadataPath).mtimeMs > indexStat.mtimeMs) return true;
|
|
1455
|
-
if (session.eventPath && fs.existsSync(session.eventPath) && fs.statSync(session.eventPath).mtimeMs > indexStat.mtimeMs) return true;
|
|
1627
|
+
stat = fs.statSync(ftsPath);
|
|
1456
1628
|
} catch {
|
|
1457
|
-
return
|
|
1629
|
+
return "";
|
|
1458
1630
|
}
|
|
1631
|
+
const rows = readFtsMeta(ftsPath, stat);
|
|
1632
|
+
return rows?.find((row) => row.key === "sessionFingerprint")?.value || "";
|
|
1459
1633
|
}
|
|
1460
|
-
return
|
|
1634
|
+
return readIndexSummary(indexPath)?.sessionFingerprint || "";
|
|
1461
1635
|
}
|
|
1462
1636
|
|
|
1463
1637
|
function parseSinceFilter(value) {
|
|
@@ -1498,11 +1672,23 @@ module.exports = {
|
|
|
1498
1672
|
buildIndexSummary,
|
|
1499
1673
|
chunkText,
|
|
1500
1674
|
listHistorySessions,
|
|
1675
|
+
listHistorySessionsFromSessions,
|
|
1501
1676
|
listRecentSessions,
|
|
1677
|
+
listRecentSessionsFromSessions,
|
|
1502
1678
|
loadIndex,
|
|
1503
1679
|
readIndexSummary,
|
|
1680
|
+
rebuildIndexSummary,
|
|
1504
1681
|
reindexIfNeeded,
|
|
1505
1682
|
sessionHistoryTime,
|
|
1506
1683
|
searchPastSessions,
|
|
1507
|
-
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
|
+
}
|
|
1508
1694
|
};
|