coding-tool-x 3.8.7 → 3.8.8
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/dist/web/assets/{AgentsDrawer-C7ePByZx.js → AgentsDrawer-DRQuwi0m.js} +1 -1
- package/dist/web/assets/Analytics-Cf5KwEdj.js +1 -0
- package/dist/web/assets/AssetPathField-DyniM0pF.js +1 -0
- package/dist/web/assets/{ChatHistoryDrawer-Ccl4rhdz.js → ChatHistoryDrawer-CrfS0KYV.js} +1 -1
- package/dist/web/assets/{CollapseItem-MxG_UD4o.js → CollapseItem-ClMsJuUu.js} +1 -1
- package/dist/web/assets/{CommandsDrawer-DWbaxHRZ.js → CommandsDrawer-CzHelf56.js} +1 -1
- package/dist/web/assets/ConfigExportDrawer-BEZ8wTby.js +1 -0
- package/dist/web/assets/ConfigTemplates-Crg3xWe5.js +1 -0
- package/dist/web/assets/{ConfigTemplatesDrawer-CKMFsfEF.js → ConfigTemplatesDrawer-BqM32Im7.js} +1 -1
- package/dist/web/assets/{ConfigTemplatesPanel-Cvbnkvpt.js → ConfigTemplatesPanel-lTdutC8H.js} +1 -1
- package/dist/web/assets/{Divider-D28g1gwj.js → Divider-DsdbkHrG.js} +1 -1
- package/dist/web/assets/{FavoritesDrawer-DA-dR_vP.js → FavoritesDrawer-DDyWUBJR.js} +1 -1
- package/dist/web/assets/Forward-DLCL7aMY.js +1 -0
- package/dist/web/assets/{GatewayConvertDrawer-402_raGb.js → GatewayConvertDrawer-Rigtt_5J.js} +1 -1
- package/dist/web/assets/Home-tCSKmSHV.js +1 -0
- package/dist/web/assets/{MarkdownEditor-BvkDECwH.js → MarkdownEditor-BMMUufSc.js} +1 -1
- package/dist/web/assets/McpDrawer-CmE-d7VW.js +1 -0
- package/dist/web/assets/{McpFormDrawer-DPMnyt5L.js → McpFormDrawer-ByvS18od.js} +1 -1
- package/dist/web/assets/{PluginManager-FKVeugr4.js → PluginManager-C4kJlpVX.js} +1 -1
- package/dist/web/assets/{PluginsDrawer-MvM1R55d.js → PluginsDrawer-05R4k15x.js} +1 -1
- package/dist/web/assets/PluginsPanel-BCoEs7Og.js +1 -0
- package/dist/web/assets/PluginsPanel-CzGMK-Ap.css +1 -0
- package/dist/web/assets/ProjectList-BJt1OIaI.js +1 -0
- package/dist/web/assets/ProjectList-DG3yu8xZ.css +1 -0
- package/dist/web/assets/PromptsDrawer-DnwBf_oW.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-B7d-38wH.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-yrAD4p1A.css +1 -0
- package/dist/web/assets/SessionCard-BUgjHQwX.js +1 -0
- package/dist/web/assets/SessionList-B-bXZRFk.js +1 -0
- package/dist/web/assets/SessionList-qG_XpPBJ.css +1 -0
- package/dist/web/assets/{SettingsDrawer-lMjttbkD.js → SettingsDrawer-BITzjQLl.js} +1 -1
- package/dist/web/assets/SkillManager-D9jkNuRM.js +1 -0
- package/dist/web/assets/{SkillsDrawer-DF6JsSyj.js → SkillsDrawer-0Z4IefxC.js} +1 -1
- package/dist/web/assets/SkillsPanel-BKIVOj7n.js +1 -0
- package/dist/web/assets/{SkillsPanel-B3Nm5j32.css → SkillsPanel-C4ke56nF.css} +1 -1
- package/dist/web/assets/{SpeedTestDrawer-BaZkZqPP.js → SpeedTestDrawer-Bx1bYf5i.js} +1 -1
- package/dist/web/assets/Upload-CzC4rGI4.js +1 -0
- package/dist/web/assets/{WorkspaceDrawer-BFSwZ5yU.js → WorkspaceDrawer-eujtWpoq.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-BHL1uYct.js → WorkspaceManager-CQ-8Ej68.js} +1 -1
- package/dist/web/assets/{WorkspacePanel-B5PooBtg.js → WorkspacePanel-Cql5qE2R.js} +1 -1
- package/dist/web/assets/headers-BqDmkMjy.js +1 -0
- package/dist/web/assets/{icons-B2Vg-IOC.js → icons-DmFe4b5j.js} +1 -1
- package/dist/web/assets/index-Cmmwj3Fx.js +2 -0
- package/dist/web/assets/{index-BohDDAy_.css → index-Cnbe696C.css} +1 -1
- package/dist/web/assets/{light-Cp0t7v32.js → light-0eavxNvt.js} +1 -1
- package/dist/web/assets/platforms-BmzzwnSw.js +1 -0
- package/dist/web/assets/platforms-DWXgmy2w.js +1 -0
- package/dist/web/assets/session-visibility-BAmxh3cK.js +1 -0
- package/dist/web/assets/sessions-D4yQtfKb.js +1 -0
- package/dist/web/assets/sessions-DqlbocLJ.js +1 -0
- package/dist/web/assets/use-message-DqCwefG6.js +1 -0
- package/dist/web/assets/{useResponsiveDrawer-D-DDpbnI.js → useResponsiveDrawer-DZd5rNk5.js} +1 -1
- package/dist/web/index.html +7 -7
- package/package.json +1 -1
- package/src/commands/search.js +17 -2
- package/src/config/paths.js +2 -0
- package/src/platforms/driver-registry.js +9 -1
- package/src/platforms/drivers/claude/session-history-adapter.js +72 -1
- package/src/platforms/drivers/claude/sessions-implementation.js +49 -7
- package/src/platforms/drivers/claude/sessions.js +30 -6
- package/src/platforms/drivers/codex/parser.js +57 -1
- package/src/platforms/drivers/codex/session-history-adapter.js +8 -2
- package/src/platforms/drivers/codex/sessions-implementation.js +25 -3
- package/src/platforms/drivers/codex/sessions.js +18 -1
- package/src/platforms/drivers/dsh/api-operations.js +396 -0
- package/src/platforms/drivers/dsh/channels-implementation.js +458 -0
- package/src/platforms/drivers/dsh/channels.js +191 -0
- package/src/platforms/drivers/dsh/common.js +571 -0
- package/src/platforms/drivers/dsh/native-config.js +93 -0
- package/src/platforms/drivers/dsh/native-logs.js +116 -0
- package/src/platforms/drivers/dsh/plugin-manager.js +117 -0
- package/src/platforms/drivers/dsh/projects.js +23 -0
- package/src/platforms/drivers/dsh/proxy-implementation.js +260 -0
- package/src/platforms/drivers/dsh/proxy.js +23 -0
- package/src/platforms/drivers/dsh/resources.js +346 -0
- package/src/platforms/drivers/dsh/sessions.js +587 -0
- package/src/platforms/drivers/dsh/statistics-implementation.js +238 -0
- package/src/platforms/drivers/dsh/statistics.js +24 -0
- package/src/platforms/drivers/gemini/session-history-adapter.js +49 -1
- package/src/platforms/drivers/gemini/sessions-implementation.js +46 -1
- package/src/platforms/drivers/gemini/sessions.js +2 -1
- package/src/platforms/drivers/omp/session-history-adapter.js +42 -1
- package/src/platforms/drivers/omp/sessions-implementation.js +51 -3
- package/src/platforms/drivers/omp/sessions.js +33 -2
- package/src/platforms/drivers/opencode/sessions-implementation.js +89 -9
- package/src/platforms/drivers/opencode/sessions.js +37 -1
- package/src/platforms/manifest-schema.js +43 -3
- package/src/platforms/manifests/dsh.json +542 -0
- package/src/platforms/path-resolver.js +2 -0
- package/src/platforms/platform-path-context.js +12 -2
- package/src/platforms/registry.js +16 -1
- package/src/platforms/runtime.js +5 -0
- package/src/platforms/session-history-adapters.js +1 -0
- package/src/server/api/platform-api-config.js +23 -2
- package/src/server/api/platform-route-factory.js +9 -2
- package/src/server/api/platforms.js +19 -1
- package/src/server/api/plugins.js +108 -13
- package/src/server/services/config-registry-service.js +10 -1
- package/src/server/services/config-templates-service.js +1 -0
- package/src/server/services/plugins-service.js +474 -103
- package/src/server/services/session-history-index.js +900 -140
- package/src/server/services/session-history-worker.js +3 -1
- package/src/server/services/skill-service.js +214 -7
- package/src/server/services/usage-log-utils.js +1 -0
- package/src/server/services/workspace-service.js +39 -10
- package/src/shared/driver-factories/api.js +22 -6
- package/src/shared/driver-factories/projects.js +8 -0
- package/dist/web/assets/Analytics-BVG6ehFv.js +0 -1
- package/dist/web/assets/AssetPathField-BZI9NvEr.js +0 -1
- package/dist/web/assets/ConfigExportDrawer-DrawKwxE.js +0 -1
- package/dist/web/assets/ConfigTemplates-AiRPv2cd.js +0 -1
- package/dist/web/assets/Home-CTF8ZWKW.js +0 -1
- package/dist/web/assets/McpDrawer-DZGHkuDm.js +0 -1
- package/dist/web/assets/PluginsPanel-CKuBs38B.js +0 -1
- package/dist/web/assets/PluginsPanel-D6ZqITcR.css +0 -1
- package/dist/web/assets/ProjectList-677RLZ8O.css +0 -1
- package/dist/web/assets/ProjectList-tsjwjXV1.js +0 -1
- package/dist/web/assets/PromptsDrawer-B-X9ci8n.js +0 -1
- package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +0 -1
- package/dist/web/assets/RecentSessionsDrawer-wLktMF_c.js +0 -1
- package/dist/web/assets/SessionCard-DkJPy0TY.js +0 -1
- package/dist/web/assets/SessionList-BpYRz1cX.css +0 -1
- package/dist/web/assets/SessionList-Gzhh4FOZ.js +0 -1
- package/dist/web/assets/SkillManager-CzFxWxx-.js +0 -1
- package/dist/web/assets/SkillsPanel--lLEts9y.js +0 -1
- package/dist/web/assets/Upload-BEQ-3Kqq.js +0 -1
- package/dist/web/assets/headers-cf-QapUq.js +0 -1
- package/dist/web/assets/index-BFInlZGI.js +0 -2
- package/dist/web/assets/platforms-CBkB45JP.js +0 -1
- package/dist/web/assets/platforms-FTpjxAT6.js +0 -1
- package/dist/web/assets/sessions-Cmy31slD.js +0 -1
- package/dist/web/assets/sessions-yC_oVVh8.js +0 -1
- package/dist/web/assets/use-message-DoLM8C1C.js +0 -1
|
@@ -25,6 +25,8 @@ const INVENTORY_LOCK_WAIT_MS = 30000;
|
|
|
25
25
|
const INVENTORY_PARSE_BATCH_SIZE = 8;
|
|
26
26
|
/** @type {number} Maximum number of files parsed concurrently */
|
|
27
27
|
const INVENTORY_PARSE_CONCURRENCY = 2;
|
|
28
|
+
const SUMMARY_REFRESH_CONCURRENCY = 8;
|
|
29
|
+
const SUMMARY_VERSION = 1;
|
|
28
30
|
const BUILTIN_SESSION_SOURCES = new Set(['claude', 'codex', 'gemini', 'omp']);
|
|
29
31
|
// Keep synchronized with the parserVersion exposed by the built-in session drivers.
|
|
30
32
|
const BUILTIN_SESSION_PARSER_VERSIONS = Object.freeze({ claude: 2, codex: 2, gemini: 2, omp: 2 });
|
|
@@ -103,8 +105,8 @@ function _tryAcquireInventoryLock(dbPath, source) {
|
|
|
103
105
|
return null;
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
async function _acquireInventoryLock(dbPath, source) {
|
|
107
|
-
const deadline = Date.now() +
|
|
108
|
+
async function _acquireInventoryLock(dbPath, source, waitMs = INVENTORY_LOCK_WAIT_MS) {
|
|
109
|
+
const deadline = Date.now() + Math.max(0, Number(waitMs) || 0);
|
|
108
110
|
while (true) {
|
|
109
111
|
const lock = _tryAcquireInventoryLock(dbPath, source);
|
|
110
112
|
if (lock?.busy) {
|
|
@@ -117,6 +119,21 @@ async function _acquireInventoryLock(dbPath, source) {
|
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
function _hasActiveInventoryLock(dbPath) {
|
|
123
|
+
const lockPath = _inventoryLockPath(dbPath);
|
|
124
|
+
try {
|
|
125
|
+
const owner = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
126
|
+
const ageMs = Math.max(0, Date.now() - fs.statSync(lockPath).mtimeMs);
|
|
127
|
+
const ownerPid = Number(owner?.pid);
|
|
128
|
+
return _isProcessAlive(ownerPid) || (
|
|
129
|
+
(!Number.isInteger(ownerPid) || ownerPid <= 0)
|
|
130
|
+
&& ageMs < INVENTORY_LOCK_STALE_MS
|
|
131
|
+
);
|
|
132
|
+
} catch (_) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
function _releaseInventoryLock(lock) {
|
|
121
138
|
if (!lock) return;
|
|
122
139
|
try { fs.closeSync(lock.fd); } catch (_err) {}
|
|
@@ -199,9 +216,38 @@ const SCHEMA_SQL = `
|
|
|
199
216
|
CREATE TABLE IF NOT EXISTS source_state (
|
|
200
217
|
source TEXT PRIMARY KEY,
|
|
201
218
|
last_inventory_ms INTEGER,
|
|
202
|
-
last_error TEXT
|
|
219
|
+
last_error TEXT,
|
|
220
|
+
summary_inventory_ms INTEGER,
|
|
221
|
+
summary_error TEXT
|
|
203
222
|
);
|
|
204
223
|
|
|
224
|
+
CREATE TABLE IF NOT EXISTS session_summary (
|
|
225
|
+
source TEXT NOT NULL,
|
|
226
|
+
file_path TEXT NOT NULL,
|
|
227
|
+
session_id TEXT NOT NULL,
|
|
228
|
+
project_name TEXT NOT NULL,
|
|
229
|
+
project_display_name TEXT,
|
|
230
|
+
project_full_path TEXT,
|
|
231
|
+
first_message TEXT,
|
|
232
|
+
git_branch TEXT,
|
|
233
|
+
provider TEXT,
|
|
234
|
+
model TEXT,
|
|
235
|
+
size INTEGER NOT NULL DEFAULT 0,
|
|
236
|
+
mtime_ms INTEGER NOT NULL DEFAULT 0,
|
|
237
|
+
started_at INTEGER,
|
|
238
|
+
updated_at INTEGER,
|
|
239
|
+
summary_version INTEGER NOT NULL DEFAULT 1,
|
|
240
|
+
extra_json TEXT,
|
|
241
|
+
PRIMARY KEY (source, file_path),
|
|
242
|
+
UNIQUE (source, session_id)
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
CREATE INDEX IF NOT EXISTS idx_session_summary_project
|
|
246
|
+
ON session_summary(source, project_name, updated_at DESC, session_id ASC);
|
|
247
|
+
|
|
248
|
+
CREATE INDEX IF NOT EXISTS idx_session_summary_updated
|
|
249
|
+
ON session_summary(source, updated_at DESC, session_id ASC);
|
|
250
|
+
|
|
205
251
|
CREATE TABLE IF NOT EXISTS session_file (
|
|
206
252
|
source TEXT NOT NULL,
|
|
207
253
|
file_path TEXT NOT NULL,
|
|
@@ -221,6 +267,7 @@ CREATE TABLE IF NOT EXISTS session_file (
|
|
|
221
267
|
usage_json TEXT,
|
|
222
268
|
extra_json TEXT,
|
|
223
269
|
parser_version INTEGER NOT NULL DEFAULT 0,
|
|
270
|
+
content_indexed INTEGER NOT NULL DEFAULT 1,
|
|
224
271
|
PRIMARY KEY (source, file_path),
|
|
225
272
|
UNIQUE (source, session_id)
|
|
226
273
|
);
|
|
@@ -310,6 +357,17 @@ function _normalizePageOpts(options = {}) {
|
|
|
310
357
|
return { page, limit, order };
|
|
311
358
|
}
|
|
312
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Normalize the bounded list pagination contract. Project/session lists are
|
|
362
|
+
* intentionally capped at 100 even when a caller supplies a larger limit.
|
|
363
|
+
*/
|
|
364
|
+
function _normalizeListOpts(options = {}) {
|
|
365
|
+
const page = Math.max(1, Number.parseInt(options.page, 10) || 1);
|
|
366
|
+
const limit = Math.max(1, Math.min(100, Number.parseInt(options.limit, 10) || 20));
|
|
367
|
+
const query = String(options.q ?? options.search ?? '').trim();
|
|
368
|
+
return { page, limit, query };
|
|
369
|
+
}
|
|
370
|
+
|
|
313
371
|
/**
|
|
314
372
|
* Double embedded quotes in a string for FTS5 MATCH.
|
|
315
373
|
* @param {string} s
|
|
@@ -319,6 +377,13 @@ function _ftsQuote(s) {
|
|
|
319
377
|
return '"' + String(s).replace(/"/g, '""') + '"';
|
|
320
378
|
}
|
|
321
379
|
|
|
380
|
+
function _timestampValue(value) {
|
|
381
|
+
const numeric = Number(value);
|
|
382
|
+
if (Number.isFinite(numeric) && numeric > 0) return numeric;
|
|
383
|
+
const parsed = Date.parse(String(value || ''));
|
|
384
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
322
387
|
function _normalizeTypedSessionsResult(result, defaultOperation = 'resolve-driver') {
|
|
323
388
|
if (!result || typeof result !== 'object') {
|
|
324
389
|
return null;
|
|
@@ -478,7 +543,39 @@ function _adaptRuntimeSessionsDriver(driver) {
|
|
|
478
543
|
await unwrap(await driver.parse(descriptor, ...args)),
|
|
479
544
|
descriptor,
|
|
480
545
|
{ preservePayloadUpdatedAt: driver.preservePayloadUpdatedAt === true }
|
|
481
|
-
)
|
|
546
|
+
),
|
|
547
|
+
summarize: typeof driver.summarize === 'function'
|
|
548
|
+
? async (descriptor, ...args) => _normalizeRuntimeSummaryResult(
|
|
549
|
+
await unwrap(await driver.summarize(descriptor, ...args)),
|
|
550
|
+
descriptor
|
|
551
|
+
)
|
|
552
|
+
: null
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function _normalizeRuntimeSummaryResult(result, descriptor = {}) {
|
|
557
|
+
if (!result || typeof result !== 'object') return null;
|
|
558
|
+
const source = result.session && typeof result.session === 'object' ? result.session : result;
|
|
559
|
+
if (!source.sessionId && !descriptor.sessionId) return null;
|
|
560
|
+
const projectHint = descriptor.projectHint || source.projectHint || source.projectName || '';
|
|
561
|
+
const projectName = source.projectName || projectHint || 'unknown';
|
|
562
|
+
const extra = _safeParseObject(source.extraJson);
|
|
563
|
+
return {
|
|
564
|
+
sessionId: String(source.sessionId || descriptor.sessionId),
|
|
565
|
+
projectName,
|
|
566
|
+
projectDisplayName: source.projectDisplayName || projectName,
|
|
567
|
+
projectFullPath: source.projectFullPath || source.projectPath || null,
|
|
568
|
+
firstMessage: source.firstMessage || null,
|
|
569
|
+
gitBranch: source.gitBranch || null,
|
|
570
|
+
provider: source.provider || null,
|
|
571
|
+
model: source.model || null,
|
|
572
|
+
startedAt: source.startedAt || null,
|
|
573
|
+
updatedAt: source.updatedAt ?? descriptor.mtimeMs ?? null,
|
|
574
|
+
extraJson: JSON.stringify({
|
|
575
|
+
...extra,
|
|
576
|
+
projectHint: projectHint || null,
|
|
577
|
+
mtimeMs: descriptor.mtimeMs ?? null
|
|
578
|
+
})
|
|
482
579
|
};
|
|
483
580
|
}
|
|
484
581
|
// ---------------------------------------------------------------------------
|
|
@@ -513,6 +610,10 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
513
610
|
let _ftsAvailable = null;
|
|
514
611
|
/** @type {Map<string, Promise<void>>} */
|
|
515
612
|
const _inflight = new Map();
|
|
613
|
+
/** @type {Map<string, Promise<void>>} */
|
|
614
|
+
const _summaryInflight = new Map();
|
|
615
|
+
/** @type {Map<string, Promise<void>>} */
|
|
616
|
+
const _contentInflight = new Map();
|
|
516
617
|
|
|
517
618
|
function _getDb() {
|
|
518
619
|
if (_db) return _db;
|
|
@@ -535,6 +636,29 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
535
636
|
return _parserVersionForDriver(runtimeDriver);
|
|
536
637
|
}
|
|
537
638
|
|
|
639
|
+
function _resolveSessionsAdapter(source, config = indexConfig) {
|
|
640
|
+
let adapter = null;
|
|
641
|
+
if (explicitAdapters) {
|
|
642
|
+
adapter = adapters[source];
|
|
643
|
+
} else {
|
|
644
|
+
const runtimeDriver = _getRuntimeSessionsDriver(runtime, source, config);
|
|
645
|
+
if (_isTypedSessionsResult(runtimeDriver)) {
|
|
646
|
+
throw _typedFailureToError(runtimeDriver);
|
|
647
|
+
}
|
|
648
|
+
if (runtimeDriver) {
|
|
649
|
+
adapter = _adaptRuntimeSessionsDriver(runtimeDriver);
|
|
650
|
+
} else if (BUILTIN_SESSION_SOURCES.has(source) && adapters[source]) {
|
|
651
|
+
adapter = adapters[source];
|
|
652
|
+
} else {
|
|
653
|
+
throw _typedFailureToError(_typedSessionsUnsupported(
|
|
654
|
+
source,
|
|
655
|
+
new Error(`unsupported sessions source: ${source}`)
|
|
656
|
+
));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return adapter;
|
|
660
|
+
}
|
|
661
|
+
|
|
538
662
|
/**
|
|
539
663
|
* Check only the persisted inventory timestamp. Inventory owns file checks.
|
|
540
664
|
*
|
|
@@ -542,15 +666,37 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
542
666
|
* @returns {boolean}
|
|
543
667
|
*/
|
|
544
668
|
function _isSourceFresh(source) {
|
|
669
|
+
return _isSourceFreshForScope(source, null);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function _isSourceFreshForScope(source, projectName = null) {
|
|
545
673
|
const row = _getDb().prepare(
|
|
546
674
|
'SELECT last_inventory_ms FROM source_state WHERE source = ?'
|
|
547
675
|
).get(source);
|
|
548
|
-
|
|
676
|
+
const migrationPending = _hasKnownParserMigrationPending(source)
|
|
677
|
+
|| (explicitAdapters && _hasParserMigrationPending(source));
|
|
678
|
+
const baseFresh = Boolean(
|
|
549
679
|
row?.last_inventory_ms
|
|
550
680
|
&& Date.now() - Number(row.last_inventory_ms) < INDEX_INVENTORY_TTL_MS
|
|
551
|
-
&& !
|
|
552
|
-
&& (!explicitAdapters || !_hasParserMigrationPending(source))
|
|
681
|
+
&& !migrationPending
|
|
553
682
|
);
|
|
683
|
+
|
|
684
|
+
const scope = projectName
|
|
685
|
+
? sourceFreshness.get(`${source}:${projectName}`)
|
|
686
|
+
: sourceFreshness.get(source);
|
|
687
|
+
if (projectName) {
|
|
688
|
+
// A complete source inventory also covers project-scoped searches. A
|
|
689
|
+
// project-only refresh has no persisted source timestamp, so rely on
|
|
690
|
+
// its in-process scope marker until the normal inventory TTL expires.
|
|
691
|
+
if (baseFresh && scope?.kind === 'source') return true;
|
|
692
|
+
return scope?.kind === 'project'
|
|
693
|
+
&& scope.projectName === projectName
|
|
694
|
+
&& Date.now() - scope.at < INDEX_INVENTORY_TTL_MS;
|
|
695
|
+
}
|
|
696
|
+
if (!baseFresh) return false;
|
|
697
|
+
// A project-scoped refresh deliberately does not make the complete source
|
|
698
|
+
// fresh. Otherwise a later workspace search could reuse a partial index.
|
|
699
|
+
return !scope || scope.kind === 'source';
|
|
554
700
|
}
|
|
555
701
|
|
|
556
702
|
function _hasKnownParserMigrationPending(source) {
|
|
@@ -586,10 +732,31 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
586
732
|
function _initSchema(db) {
|
|
587
733
|
db.exec('PRAGMA journal_mode = WAL');
|
|
588
734
|
db.exec(SCHEMA_SQL);
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
735
|
+
const ensureColumn = (table, name, definition) => {
|
|
736
|
+
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
737
|
+
if (!columns.some((column) => column.name === name)) {
|
|
738
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${definition}`);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
ensureColumn('source_state', 'summary_inventory_ms', 'INTEGER');
|
|
742
|
+
ensureColumn('source_state', 'summary_error', 'TEXT');
|
|
743
|
+
ensureColumn('session_file', 'parser_version', 'INTEGER NOT NULL DEFAULT 0');
|
|
744
|
+
ensureColumn('session_file', 'content_indexed', 'INTEGER NOT NULL DEFAULT 1');
|
|
745
|
+
|
|
746
|
+
// The first migration is deliberately a metadata-only copy. It reuses
|
|
747
|
+
// rows already present in session_file and never touches JSONL files or
|
|
748
|
+
// the message/FTS tables.
|
|
749
|
+
db.exec(`
|
|
750
|
+
INSERT OR IGNORE INTO session_summary(
|
|
751
|
+
source, file_path, session_id, project_name, project_display_name,
|
|
752
|
+
project_full_path, first_message, git_branch, provider, model,
|
|
753
|
+
size, mtime_ms, started_at, updated_at, summary_version, extra_json
|
|
754
|
+
)
|
|
755
|
+
SELECT source, file_path, session_id, project_name, project_display_name,
|
|
756
|
+
project_full_path, first_message, git_branch, provider, model,
|
|
757
|
+
size, mtime_ms, started_at, updated_at, ${SUMMARY_VERSION}, extra_json
|
|
758
|
+
FROM session_file
|
|
759
|
+
`);
|
|
593
760
|
}
|
|
594
761
|
|
|
595
762
|
function _initFts(db) {
|
|
@@ -600,6 +767,234 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
600
767
|
}
|
|
601
768
|
}
|
|
602
769
|
|
|
770
|
+
function _isSummaryFresh(source) {
|
|
771
|
+
const row = _getDb().prepare(
|
|
772
|
+
'SELECT summary_inventory_ms FROM source_state WHERE source = ?'
|
|
773
|
+
).get(source);
|
|
774
|
+
return Boolean(
|
|
775
|
+
row?.summary_inventory_ms
|
|
776
|
+
&& Date.now() - Number(row.summary_inventory_ms) < INDEX_INVENTORY_TTL_MS
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function _hasSummaryData(source) {
|
|
781
|
+
return Boolean(_getDb().prepare(
|
|
782
|
+
'SELECT 1 AS indexed FROM session_summary WHERE source = ? LIMIT 1'
|
|
783
|
+
).get(source));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function _summaryFromDescriptor(descriptor = {}) {
|
|
787
|
+
const projectName = descriptor.projectHint || descriptor.projectName || 'unknown';
|
|
788
|
+
return {
|
|
789
|
+
sessionId: String(descriptor.sessionId || ''),
|
|
790
|
+
projectName,
|
|
791
|
+
projectDisplayName: descriptor.projectDisplayName || projectName,
|
|
792
|
+
projectFullPath: descriptor.projectFullPath || descriptor.projectPath || null,
|
|
793
|
+
firstMessage: descriptor.firstMessage || null,
|
|
794
|
+
gitBranch: descriptor.gitBranch || null,
|
|
795
|
+
provider: descriptor.provider || null,
|
|
796
|
+
model: descriptor.model || null,
|
|
797
|
+
startedAt: descriptor.startedAt || null,
|
|
798
|
+
updatedAt: descriptor.updatedAt ?? descriptor.mtimeMs ?? null,
|
|
799
|
+
extraJson: JSON.stringify({ projectHint: descriptor.projectHint || null, mtimeMs: descriptor.mtimeMs ?? null })
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function _summarizeDescriptor(adapter, descriptor) {
|
|
804
|
+
if (typeof adapter?.summarize !== 'function') {
|
|
805
|
+
return _summaryFromDescriptor(descriptor);
|
|
806
|
+
}
|
|
807
|
+
const result = await adapter.summarize({ ...descriptor, projectsDir: explicitProjectsDir });
|
|
808
|
+
if (_isTypedFailureResult(result)) throw _typedFailureToError(result);
|
|
809
|
+
const summary = _normalizeRuntimeSummaryResult(result, descriptor) || _summaryFromDescriptor(descriptor);
|
|
810
|
+
if (!summary.sessionId) summary.sessionId = String(descriptor.sessionId || '');
|
|
811
|
+
return summary;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function _recordSummaryState(db, source, inventoryMs, error) {
|
|
815
|
+
db.prepare(`
|
|
816
|
+
INSERT INTO source_state(source, summary_inventory_ms, summary_error)
|
|
817
|
+
VALUES(?, ?, ?)
|
|
818
|
+
ON CONFLICT(source) DO UPDATE SET
|
|
819
|
+
summary_inventory_ms = excluded.summary_inventory_ms,
|
|
820
|
+
summary_error = excluded.summary_error
|
|
821
|
+
`).run(source, inventoryMs, error || null);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function _upsertSummary(db, source, descriptor, summary) {
|
|
825
|
+
const sessionId = String(summary.sessionId || descriptor.sessionId || '');
|
|
826
|
+
if (!sessionId) return;
|
|
827
|
+
db.prepare(
|
|
828
|
+
'DELETE FROM session_summary WHERE source = ? AND session_id = ? AND file_path <> ?'
|
|
829
|
+
).run(source, sessionId, descriptor.filePath);
|
|
830
|
+
db.prepare(`
|
|
831
|
+
INSERT INTO session_summary(
|
|
832
|
+
source, file_path, session_id, project_name, project_display_name,
|
|
833
|
+
project_full_path, first_message, git_branch, provider, model,
|
|
834
|
+
size, mtime_ms, started_at, updated_at, summary_version, extra_json
|
|
835
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
836
|
+
ON CONFLICT(source, file_path) DO UPDATE SET
|
|
837
|
+
session_id = excluded.session_id,
|
|
838
|
+
project_name = excluded.project_name,
|
|
839
|
+
project_display_name = excluded.project_display_name,
|
|
840
|
+
project_full_path = excluded.project_full_path,
|
|
841
|
+
first_message = excluded.first_message,
|
|
842
|
+
git_branch = excluded.git_branch,
|
|
843
|
+
provider = excluded.provider,
|
|
844
|
+
model = excluded.model,
|
|
845
|
+
size = excluded.size,
|
|
846
|
+
mtime_ms = excluded.mtime_ms,
|
|
847
|
+
started_at = excluded.started_at,
|
|
848
|
+
updated_at = excluded.updated_at,
|
|
849
|
+
summary_version = excluded.summary_version,
|
|
850
|
+
extra_json = excluded.extra_json
|
|
851
|
+
`).run(
|
|
852
|
+
source,
|
|
853
|
+
descriptor.filePath,
|
|
854
|
+
sessionId,
|
|
855
|
+
summary.projectName || 'unknown',
|
|
856
|
+
summary.projectDisplayName || summary.projectName || 'unknown',
|
|
857
|
+
summary.projectFullPath || null,
|
|
858
|
+
summary.firstMessage || null,
|
|
859
|
+
summary.gitBranch || null,
|
|
860
|
+
summary.provider || null,
|
|
861
|
+
summary.model || null,
|
|
862
|
+
Number(descriptor.size) || 0,
|
|
863
|
+
Number(descriptor.mtimeMs) || 0,
|
|
864
|
+
summary.startedAt || null,
|
|
865
|
+
summary.updatedAt ?? descriptor.mtimeMs ?? null,
|
|
866
|
+
SUMMARY_VERSION,
|
|
867
|
+
summary.extraJson || null
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function _runSummaryInventory(source, { force = false, config = indexConfig } = {}) {
|
|
872
|
+
// A list request must never wait behind the expensive full-content
|
|
873
|
+
// inventory lock. It can serve the previous summaries and expose the
|
|
874
|
+
// refreshing/stale state instead.
|
|
875
|
+
const inventoryLock = await _acquireInventoryLock(dbPath, source, 100);
|
|
876
|
+
if (!inventoryLock) return;
|
|
877
|
+
let db = null;
|
|
878
|
+
let errorMsg = null;
|
|
879
|
+
let stateToRecord = null;
|
|
880
|
+
try {
|
|
881
|
+
db = _getDb();
|
|
882
|
+
if (!force && _isSummaryFresh(source)) return;
|
|
883
|
+
const adapter = _resolveSessionsAdapter(source, config);
|
|
884
|
+
const inventoryResult = await adapter.inventory({
|
|
885
|
+
projectsDir: source === 'claude' ? config.projectsDir : explicitProjectsDir
|
|
886
|
+
});
|
|
887
|
+
if (_isTypedFailureResult(inventoryResult)) throw _typedFailureToError(inventoryResult);
|
|
888
|
+
|
|
889
|
+
const winners = new Map();
|
|
890
|
+
for (const descriptor of Array.isArray(inventoryResult) ? inventoryResult : []) {
|
|
891
|
+
if (!descriptor?.filePath) continue;
|
|
892
|
+
const current = winners.get(descriptor.sessionId || descriptor.filePath);
|
|
893
|
+
if (!current
|
|
894
|
+
|| Number(descriptor.mtimeMs) > Number(current.mtimeMs)
|
|
895
|
+
|| (Number(descriptor.mtimeMs) === Number(current.mtimeMs) && descriptor.filePath < current.filePath)) {
|
|
896
|
+
winners.set(descriptor.sessionId || descriptor.filePath, descriptor);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const indexed = new Map(db.prepare(`
|
|
901
|
+
SELECT file_path, size, mtime_ms, summary_version, session_id
|
|
902
|
+
FROM session_summary WHERE source = ?
|
|
903
|
+
`).all(source).map(row => [row.file_path, row]));
|
|
904
|
+
const activePaths = new Set();
|
|
905
|
+
const changed = [];
|
|
906
|
+
for (const descriptor of winners.values()) {
|
|
907
|
+
activePaths.add(descriptor.filePath);
|
|
908
|
+
const row = indexed.get(descriptor.filePath);
|
|
909
|
+
if (!row
|
|
910
|
+
|| Number(row.size) !== Number(descriptor.size)
|
|
911
|
+
|| Number(row.mtime_ms) !== Number(descriptor.mtimeMs)
|
|
912
|
+
|| Number(row.summary_version) !== SUMMARY_VERSION) {
|
|
913
|
+
changed.push(descriptor);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const summaries = new Array(changed.length);
|
|
918
|
+
let next = 0;
|
|
919
|
+
const summarizeWorker = async () => {
|
|
920
|
+
while (true) {
|
|
921
|
+
const index = next++;
|
|
922
|
+
if (index >= changed.length) return;
|
|
923
|
+
const descriptor = changed[index];
|
|
924
|
+
try {
|
|
925
|
+
summaries[index] = { descriptor, value: await _summarizeDescriptor(adapter, descriptor) };
|
|
926
|
+
} catch (error) {
|
|
927
|
+
summaries[index] = { descriptor, error };
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
await Promise.all(Array.from({
|
|
932
|
+
length: Math.min(SUMMARY_REFRESH_CONCURRENCY, changed.length)
|
|
933
|
+
}, summarizeWorker));
|
|
934
|
+
|
|
935
|
+
db.exec('BEGIN IMMEDIATE');
|
|
936
|
+
try {
|
|
937
|
+
for (const row of indexed.values()) {
|
|
938
|
+
if (!activePaths.has(row.file_path)) {
|
|
939
|
+
db.prepare('DELETE FROM session_summary WHERE source = ? AND file_path = ?')
|
|
940
|
+
.run(source, row.file_path);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
for (const item of summaries) {
|
|
944
|
+
if (item?.error) {
|
|
945
|
+
const detail = `${item.descriptor.filePath}: ${item.error.message}`;
|
|
946
|
+
errorMsg = errorMsg ? `${errorMsg}; ${detail}` : detail;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (item?.value) _upsertSummary(db, source, item.descriptor, item.value);
|
|
950
|
+
}
|
|
951
|
+
db.exec('COMMIT');
|
|
952
|
+
} catch (error) {
|
|
953
|
+
try { db.exec('ROLLBACK'); } catch (_) {}
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
956
|
+
stateToRecord = { inventoryMs: Date.now(), error: errorMsg };
|
|
957
|
+
} catch (error) {
|
|
958
|
+
errorMsg = error?.message || String(error);
|
|
959
|
+
stateToRecord = { inventoryMs: null, error: errorMsg };
|
|
960
|
+
// Summary refresh is best-effort. Keep the previous lightweight rows
|
|
961
|
+
// available when a directory changes underneath inventory (or SQLite
|
|
962
|
+
// is temporarily busy); callers can inspect source meta for stale/error
|
|
963
|
+
// state and retry on the next refresh.
|
|
964
|
+
return null;
|
|
965
|
+
} finally {
|
|
966
|
+
if (stateToRecord && db) {
|
|
967
|
+
try { _recordSummaryState(db, source, stateToRecord.inventoryMs, stateToRecord.error); } catch (_) {}
|
|
968
|
+
}
|
|
969
|
+
_releaseInventoryLock(inventoryLock);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function ensureSummaryIndexed(source, options = {}) {
|
|
974
|
+
const consistency = options.consistency || 'stale-ok';
|
|
975
|
+
const force = options.force === true;
|
|
976
|
+
const key = `summary:${source}`;
|
|
977
|
+
if (!force && _isSummaryFresh(source)) return;
|
|
978
|
+
if (_summaryInflight.has(key)) {
|
|
979
|
+
if (consistency === 'complete' || !_hasSummaryData(source)) return _summaryInflight.get(key);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
const promise = _runSummaryInventory(source, {
|
|
983
|
+
force,
|
|
984
|
+
config: options.config || indexConfig
|
|
985
|
+
});
|
|
986
|
+
_summaryInflight.set(key, promise);
|
|
987
|
+
promise.finally(() => {
|
|
988
|
+
if (_summaryInflight.get(key) === promise) _summaryInflight.delete(key);
|
|
989
|
+
}).catch(() => {});
|
|
990
|
+
if (consistency === 'complete') return promise;
|
|
991
|
+
if (_hasSummaryData(source)) return;
|
|
992
|
+
await Promise.race([
|
|
993
|
+
promise,
|
|
994
|
+
new Promise(resolve => setTimeout(resolve, INDEX_COLD_WAIT_MS))
|
|
995
|
+
]);
|
|
996
|
+
}
|
|
997
|
+
|
|
603
998
|
/**
|
|
604
999
|
* @param {string} source
|
|
605
1000
|
* @param {{ force?: boolean, consistency?: string, allowStaleData?: boolean }} [options]
|
|
@@ -609,9 +1004,12 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
609
1004
|
const consistency = options.consistency || 'stale-ok';
|
|
610
1005
|
const force = options.force === true;
|
|
611
1006
|
const allowStaleData = options.allowStaleData === true;
|
|
612
|
-
const
|
|
1007
|
+
const projectName = typeof options.projectName === 'string' && options.projectName.trim()
|
|
1008
|
+
? options.projectName.trim()
|
|
1009
|
+
: null;
|
|
1010
|
+
const key = `ensure:${source}:${projectName || '*'}`;
|
|
613
1011
|
|
|
614
|
-
if (!force &&
|
|
1012
|
+
if (!force && _isSourceFreshForScope(source, projectName)) {
|
|
615
1013
|
return;
|
|
616
1014
|
}
|
|
617
1015
|
|
|
@@ -630,13 +1028,23 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
630
1028
|
|
|
631
1029
|
const useWorker = shouldUseWorker && !runtimeProvided && !explicitAdapters;
|
|
632
1030
|
const workerOptions = { force };
|
|
1031
|
+
if (projectName) workerOptions.projectName = projectName;
|
|
633
1032
|
const workerProjectsDir = source === 'claude'
|
|
634
1033
|
? (options.config?.projectsDir || explicitProjectsDir)
|
|
635
1034
|
: explicitProjectsDir;
|
|
636
1035
|
if (workerProjectsDir) workerOptions.projectsDir = workerProjectsDir;
|
|
637
1036
|
const promise = useWorker
|
|
638
|
-
? workerRunner(source, dbPath, workerOptions)
|
|
639
|
-
|
|
1037
|
+
? workerRunner(source, dbPath, workerOptions).then(() => {
|
|
1038
|
+
sourceFreshness.set(projectName ? `${source}:${projectName}` : source, projectName
|
|
1039
|
+
? { kind: 'project', projectName, at: Date.now() }
|
|
1040
|
+
: { kind: 'source', at: Date.now() });
|
|
1041
|
+
return undefined;
|
|
1042
|
+
})
|
|
1043
|
+
: _runInventory(source, {
|
|
1044
|
+
force,
|
|
1045
|
+
projectName,
|
|
1046
|
+
config: options.config || indexConfig
|
|
1047
|
+
});
|
|
640
1048
|
_inflight.set(key, promise);
|
|
641
1049
|
promise.finally(() => {
|
|
642
1050
|
if (_inflight.get(key) === promise) {
|
|
@@ -683,7 +1091,7 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
683
1091
|
).run(source, lastInventoryMs, lastError);
|
|
684
1092
|
}
|
|
685
1093
|
|
|
686
|
-
async function _runInventory(source, { force = false, config = indexConfig } = {}) {
|
|
1094
|
+
async function _runInventory(source, { force = false, projectName = null, config = indexConfig } = {}) {
|
|
687
1095
|
// SQLite's writer lock is database-wide, so source-specific lock files do
|
|
688
1096
|
// not prevent two different sources from entering BEGIN IMMEDIATE at the
|
|
689
1097
|
// same time. Acquire one lock for the whole index before opening/schema
|
|
@@ -701,7 +1109,7 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
701
1109
|
|
|
702
1110
|
try {
|
|
703
1111
|
db = _getDb();
|
|
704
|
-
if (!force &&
|
|
1112
|
+
if (!force && _isSourceFreshForScope(source, projectName)) {
|
|
705
1113
|
return;
|
|
706
1114
|
}
|
|
707
1115
|
let adapter = null;
|
|
@@ -731,17 +1139,21 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
731
1139
|
}
|
|
732
1140
|
|
|
733
1141
|
const indexedFiles = new Map();
|
|
734
|
-
for (const row of db.prepare('SELECT file_path, size, mtime_ms, session_id, parser_version FROM session_file WHERE source = ?').all(source)) {
|
|
1142
|
+
for (const row of db.prepare('SELECT file_path, size, mtime_ms, session_id, project_name, parser_version FROM session_file WHERE source = ?').all(source)) {
|
|
735
1143
|
indexedFiles.set(row.file_path, {
|
|
736
1144
|
size: row.size,
|
|
737
1145
|
mtime_ms: row.mtime_ms,
|
|
738
1146
|
sessionId: row.session_id,
|
|
1147
|
+
projectName: row.project_name,
|
|
739
1148
|
parserVersion: row.parser_version
|
|
740
1149
|
});
|
|
741
1150
|
}
|
|
742
1151
|
|
|
743
1152
|
const winnersBySessionId = new Map();
|
|
744
|
-
|
|
1153
|
+
const scopedInventory = projectName
|
|
1154
|
+
? inventoryResult.filter(descriptor => descriptor?.projectHint === projectName)
|
|
1155
|
+
: inventoryResult;
|
|
1156
|
+
for (const descriptor of scopedInventory) {
|
|
745
1157
|
const current = winnersBySessionId.get(descriptor.sessionId);
|
|
746
1158
|
if (!current
|
|
747
1159
|
|| descriptor.mtimeMs > current.mtimeMs
|
|
@@ -752,7 +1164,14 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
752
1164
|
|
|
753
1165
|
const toParse = [];
|
|
754
1166
|
const activePaths = new Set();
|
|
755
|
-
|
|
1167
|
+
// Inventory adapters may return directory order. Parse changed files in
|
|
1168
|
+
// recency order so a bounded search warms the newest conversations
|
|
1169
|
+
// first, matching the result ordering used by the query layer.
|
|
1170
|
+
const orderedDescriptors = [...winnersBySessionId.values()].sort((left, right) =>
|
|
1171
|
+
Number(right.mtimeMs || 0) - Number(left.mtimeMs || 0)
|
|
1172
|
+
|| String(left.filePath).localeCompare(String(right.filePath))
|
|
1173
|
+
);
|
|
1174
|
+
for (const d of orderedDescriptors) {
|
|
756
1175
|
activePaths.add(d.filePath);
|
|
757
1176
|
const idx = indexedFiles.get(d.filePath);
|
|
758
1177
|
if (!idx
|
|
@@ -819,11 +1238,15 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
819
1238
|
content, timestamp, model, provider, user_message_number, extra_json
|
|
820
1239
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
821
1240
|
`);
|
|
1241
|
+
const statements = { deletePath, deleteSession, insertFile, insertMessage, db };
|
|
822
1242
|
|
|
823
1243
|
db.exec('BEGIN IMMEDIATE');
|
|
824
1244
|
try {
|
|
825
|
-
for (const filePath of indexedFiles.
|
|
826
|
-
if (!activePaths.has(filePath))
|
|
1245
|
+
for (const [filePath, row] of indexedFiles.entries()) {
|
|
1246
|
+
if ((!projectName || row.projectName === projectName) && !activePaths.has(filePath)) {
|
|
1247
|
+
deletePath.run(source, filePath);
|
|
1248
|
+
db.prepare('DELETE FROM session_summary WHERE source = ? AND file_path = ?').run(source, filePath);
|
|
1249
|
+
}
|
|
827
1250
|
}
|
|
828
1251
|
db.exec('COMMIT');
|
|
829
1252
|
} catch (error) {
|
|
@@ -863,7 +1286,7 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
863
1286
|
for (const item of parsed) {
|
|
864
1287
|
if (item?.value) {
|
|
865
1288
|
const { descriptor, parseResult } = item.value;
|
|
866
|
-
_insertSession(
|
|
1289
|
+
_insertSession(statements, source, descriptor, parseResult.session, parseResult.messages, parserVersion);
|
|
867
1290
|
}
|
|
868
1291
|
}
|
|
869
1292
|
db.exec('COMMIT');
|
|
@@ -877,9 +1300,12 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
877
1300
|
}
|
|
878
1301
|
|
|
879
1302
|
stateToRecord = {
|
|
880
|
-
lastInventoryMs: Date.now(),
|
|
1303
|
+
lastInventoryMs: projectName ? null : Date.now(),
|
|
881
1304
|
lastError: errorMsg
|
|
882
1305
|
};
|
|
1306
|
+
sourceFreshness.set(projectName ? `${source}:${projectName}` : source, projectName
|
|
1307
|
+
? { kind: 'project', projectName, at: Date.now() }
|
|
1308
|
+
: { kind: 'source', at: Date.now() });
|
|
883
1309
|
} catch (err) {
|
|
884
1310
|
errorMsg = err && err.message ? err.message : String(err);
|
|
885
1311
|
stateToRecord = {
|
|
@@ -890,7 +1316,7 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
890
1316
|
} finally {
|
|
891
1317
|
if (stateToRecord) {
|
|
892
1318
|
try {
|
|
893
|
-
if (db) {
|
|
1319
|
+
if (db && !projectName) {
|
|
894
1320
|
_recordSourceState(db, source, stateToRecord.lastInventoryMs, stateToRecord.lastError);
|
|
895
1321
|
}
|
|
896
1322
|
} catch (_err) {}
|
|
@@ -924,6 +1350,7 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
924
1350
|
session.extraJson || null,
|
|
925
1351
|
parserVersion
|
|
926
1352
|
);
|
|
1353
|
+
_upsertSummary(statements.db || _getDb(), source, descriptor, session);
|
|
927
1354
|
|
|
928
1355
|
let ordinal = 0;
|
|
929
1356
|
for (const msg of messages) {
|
|
@@ -950,89 +1377,311 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
950
1377
|
|
|
951
1378
|
// ---- Public query methods ----
|
|
952
1379
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1380
|
+
function _projectRowsToPayload(rows, source, page, limit, total) {
|
|
1381
|
+
return {
|
|
1382
|
+
projects: rows.map(r => ({
|
|
1383
|
+
name: r.project_name,
|
|
1384
|
+
displayName: r.project_display_name || r.project_name,
|
|
1385
|
+
fullPath: r.project_full_path || '',
|
|
1386
|
+
path: r.project_full_path || '',
|
|
1387
|
+
sessionCount: Number(r.session_count) || 0,
|
|
1388
|
+
lastUsed: r.last_used,
|
|
1389
|
+
latestSession: r.latest_session,
|
|
1390
|
+
source
|
|
1391
|
+
})),
|
|
1392
|
+
currentProject: null,
|
|
1393
|
+
pagination: {
|
|
1394
|
+
page,
|
|
1395
|
+
limit,
|
|
1396
|
+
total,
|
|
1397
|
+
hasMore: page * limit < total
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function _sessionRowToSummary(r) {
|
|
1403
|
+
const extra = _safeParseObject(r.extra_json);
|
|
1404
|
+
return {
|
|
1405
|
+
sessionId: r.session_id,
|
|
1406
|
+
filePath: r.file_path,
|
|
1407
|
+
size: Number(r.size) || 0,
|
|
1408
|
+
mtime: r.mtime_ms,
|
|
1409
|
+
firstMessage: r.first_message,
|
|
1410
|
+
gitBranch: r.git_branch,
|
|
1411
|
+
provider: r.provider,
|
|
1412
|
+
model: r.model,
|
|
1413
|
+
messageCount: r.message_count == null ? null : Number(r.message_count),
|
|
1414
|
+
tokens: _safeParseJson(r.usage_json, null),
|
|
1415
|
+
extra,
|
|
1416
|
+
source: r.source,
|
|
1417
|
+
projectName: r.project_name,
|
|
1418
|
+
projectHint: extra.projectHint || r.project_name,
|
|
1419
|
+
projectDisplayName: r.project_display_name,
|
|
1420
|
+
projectFullPath: r.project_full_path,
|
|
1421
|
+
startedAt: r.started_at,
|
|
1422
|
+
updatedAt: r.updated_at
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
async function listProjectsPage(source, options = {}) {
|
|
1427
|
+
await ensureSummaryIndexed(source, {
|
|
960
1428
|
force: options.force === true,
|
|
961
1429
|
consistency: options.consistency || 'stale-ok',
|
|
962
|
-
allowStaleData: true,
|
|
963
1430
|
config: options.config
|
|
964
1431
|
});
|
|
965
1432
|
const db = _getDb();
|
|
966
|
-
|
|
1433
|
+
const { page, limit, query } = _normalizeListOpts(options);
|
|
1434
|
+
const pattern = `%${query.replace(/[%_]/g, ch => `\\${ch}`)}%`;
|
|
1435
|
+
const queryClause = query
|
|
1436
|
+
? `AND (project_name LIKE ? ESCAPE '\\' OR project_display_name LIKE ? ESCAPE '\\' OR project_full_path LIKE ? ESCAPE '\\')`
|
|
1437
|
+
: '';
|
|
1438
|
+
const queryParams = query ? [pattern, pattern, pattern] : [];
|
|
1439
|
+
const total = Number(db.prepare(`
|
|
1440
|
+
SELECT COUNT(*) AS total FROM (
|
|
1441
|
+
SELECT project_name FROM session_summary
|
|
1442
|
+
WHERE source = ? ${queryClause}
|
|
1443
|
+
GROUP BY project_name
|
|
1444
|
+
)
|
|
1445
|
+
`).get(source, ...queryParams)?.total) || 0;
|
|
967
1446
|
const rows = db.prepare(`
|
|
968
|
-
SELECT project_name, project_display_name
|
|
1447
|
+
SELECT project_name, MAX(project_display_name) AS project_display_name,
|
|
1448
|
+
MAX(project_full_path) AS project_full_path,
|
|
969
1449
|
COUNT(*) AS session_count,
|
|
970
1450
|
MAX(updated_at) AS last_used,
|
|
971
|
-
(SELECT session_id FROM
|
|
1451
|
+
(SELECT session_id FROM session_summary sf2
|
|
972
1452
|
WHERE sf2.source = ? AND sf2.project_name = sf.project_name
|
|
973
|
-
ORDER BY updated_at DESC LIMIT 1) AS latest_session
|
|
974
|
-
FROM
|
|
975
|
-
WHERE source = ?
|
|
1453
|
+
ORDER BY updated_at DESC, session_id ASC LIMIT 1) AS latest_session
|
|
1454
|
+
FROM session_summary sf
|
|
1455
|
+
WHERE source = ? ${queryClause}
|
|
976
1456
|
GROUP BY project_name
|
|
977
|
-
ORDER BY last_used DESC
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
return rows
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
source
|
|
1457
|
+
ORDER BY last_used DESC, project_name ASC
|
|
1458
|
+
LIMIT ? OFFSET ?
|
|
1459
|
+
`).all(source, source, ...queryParams, limit, (page - 1) * limit);
|
|
1460
|
+
return _projectRowsToPayload(rows, source, page, limit, total);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
async function getAllProjects(source, options = {}) {
|
|
1464
|
+
const projects = [];
|
|
1465
|
+
let page = 1;
|
|
1466
|
+
const limit = 100;
|
|
1467
|
+
while (true) {
|
|
1468
|
+
const payload = await listProjectsPage(source, {
|
|
1469
|
+
...options,
|
|
1470
|
+
page,
|
|
1471
|
+
limit,
|
|
1472
|
+
// A forced refresh is only needed once. Repeating it for every page
|
|
1473
|
+
// would serialize needless metadata scans.
|
|
1474
|
+
force: page === 1 ? options.force === true : false
|
|
1475
|
+
});
|
|
1476
|
+
projects.push(...(Array.isArray(payload.projects) ? payload.projects : []));
|
|
1477
|
+
if (!payload.pagination?.hasMore) break;
|
|
1478
|
+
page += 1;
|
|
1479
|
+
}
|
|
1480
|
+
return projects;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
async function listProjects(source, options = {}) {
|
|
1484
|
+
return getAllProjects(source, options);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
async function listSessionsPage(source, projectName, options = {}) {
|
|
1488
|
+
await ensureSummaryIndexed(source, {
|
|
1489
|
+
force: options.force === true,
|
|
1490
|
+
consistency: options.consistency || 'stale-ok',
|
|
1491
|
+
config: options.config
|
|
1492
|
+
});
|
|
1493
|
+
const db = _getDb();
|
|
1494
|
+
const { page, limit, query } = _normalizeListOpts(options);
|
|
1495
|
+
const pattern = `%${query.replace(/[%_]/g, ch => `\\${ch}`)}%`;
|
|
1496
|
+
const queryClause = query
|
|
1497
|
+
? `AND (session_id LIKE ? ESCAPE '\\' OR first_message LIKE ? ESCAPE '\\' OR git_branch LIKE ? ESCAPE '\\')`
|
|
1498
|
+
: '';
|
|
1499
|
+
const queryParams = query ? [pattern, pattern, pattern] : [];
|
|
1500
|
+
const countRow = db.prepare(`
|
|
1501
|
+
SELECT COUNT(*) AS total, COALESCE(SUM(size), 0) AS total_size
|
|
1502
|
+
FROM session_summary
|
|
1503
|
+
WHERE source = ? AND project_name = ? ${queryClause}
|
|
1504
|
+
`).get(source, projectName, ...queryParams);
|
|
1505
|
+
const total = Number(countRow?.total) || 0;
|
|
1506
|
+
const totalSize = Number(countRow?.total_size) || 0;
|
|
1507
|
+
const rows = db.prepare(`
|
|
1508
|
+
SELECT s.*,
|
|
1509
|
+
f.message_count AS content_message_count,
|
|
1510
|
+
f.usage_json AS content_usage_json,
|
|
1511
|
+
f.extra_json AS content_extra_json
|
|
1512
|
+
FROM session_summary s
|
|
1513
|
+
LEFT JOIN session_file f ON f.source = s.source AND f.session_id = s.session_id
|
|
1514
|
+
WHERE s.source = ? AND s.project_name = ? ${queryClause}
|
|
1515
|
+
ORDER BY s.updated_at DESC, s.session_id ASC
|
|
1516
|
+
LIMIT ? OFFSET ?
|
|
1517
|
+
`).all(source, projectName, ...queryParams, limit, (page - 1) * limit);
|
|
1518
|
+
const sessions = rows.map(r => _sessionRowToSummary({
|
|
1519
|
+
...r,
|
|
1520
|
+
session_id: r.session_id,
|
|
1521
|
+
file_path: r.file_path,
|
|
1522
|
+
size: r.size,
|
|
1523
|
+
mtime_ms: r.mtime_ms,
|
|
1524
|
+
first_message: r.first_message,
|
|
1525
|
+
git_branch: r.git_branch,
|
|
1526
|
+
provider: r.provider,
|
|
1527
|
+
model: r.model,
|
|
1528
|
+
project_name: r.project_name,
|
|
1529
|
+
project_display_name: r.project_display_name,
|
|
1530
|
+
project_full_path: r.project_full_path,
|
|
1531
|
+
started_at: r.started_at,
|
|
1532
|
+
updated_at: r.updated_at,
|
|
1533
|
+
source: r.source,
|
|
1534
|
+
extra_json: r.content_extra_json || r.extra_json,
|
|
1535
|
+
message_count: r.content_message_count,
|
|
1536
|
+
usage_json: r.content_usage_json
|
|
989
1537
|
}));
|
|
1538
|
+
return {
|
|
1539
|
+
sessions,
|
|
1540
|
+
totalSize,
|
|
1541
|
+
projectInfo: { sessionCount: total, totalSize },
|
|
1542
|
+
pagination: { page, limit, total, hasMore: page * limit < total }
|
|
1543
|
+
};
|
|
990
1544
|
}
|
|
991
1545
|
|
|
992
|
-
/**
|
|
993
|
-
* @param {string} source
|
|
994
|
-
* @param {string} projectName
|
|
995
|
-
* @param {object} [options]
|
|
996
|
-
* @returns {Promise<Array>}
|
|
997
|
-
*/
|
|
998
1546
|
async function listSessions(source, projectName, options = {}) {
|
|
1547
|
+
// Keep the historical internal API stable for commands and callers that
|
|
1548
|
+
// explicitly request the complete index. HTTP list routes use
|
|
1549
|
+
// listSessionsPage above and therefore never enter this path.
|
|
999
1550
|
await ensureSourceIndexed(source, {
|
|
1000
1551
|
force: options.force === true,
|
|
1001
1552
|
consistency: options.consistency || 'stale-ok',
|
|
1002
1553
|
config: options.config
|
|
1003
1554
|
});
|
|
1004
|
-
|
|
1005
1555
|
const db = _getDb();
|
|
1006
1556
|
const rows = db.prepare(`
|
|
1007
1557
|
SELECT * FROM session_file
|
|
1008
1558
|
WHERE source = ? AND project_name = ?
|
|
1009
|
-
ORDER BY updated_at DESC
|
|
1559
|
+
ORDER BY updated_at DESC, session_id ASC
|
|
1010
1560
|
`).all(source, projectName);
|
|
1561
|
+
return rows.map(_sessionRowToSummary);
|
|
1562
|
+
}
|
|
1011
1563
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1564
|
+
async function _parseSingleSessionContent(source, summaryRow, config = indexConfig) {
|
|
1565
|
+
const adapter = _resolveSessionsAdapter(source, config);
|
|
1566
|
+
const descriptor = {
|
|
1567
|
+
filePath: summaryRow.file_path,
|
|
1568
|
+
size: Number(summaryRow.size) || 0,
|
|
1569
|
+
mtimeMs: Number(summaryRow.mtime_ms) || 0,
|
|
1570
|
+
sessionId: summaryRow.session_id,
|
|
1571
|
+
projectHint: summaryRow.project_name,
|
|
1572
|
+
projectsDir: source === 'claude' ? config.projectsDir : explicitProjectsDir
|
|
1573
|
+
};
|
|
1574
|
+
const parseResult = await adapter.parse(descriptor);
|
|
1575
|
+
if (_isTypedFailureResult(parseResult)) throw _typedFailureToError(parseResult);
|
|
1576
|
+
if (!parseResult?.session || typeof parseResult.session !== 'object'
|
|
1577
|
+
|| typeof parseResult.session.sessionId !== 'string'
|
|
1578
|
+
|| !parseResult.session.sessionId.trim()
|
|
1579
|
+
|| !Array.isArray(parseResult.messages)) {
|
|
1580
|
+
throw new Error('invalid parsed session result');
|
|
1581
|
+
}
|
|
1582
|
+
return { descriptor, parseResult };
|
|
1583
|
+
}
|
|
1014
1584
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1585
|
+
/**
|
|
1586
|
+
* Parse and index exactly one file for detail requests. This map is shared
|
|
1587
|
+
* by outline, messages, and status callers so opening a session while the
|
|
1588
|
+
* drawer fires concurrent requests cannot duplicate the expensive parse.
|
|
1589
|
+
*/
|
|
1590
|
+
async function ensureSessionContentIndexed(source, sessionId, options = {}) {
|
|
1591
|
+
const key = `content:${source}:${sessionId}`;
|
|
1592
|
+
if (_contentInflight.has(key)) return _contentInflight.get(key);
|
|
1593
|
+
const promise = (async () => {
|
|
1594
|
+
await ensureSummaryIndexed(source, {
|
|
1595
|
+
force: options.force === true,
|
|
1596
|
+
consistency: 'complete',
|
|
1597
|
+
config: options.config || indexConfig
|
|
1598
|
+
});
|
|
1599
|
+
const db = _getDb();
|
|
1600
|
+
const summaryRow = db.prepare(
|
|
1601
|
+
'SELECT * FROM session_summary WHERE source = ? AND session_id = ?'
|
|
1602
|
+
).get(source, sessionId);
|
|
1603
|
+
if (!summaryRow) return null;
|
|
1604
|
+
|
|
1605
|
+
let stat;
|
|
1606
|
+
try {
|
|
1607
|
+
stat = await fs.promises.stat(summaryRow.file_path);
|
|
1608
|
+
} catch (_) {
|
|
1609
|
+
db.prepare('DELETE FROM session_summary WHERE source = ? AND file_path = ?')
|
|
1610
|
+
.run(source, summaryRow.file_path);
|
|
1611
|
+
db.prepare('DELETE FROM session_file WHERE source = ? AND file_path = ?')
|
|
1612
|
+
.run(source, summaryRow.file_path);
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const current = db.prepare(`
|
|
1617
|
+
SELECT * FROM session_file
|
|
1618
|
+
WHERE source = ? AND session_id = ? AND file_path = ?
|
|
1619
|
+
`).get(source, sessionId, summaryRow.file_path);
|
|
1620
|
+
if (current
|
|
1621
|
+
&& Number(current.size) === Number(stat.size)
|
|
1622
|
+
&& Number(current.mtime_ms) === Number(stat.mtimeMs)
|
|
1623
|
+
&& Number(current.content_indexed ?? 1) === 1) {
|
|
1624
|
+
return current;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
const parsed = await _parseSingleSessionContent(source, {
|
|
1628
|
+
...summaryRow,
|
|
1629
|
+
size: stat.size,
|
|
1630
|
+
mtime_ms: stat.mtimeMs
|
|
1631
|
+
}, options.config || indexConfig);
|
|
1632
|
+
let afterStat;
|
|
1633
|
+
try {
|
|
1634
|
+
afterStat = await fs.promises.stat(summaryRow.file_path);
|
|
1635
|
+
} catch (_) {
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
if (afterStat.size !== stat.size || afterStat.mtimeMs !== stat.mtimeMs) {
|
|
1639
|
+
// A growing session is left as a stale summary. The next detail
|
|
1640
|
+
// request retries it after the writer has produced a stable file.
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
const deletePath = db.prepare('DELETE FROM session_file WHERE source = ? AND file_path = ?');
|
|
1645
|
+
const deleteSession = db.prepare('DELETE FROM session_file WHERE source = ? AND session_id = ?');
|
|
1646
|
+
const insertFile = db.prepare(`
|
|
1647
|
+
INSERT INTO session_file(
|
|
1648
|
+
source, file_path, size, mtime_ms, session_id,
|
|
1649
|
+
project_name, project_display_name, project_full_path,
|
|
1650
|
+
first_message, git_branch, provider, model,
|
|
1651
|
+
started_at, updated_at, message_count, usage_json, extra_json,
|
|
1652
|
+
parser_version
|
|
1653
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1654
|
+
`);
|
|
1655
|
+
const insertMessage = db.prepare(`
|
|
1656
|
+
INSERT INTO session_message(
|
|
1657
|
+
source, session_id, ordinal, message_id, role, type, subtype,
|
|
1658
|
+
content, timestamp, model, provider, user_message_number, extra_json
|
|
1659
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1660
|
+
`);
|
|
1661
|
+
db.exec('BEGIN IMMEDIATE');
|
|
1662
|
+
try {
|
|
1663
|
+
_insertSession(
|
|
1664
|
+
{ deletePath, deleteSession, insertFile, insertMessage, db },
|
|
1665
|
+
source,
|
|
1666
|
+
{ ...parsed.descriptor, size: afterStat.size, mtimeMs: afterStat.mtimeMs },
|
|
1667
|
+
parsed.parseResult.session,
|
|
1668
|
+
parsed.parseResult.messages,
|
|
1669
|
+
_parserVersionForSource(source, _resolveSessionsAdapter(source, options.config || indexConfig))
|
|
1670
|
+
);
|
|
1671
|
+
db.exec('COMMIT');
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
try { db.exec('ROLLBACK'); } catch (_) {}
|
|
1674
|
+
throw error;
|
|
1675
|
+
}
|
|
1676
|
+
return db.prepare(
|
|
1677
|
+
'SELECT * FROM session_file WHERE source = ? AND session_id = ?'
|
|
1678
|
+
).get(source, parsed.parseResult.session.sessionId);
|
|
1679
|
+
})();
|
|
1680
|
+
_contentInflight.set(key, promise);
|
|
1681
|
+
promise.finally(() => {
|
|
1682
|
+
if (_contentInflight.get(key) === promise) _contentInflight.delete(key);
|
|
1683
|
+
}).catch(() => {});
|
|
1684
|
+
return promise;
|
|
1036
1685
|
}
|
|
1037
1686
|
|
|
1038
1687
|
/**
|
|
@@ -1042,10 +1691,10 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1042
1691
|
* @returns {Promise<{sessionId, lastModified, size, filePath}|null>}
|
|
1043
1692
|
*/
|
|
1044
1693
|
async function getSessionStatus(source, sessionId, options = {}) {
|
|
1045
|
-
await _ensureSessionCurrent(source, sessionId);
|
|
1694
|
+
await _ensureSessionCurrent(source, sessionId, options);
|
|
1046
1695
|
const db = _getDb();
|
|
1047
1696
|
const row = db.prepare(
|
|
1048
|
-
'SELECT session_id, mtime_ms, size, file_path FROM
|
|
1697
|
+
'SELECT session_id, mtime_ms, size, file_path FROM session_summary WHERE source = ? AND session_id = ?'
|
|
1049
1698
|
).get(source, sessionId);
|
|
1050
1699
|
if (!row) return null;
|
|
1051
1700
|
|
|
@@ -1064,7 +1713,7 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1064
1713
|
* @returns {Promise<{sessionId, items: Array}|null>}
|
|
1065
1714
|
*/
|
|
1066
1715
|
async function getSessionOutline(source, sessionId, options = {}) {
|
|
1067
|
-
await
|
|
1716
|
+
await ensureSessionContentIndexed(source, sessionId, options);
|
|
1068
1717
|
const db = _getDb();
|
|
1069
1718
|
|
|
1070
1719
|
const rows = db.prepare(`
|
|
@@ -1098,7 +1747,7 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1098
1747
|
* @returns {Promise<{messages: Array, metadata: object, pagination: object}|null>}
|
|
1099
1748
|
*/
|
|
1100
1749
|
async function getMessagePage(source, sessionId, options = {}) {
|
|
1101
|
-
await
|
|
1750
|
+
await ensureSessionContentIndexed(source, sessionId, options);
|
|
1102
1751
|
const db = _getDb();
|
|
1103
1752
|
const { page, limit, order } = _normalizePageOpts(options);
|
|
1104
1753
|
|
|
@@ -1153,7 +1802,7 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1153
1802
|
* @returns {Promise<Array>}
|
|
1154
1803
|
*/
|
|
1155
1804
|
async function getRecentSessions(source, limit = 5, options = {}) {
|
|
1156
|
-
await
|
|
1805
|
+
await ensureSummaryIndexed(source, {
|
|
1157
1806
|
force: options.force === true,
|
|
1158
1807
|
consistency: options.consistency || 'stale-ok',
|
|
1159
1808
|
config: options.config
|
|
@@ -1161,27 +1810,22 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1161
1810
|
const db = _getDb();
|
|
1162
1811
|
|
|
1163
1812
|
const rows = db.prepare(`
|
|
1164
|
-
SELECT
|
|
1165
|
-
|
|
1166
|
-
|
|
1813
|
+
SELECT s.*,
|
|
1814
|
+
f.message_count AS content_message_count,
|
|
1815
|
+
f.usage_json AS content_usage_json,
|
|
1816
|
+
f.extra_json AS content_extra_json
|
|
1817
|
+
FROM session_summary s
|
|
1818
|
+
LEFT JOIN session_file f ON f.source = s.source AND f.session_id = s.session_id
|
|
1819
|
+
WHERE s.source = ?
|
|
1820
|
+
ORDER BY s.updated_at DESC, s.session_id ASC
|
|
1167
1821
|
LIMIT ?
|
|
1168
1822
|
`).all(source, Math.max(1, Math.min(limit, 100)));
|
|
1169
1823
|
|
|
1170
|
-
return rows.map(r => ({
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
firstMessage: r.first_message,
|
|
1176
|
-
gitBranch: r.git_branch,
|
|
1177
|
-
provider: r.provider,
|
|
1178
|
-
model: r.model,
|
|
1179
|
-
messageCount: r.message_count,
|
|
1180
|
-
projectName: r.project_name,
|
|
1181
|
-
projectDisplayName: r.project_display_name,
|
|
1182
|
-
projectFullPath: r.project_full_path,
|
|
1183
|
-
source: r.source,
|
|
1184
|
-
updatedAt: r.updated_at
|
|
1824
|
+
return rows.map(r => _sessionRowToSummary({
|
|
1825
|
+
...r,
|
|
1826
|
+
message_count: r.content_message_count,
|
|
1827
|
+
usage_json: r.content_usage_json,
|
|
1828
|
+
extra_json: r.content_extra_json || r.extra_json
|
|
1185
1829
|
}));
|
|
1186
1830
|
}
|
|
1187
1831
|
|
|
@@ -1193,14 +1837,18 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1193
1837
|
*/
|
|
1194
1838
|
async function searchSessions(source, keyword, options = {}) {
|
|
1195
1839
|
if (!String(keyword || '').trim()) return [];
|
|
1840
|
+
const projectName = typeof options.projectName === 'string' && options.projectName.trim()
|
|
1841
|
+
? options.projectName.trim()
|
|
1842
|
+
: null;
|
|
1843
|
+
const resultLimit = Math.max(1, Math.min(200, Number.parseInt(options.limit, 10) || 100));
|
|
1196
1844
|
await ensureSourceIndexed(source, {
|
|
1197
1845
|
force: options.force === true,
|
|
1198
1846
|
consistency: options.consistency || 'complete',
|
|
1847
|
+
projectName,
|
|
1199
1848
|
config: options.config
|
|
1200
1849
|
});
|
|
1201
1850
|
const db = _getDb();
|
|
1202
|
-
const contextLength = options.contextLength || 35;
|
|
1203
|
-
const projectName = options.projectName || null;
|
|
1851
|
+
const contextLength = Math.max(1, Math.min(200, Number(options.contextLength) || 35));
|
|
1204
1852
|
|
|
1205
1853
|
// Build candidate query
|
|
1206
1854
|
let candidates;
|
|
@@ -1209,26 +1857,38 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1209
1857
|
const needle = String(keyword);
|
|
1210
1858
|
if ([...needle].length >= 3) {
|
|
1211
1859
|
const ftsRows = db.prepare(`
|
|
1860
|
+
WITH matched_sessions AS (
|
|
1861
|
+
SELECT sm.source, sm.session_id, MAX(sf.updated_at) AS updated_at
|
|
1862
|
+
FROM session_message_fts fts
|
|
1863
|
+
JOIN session_message sm ON sm.rowid = fts.rowid
|
|
1864
|
+
JOIN session_file sf ON sf.source = sm.source AND sf.session_id = sm.session_id
|
|
1865
|
+
WHERE sm.source = ? AND session_message_fts MATCH ?
|
|
1866
|
+
${projectName ? 'AND sf.project_name = ?' : ''}
|
|
1867
|
+
GROUP BY sm.source, sm.session_id
|
|
1868
|
+
ORDER BY updated_at DESC, sm.session_id ASC
|
|
1869
|
+
LIMIT ?
|
|
1870
|
+
)
|
|
1212
1871
|
SELECT sm.source, sm.session_id, sm.ordinal, sm.content, sm.role, sm.type, sm.timestamp,
|
|
1213
1872
|
sf.project_name, sf.project_display_name, sf.project_full_path,
|
|
1214
1873
|
sf.file_path, sf.first_message, sf.updated_at
|
|
1215
1874
|
FROM session_message_fts fts
|
|
1216
1875
|
JOIN session_message sm ON sm.rowid = fts.rowid
|
|
1876
|
+
JOIN matched_sessions ms ON ms.source = sm.source AND ms.session_id = sm.session_id
|
|
1217
1877
|
JOIN session_file sf ON sf.source = sm.source AND sf.session_id = sm.session_id
|
|
1218
|
-
WHERE
|
|
1219
|
-
|
|
1220
|
-
ORDER BY sf.updated_at DESC, sm.ordinal ASC, sm.rowid ASC
|
|
1221
|
-
LIMIT 500
|
|
1878
|
+
WHERE session_message_fts MATCH ?
|
|
1879
|
+
ORDER BY ms.updated_at DESC, sm.ordinal ASC, sm.rowid ASC
|
|
1222
1880
|
`);
|
|
1223
1881
|
const params = [source, _ftsQuote(needle)];
|
|
1224
1882
|
if (projectName) params.push(projectName);
|
|
1883
|
+
params.push(resultLimit);
|
|
1884
|
+
params.push(_ftsQuote(needle));
|
|
1225
1885
|
candidates = ftsRows.all(...params);
|
|
1226
1886
|
} else {
|
|
1227
1887
|
// Short needle: scan all messages for this source
|
|
1228
|
-
candidates = _scanMessagesRelational(db, source, keyword, projectName);
|
|
1888
|
+
candidates = _scanMessagesRelational(db, source, keyword, projectName, resultLimit);
|
|
1229
1889
|
}
|
|
1230
1890
|
} else {
|
|
1231
|
-
candidates = _scanMessagesRelational(db, source, keyword, projectName);
|
|
1891
|
+
candidates = _scanMessagesRelational(db, source, keyword, projectName, resultLimit);
|
|
1232
1892
|
}
|
|
1233
1893
|
|
|
1234
1894
|
if (candidates.length === 0) return [];
|
|
@@ -1298,22 +1958,20 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1298
1958
|
});
|
|
1299
1959
|
}
|
|
1300
1960
|
|
|
1301
|
-
//
|
|
1961
|
+
// Search is a recency browser: matchCount remains useful metadata, but
|
|
1962
|
+
// should not push older conversations above newer ones.
|
|
1302
1963
|
results.sort((a, b) => {
|
|
1303
|
-
|
|
1304
|
-
const cm = b.matchCount - a.matchCount;
|
|
1305
|
-
if (cm !== 0) return cm;
|
|
1306
1964
|
const aRow = matchMap.get(a.sessionId);
|
|
1307
1965
|
const bRow = matchMap.get(b.sessionId);
|
|
1308
|
-
const au = (aRow && aRow.session.updated_at)
|
|
1309
|
-
const bu = (bRow && bRow.session.updated_at)
|
|
1310
|
-
return bu - au;
|
|
1966
|
+
const au = _timestampValue(aRow && aRow.session.updated_at);
|
|
1967
|
+
const bu = _timestampValue(bRow && bRow.session.updated_at);
|
|
1968
|
+
return bu - au || a.sessionId.localeCompare(b.sessionId);
|
|
1311
1969
|
});
|
|
1312
1970
|
|
|
1313
|
-
return results;
|
|
1971
|
+
return results.slice(0, resultLimit);
|
|
1314
1972
|
}
|
|
1315
1973
|
|
|
1316
|
-
function _scanMessagesRelational(db, source, keyword, projectName = null) {
|
|
1974
|
+
function _scanMessagesRelational(db, source, keyword, projectName = null, resultLimit = 100) {
|
|
1317
1975
|
const useSqlMatch = /^[\x00-\x7F]*$/.test(String(keyword));
|
|
1318
1976
|
const select = `
|
|
1319
1977
|
SELECT sm.source, sm.session_id, sm.ordinal, sm.content, sm.role, sm.type, sm.timestamp,
|
|
@@ -1323,13 +1981,37 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1323
1981
|
JOIN session_file sf ON sf.source = sm.source AND sf.session_id = sm.session_id
|
|
1324
1982
|
WHERE sm.source = ?
|
|
1325
1983
|
${projectName ? 'AND sf.project_name = ?' : ''}
|
|
1326
|
-
ORDER BY sf.updated_at DESC, sm.ordinal ASC, sm.rowid ASC
|
|
1327
1984
|
`;
|
|
1328
1985
|
|
|
1329
1986
|
if (useSqlMatch) {
|
|
1330
|
-
const sql =
|
|
1331
|
-
|
|
1987
|
+
const sql = `
|
|
1988
|
+
WITH matched_messages AS (
|
|
1989
|
+
SELECT sm.source, sm.session_id, sm.ordinal, sm.rowid AS message_rowid,
|
|
1990
|
+
sm.content, sm.role, sm.type, sm.timestamp,
|
|
1991
|
+
sf.project_name, sf.project_display_name, sf.project_full_path,
|
|
1992
|
+
sf.file_path, sf.first_message, sf.updated_at
|
|
1993
|
+
FROM session_message sm
|
|
1994
|
+
JOIN session_file sf ON sf.source = sm.source AND sf.session_id = sm.session_id
|
|
1995
|
+
WHERE sm.source = ?
|
|
1996
|
+
${projectName ? 'AND sf.project_name = ?' : ''}
|
|
1997
|
+
AND instr(lower(sm.content), lower(?)) > 0
|
|
1998
|
+
),
|
|
1999
|
+
matched_sessions AS (
|
|
2000
|
+
SELECT source, session_id, MAX(updated_at) AS updated_at
|
|
2001
|
+
FROM matched_messages
|
|
2002
|
+
GROUP BY source, session_id
|
|
2003
|
+
ORDER BY updated_at DESC, session_id ASC
|
|
2004
|
+
LIMIT ?
|
|
2005
|
+
)
|
|
2006
|
+
SELECT mm.source, mm.session_id, mm.ordinal, mm.content, mm.role, mm.type, mm.timestamp,
|
|
2007
|
+
mm.project_name, mm.project_display_name, mm.project_full_path,
|
|
2008
|
+
mm.file_path, mm.first_message, mm.updated_at
|
|
2009
|
+
FROM matched_messages mm
|
|
2010
|
+
JOIN matched_sessions ms ON ms.source = mm.source AND ms.session_id = mm.session_id
|
|
2011
|
+
ORDER BY ms.updated_at DESC, mm.ordinal ASC, mm.message_rowid ASC`;
|
|
2012
|
+
const params = [source];
|
|
1332
2013
|
if (projectName) params.push(projectName);
|
|
2014
|
+
params.push(keyword, Math.max(1, resultLimit));
|
|
1333
2015
|
return db.prepare(sql).all(...params);
|
|
1334
2016
|
}
|
|
1335
2017
|
|
|
@@ -1337,17 +2019,26 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1337
2019
|
// apply the same locale-aware JavaScript matcher used by searchSessions,
|
|
1338
2020
|
// collecting at most the 500 matching candidates (not 500 raw rows).
|
|
1339
2021
|
const candidates = [];
|
|
2022
|
+
const matchedSessionIds = new Set();
|
|
1340
2023
|
const pageSize = 500;
|
|
1341
2024
|
let offset = 0;
|
|
1342
|
-
|
|
2025
|
+
let stop = false;
|
|
2026
|
+
while (!stop) {
|
|
1343
2027
|
const params = [source];
|
|
1344
2028
|
if (projectName) params.push(projectName);
|
|
1345
2029
|
params.push(pageSize, offset);
|
|
1346
|
-
const batch = db.prepare(`${select}\nLIMIT ? OFFSET ?`).all(...params);
|
|
2030
|
+
const batch = db.prepare(`${select}\nORDER BY sf.updated_at DESC, sm.ordinal ASC, sm.rowid ASC\nLIMIT ? OFFSET ?`).all(...params);
|
|
1347
2031
|
if (batch.length === 0) break;
|
|
1348
2032
|
for (const row of batch) {
|
|
1349
|
-
if (_containsLocaleMatch(row.content, keyword))
|
|
1350
|
-
if (
|
|
2033
|
+
if (!_containsLocaleMatch(row.content, keyword)) continue;
|
|
2034
|
+
if (!matchedSessionIds.has(row.session_id)) {
|
|
2035
|
+
if (matchedSessionIds.size >= resultLimit) {
|
|
2036
|
+
stop = true;
|
|
2037
|
+
break;
|
|
2038
|
+
}
|
|
2039
|
+
matchedSessionIds.add(row.session_id);
|
|
2040
|
+
}
|
|
2041
|
+
candidates.push(row);
|
|
1351
2042
|
}
|
|
1352
2043
|
if (batch.length < pageSize) break;
|
|
1353
2044
|
offset += batch.length;
|
|
@@ -1393,12 +2084,30 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1393
2084
|
/**
|
|
1394
2085
|
* Ensure the indexed data for a session is current by checking file mtime.
|
|
1395
2086
|
*/
|
|
1396
|
-
async function _ensureSessionCurrent(source, sessionId) {
|
|
1397
|
-
await ensureSourceIndexed(source, { consistency: 'stale-ok' });
|
|
2087
|
+
async function _ensureSessionCurrent(source, sessionId, options = {}) {
|
|
1398
2088
|
const db = _getDb();
|
|
1399
|
-
|
|
1400
|
-
'SELECT file_path, size, mtime_ms FROM
|
|
2089
|
+
let row = db.prepare(
|
|
2090
|
+
'SELECT file_path, size, mtime_ms FROM session_summary WHERE source = ? AND session_id = ?'
|
|
1401
2091
|
).get(source, sessionId);
|
|
2092
|
+
if (!row) {
|
|
2093
|
+
const fullState = db.prepare(
|
|
2094
|
+
'SELECT last_inventory_ms FROM source_state WHERE source = ?'
|
|
2095
|
+
).get(source);
|
|
2096
|
+
const fullIndexIsFresh = fullState?.last_inventory_ms
|
|
2097
|
+
&& Date.now() - Number(fullState.last_inventory_ms) < INDEX_INVENTORY_TTL_MS;
|
|
2098
|
+
// A completed legacy inventory with no matching session row is a
|
|
2099
|
+
// definitive miss (including malformed files). Do not start a second
|
|
2100
|
+
// summary scan merely because this status probe is lightweight.
|
|
2101
|
+
if (fullIndexIsFresh && options.force !== true) return;
|
|
2102
|
+
await ensureSummaryIndexed(source, {
|
|
2103
|
+
force: options.force === true,
|
|
2104
|
+
consistency: 'complete',
|
|
2105
|
+
config: options.config || indexConfig
|
|
2106
|
+
});
|
|
2107
|
+
row = db.prepare(
|
|
2108
|
+
'SELECT file_path, size, mtime_ms FROM session_summary WHERE source = ? AND session_id = ?'
|
|
2109
|
+
).get(source, sessionId);
|
|
2110
|
+
}
|
|
1402
2111
|
if (!row) return;
|
|
1403
2112
|
|
|
1404
2113
|
const fileKey = `${source}:${row.file_path}`;
|
|
@@ -1418,6 +2127,7 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1418
2127
|
currentStat = await fs.promises.stat(row.file_path);
|
|
1419
2128
|
} catch (_) {
|
|
1420
2129
|
fileVersions.set(fileKey, { size: -1, mtimeMs: -1, checkedAt: Date.now(), missing: true });
|
|
2130
|
+
db.prepare('DELETE FROM session_summary WHERE source = ? AND file_path = ?').run(source, row.file_path);
|
|
1421
2131
|
db.prepare('DELETE FROM session_file WHERE source = ? AND file_path = ?').run(source, row.file_path);
|
|
1422
2132
|
return;
|
|
1423
2133
|
}
|
|
@@ -1428,7 +2138,11 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1428
2138
|
checkedAt: Date.now()
|
|
1429
2139
|
});
|
|
1430
2140
|
if (currentStat.size !== row.size || currentStat.mtimeMs !== row.mtime_ms) {
|
|
1431
|
-
await
|
|
2141
|
+
await ensureSummaryIndexed(source, {
|
|
2142
|
+
force: true,
|
|
2143
|
+
consistency: 'complete',
|
|
2144
|
+
config: options.config || indexConfig
|
|
2145
|
+
});
|
|
1432
2146
|
}
|
|
1433
2147
|
})();
|
|
1434
2148
|
fileChecks.set(fileKey, check);
|
|
@@ -1449,32 +2163,45 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1449
2163
|
|
|
1450
2164
|
if (options.deleted && options.sessionId) {
|
|
1451
2165
|
db.prepare('DELETE FROM session_file WHERE source = ? AND session_id = ?').run(source, options.sessionId);
|
|
2166
|
+
db.prepare('DELETE FROM session_summary WHERE source = ? AND session_id = ?').run(source, options.sessionId);
|
|
1452
2167
|
}
|
|
1453
2168
|
if (options.projectName) {
|
|
1454
2169
|
db.prepare('DELETE FROM session_file WHERE source = ? AND project_name = ?').run(source, options.projectName);
|
|
2170
|
+
db.prepare('DELETE FROM session_summary WHERE source = ? AND project_name = ?').run(source, options.projectName);
|
|
1455
2171
|
}
|
|
1456
2172
|
|
|
1457
2173
|
// Mark state as stale
|
|
1458
2174
|
db.prepare(
|
|
1459
2175
|
'UPDATE source_state SET last_inventory_ms = NULL WHERE source = ?'
|
|
1460
2176
|
).run(source);
|
|
2177
|
+
db.prepare(
|
|
2178
|
+
'UPDATE source_state SET summary_inventory_ms = NULL, summary_error = NULL WHERE source = ?'
|
|
2179
|
+
).run(source);
|
|
1461
2180
|
for (const key of fileVersions.keys()) {
|
|
1462
2181
|
if (key.startsWith(`${source}:`)) fileVersions.delete(key);
|
|
1463
2182
|
}
|
|
2183
|
+
for (const key of sourceFreshness.keys()) {
|
|
2184
|
+
if (key === source || key.startsWith(`${source}:`)) sourceFreshness.delete(key);
|
|
2185
|
+
}
|
|
1464
2186
|
}
|
|
1465
2187
|
function getSourceIndexMeta(source) {
|
|
1466
2188
|
const row = _getDb().prepare(
|
|
1467
|
-
'SELECT last_inventory_ms, last_error FROM source_state WHERE source = ?'
|
|
2189
|
+
'SELECT last_inventory_ms, last_error, summary_inventory_ms, summary_error FROM source_state WHERE source = ?'
|
|
1468
2190
|
).get(source);
|
|
1469
|
-
const generatedAt = row?.
|
|
1470
|
-
|
|
1471
|
-
|
|
2191
|
+
const generatedAt = row?.summary_inventory_ms
|
|
2192
|
+
? Number(row.summary_inventory_ms)
|
|
2193
|
+
: (row?.last_inventory_ms ? Number(row.last_inventory_ms) : null);
|
|
2194
|
+
const error = row?.summary_error || row?.last_error || null;
|
|
2195
|
+
const stale = Boolean(error) || !generatedAt || Date.now() - generatedAt >= INDEX_INVENTORY_TTL_MS;
|
|
2196
|
+
const refreshing = _summaryInflight.has(`summary:${source}`)
|
|
2197
|
+
|| [..._inflight.keys()].some(key => key === `ensure:${source}:*` || key.startsWith(`ensure:${source}:`))
|
|
2198
|
+
|| _hasActiveInventoryLock(dbPath);
|
|
1472
2199
|
return {
|
|
1473
2200
|
generatedAt,
|
|
1474
2201
|
stale,
|
|
1475
2202
|
refreshing,
|
|
1476
|
-
fallback: Boolean(
|
|
1477
|
-
error
|
|
2203
|
+
fallback: Boolean(error && !_hasSummaryData(source)),
|
|
2204
|
+
error
|
|
1478
2205
|
};
|
|
1479
2206
|
}
|
|
1480
2207
|
function closeSessionHistoryIndex() {
|
|
@@ -1482,13 +2209,21 @@ function createSessionHistoryIndex(opts = {}) {
|
|
|
1482
2209
|
_db = null;
|
|
1483
2210
|
fileVersions.clear();
|
|
1484
2211
|
fileChecks.clear();
|
|
2212
|
+
_summaryInflight.clear();
|
|
2213
|
+
_contentInflight.clear();
|
|
2214
|
+
sourceFreshness.clear();
|
|
1485
2215
|
}
|
|
1486
2216
|
|
|
1487
2217
|
// Build the API object
|
|
1488
2218
|
const api = {
|
|
1489
2219
|
ensureSourceIndexed,
|
|
2220
|
+
ensureSummaryIndexed,
|
|
2221
|
+
ensureSessionContentIndexed,
|
|
2222
|
+
getAllProjects,
|
|
1490
2223
|
listProjects,
|
|
2224
|
+
listProjectsPage,
|
|
1491
2225
|
listSessions,
|
|
2226
|
+
listSessionsPage,
|
|
1492
2227
|
getSessionStatus,
|
|
1493
2228
|
getSessionOutline,
|
|
1494
2229
|
getMessagePage,
|
|
@@ -1529,10 +2264,30 @@ async function listProjects(source, options) {
|
|
|
1529
2264
|
return _getDefaultIndex(options).listProjects(source, options);
|
|
1530
2265
|
}
|
|
1531
2266
|
|
|
2267
|
+
async function ensureSummaryIndexed(source, options) {
|
|
2268
|
+
return _getDefaultIndex(options).ensureSummaryIndexed(source, options);
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
async function ensureSessionContentIndexed(source, sessionId, options) {
|
|
2272
|
+
return _getDefaultIndex(options).ensureSessionContentIndexed(source, sessionId, options);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
async function listProjectsPage(source, options) {
|
|
2276
|
+
return _getDefaultIndex(options).listProjectsPage(source, options);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
async function getAllProjects(source, options) {
|
|
2280
|
+
return _getDefaultIndex(options).getAllProjects(source, options);
|
|
2281
|
+
}
|
|
2282
|
+
|
|
1532
2283
|
async function listSessions(source, projectName, options) {
|
|
1533
2284
|
return _getDefaultIndex().listSessions(source, projectName, options);
|
|
1534
2285
|
}
|
|
1535
2286
|
|
|
2287
|
+
async function listSessionsPage(source, projectName, options) {
|
|
2288
|
+
return _getDefaultIndex().listSessionsPage(source, projectName, options);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
1536
2291
|
async function getSessionStatus(source, sessionId, options) {
|
|
1537
2292
|
return _getDefaultIndex().getSessionStatus(source, sessionId, options);
|
|
1538
2293
|
}
|
|
@@ -1578,8 +2333,13 @@ async function _defaultWorkerRunner(source, indexDbPath, options = {}) {
|
|
|
1578
2333
|
module.exports = {
|
|
1579
2334
|
createSessionHistoryIndex,
|
|
1580
2335
|
ensureSourceIndexed,
|
|
2336
|
+
ensureSummaryIndexed,
|
|
2337
|
+
ensureSessionContentIndexed,
|
|
2338
|
+
getAllProjects,
|
|
1581
2339
|
listProjects,
|
|
2340
|
+
listProjectsPage,
|
|
1582
2341
|
listSessions,
|
|
2342
|
+
listSessionsPage,
|
|
1583
2343
|
getSessionStatus,
|
|
1584
2344
|
getSessionOutline,
|
|
1585
2345
|
getMessagePage,
|