codesesh 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/{chunk-GCOAE7KI.js → chunk-VRVZJDNL.js} +1260 -1368
- package/dist/chunk-VRVZJDNL.js.map +1 -0
- package/dist/{dist-TZTL6VP4.js → dist-C3KC3XKJ.js} +25 -10
- package/dist/index.js +1772 -1241
- package/dist/index.js.map +1 -1
- package/dist/scan-refresh-worker.js +1 -1
- package/dist/search-index-worker.js +1 -1
- package/dist/smart-tag-worker.js +1 -1
- package/dist/web/assets/index-BNLxu3RT.js +113 -0
- package/dist/web/assets/index-BYElqfX-.css +2 -0
- package/dist/web/assets/markdown-dR4kAewZ.js +14 -0
- package/dist/web/assets/react-3acXkZNw.js +1821 -0
- package/dist/web/assets/syntax-C_RLNYQn.js +6 -0
- package/dist/web/assets/vendor-Lx2mL9a6.js +40 -0
- package/dist/web/index.html +6 -6
- package/package.json +9 -8
- package/dist/chunk-GCOAE7KI.js.map +0 -1
- package/dist/web/assets/index-D05hsCzb.css +0 -2
- package/dist/web/assets/index-DOy8q3la.js +0 -113
- package/dist/web/assets/markdown-CGVJZBE7.js +0 -14
- package/dist/web/assets/react-DkEeEGji.js +0 -1821
- package/dist/web/assets/syntax-CFQ-7fm6.js +0 -6
- package/dist/web/assets/vendor-B2nLfkMs.js +0 -80
- /package/dist/{dist-TZTL6VP4.js.map → dist-C3KC3XKJ.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
BookmarkStorageUnavailableError,
|
|
4
3
|
FileSystemSessionSource,
|
|
4
|
+
StateStorageUnavailableError,
|
|
5
5
|
attachMissingProjectIdentities,
|
|
6
6
|
buildAgentCacheMeta,
|
|
7
7
|
buildDashboard,
|
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
createProjectScopeMatcher,
|
|
12
12
|
createRegisteredAgents,
|
|
13
13
|
deleteBookmark,
|
|
14
|
+
deleteSessionAlias,
|
|
15
|
+
executeSessionSearch,
|
|
14
16
|
extractSessionFileActivity,
|
|
15
|
-
filterSessions,
|
|
16
17
|
getAgentInfoMap,
|
|
17
18
|
getAgentLastFullSyncAt,
|
|
18
19
|
getCursorDataPath,
|
|
@@ -22,33 +23,36 @@ import {
|
|
|
22
23
|
getTotalTokens,
|
|
23
24
|
importBookmarks,
|
|
24
25
|
isAgentCacheInitialized,
|
|
26
|
+
isProjectIdentityKind,
|
|
25
27
|
listBookmarks,
|
|
26
28
|
listCachedProjectGroups,
|
|
27
29
|
listFileActivity,
|
|
30
|
+
listSessionAliases,
|
|
28
31
|
listSessionFileActivity,
|
|
29
32
|
loadCachedSessionData,
|
|
30
33
|
loadCachedSessions,
|
|
31
34
|
markAgentFullSyncCompleted,
|
|
35
|
+
matchesProjectIdentity,
|
|
32
36
|
matchesProjectScope,
|
|
33
|
-
|
|
37
|
+
mergeSearchQueryOptions,
|
|
34
38
|
perf,
|
|
35
39
|
realFs,
|
|
36
40
|
refreshPricingCache,
|
|
37
41
|
resolveProviderRoots,
|
|
38
42
|
scanSessions,
|
|
39
|
-
searchFileActivitySessions,
|
|
40
|
-
searchSessions,
|
|
41
43
|
sessionSignature,
|
|
42
44
|
sortSessions,
|
|
43
45
|
startOfLocalDay,
|
|
44
|
-
upsertBookmark
|
|
45
|
-
|
|
46
|
+
upsertBookmark,
|
|
47
|
+
upsertSessionAlias
|
|
48
|
+
} from "./chunk-VRVZJDNL.js";
|
|
46
49
|
|
|
47
50
|
// src/index.ts
|
|
48
51
|
import { defineCommand, runMain } from "citty";
|
|
49
52
|
|
|
50
53
|
// src/server.ts
|
|
51
54
|
import { Hono as Hono2 } from "hono";
|
|
55
|
+
import { bodyLimit } from "hono/body-limit";
|
|
52
56
|
import { serve } from "@hono/node-server";
|
|
53
57
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
54
58
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -207,7 +211,131 @@ function logSearchIndexSync(context, result, data = {}) {
|
|
|
207
211
|
});
|
|
208
212
|
}
|
|
209
213
|
|
|
214
|
+
// src/time-window-resolution.ts
|
|
215
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
216
|
+
var DEFAULT_DASHBOARD_DAYS = 30;
|
|
217
|
+
function resolveTimeWindow(request) {
|
|
218
|
+
return request.mode === "cli" ? resolveCliWindow(request) : resolveDashboardWindow(request);
|
|
219
|
+
}
|
|
220
|
+
function resolveCliWindow(request) {
|
|
221
|
+
const now = request.now ?? Date.now();
|
|
222
|
+
const from = parseRequiredDate(request.from);
|
|
223
|
+
const to = parseRequiredDate(request.to);
|
|
224
|
+
if (from != null) return { from, to };
|
|
225
|
+
const days = parseDays(request.days);
|
|
226
|
+
if (days === 0) return { to, days };
|
|
227
|
+
if (days == null || days < 0) return { to };
|
|
228
|
+
const rollingFrom = now - days * DAY_MS;
|
|
229
|
+
return Number.isFinite(rollingFrom) ? { from: rollingFrom, to, days } : { to };
|
|
230
|
+
}
|
|
231
|
+
function resolveDashboardWindow(request) {
|
|
232
|
+
const now = request.now ?? Date.now();
|
|
233
|
+
const defaults = request.defaults ?? {};
|
|
234
|
+
const to = parseOptionalDate(request.query.to) ?? defaults.to ?? now;
|
|
235
|
+
const hasQueryDays = Boolean(request.query.days?.trim());
|
|
236
|
+
const parsedDays = hasQueryDays ? parseDays(request.query.days) : void 0;
|
|
237
|
+
let days = parsedDays != null && parsedDays > 0 ? parsedDays : defaults.days;
|
|
238
|
+
const queryFrom = parseOptionalDate(request.query.from);
|
|
239
|
+
if (queryFrom != null) {
|
|
240
|
+
days ??= elapsedDays(queryFrom, to);
|
|
241
|
+
return { from: queryFrom, to, days };
|
|
242
|
+
}
|
|
243
|
+
if (parsedDays === 0 || !hasQueryDays && defaults.days === 0) {
|
|
244
|
+
return { to, days: 0 };
|
|
245
|
+
}
|
|
246
|
+
if (defaults.from != null) {
|
|
247
|
+
days ??= elapsedDays(defaults.from, to);
|
|
248
|
+
return { from: defaults.from, to, days };
|
|
249
|
+
}
|
|
250
|
+
const resolvedDays = days != null && days > 0 ? days : DEFAULT_DASHBOARD_DAYS;
|
|
251
|
+
return {
|
|
252
|
+
from: startOfLocalDay(to) - (resolvedDays - 1) * DAY_MS,
|
|
253
|
+
to,
|
|
254
|
+
days: resolvedDays
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function parseRequiredDate(value) {
|
|
258
|
+
if (!value) return void 0;
|
|
259
|
+
const timestamp = new Date(value).getTime();
|
|
260
|
+
if (Number.isNaN(timestamp)) throw new Error(`Invalid date: ${value}`);
|
|
261
|
+
return timestamp;
|
|
262
|
+
}
|
|
263
|
+
function parseOptionalDate(value) {
|
|
264
|
+
if (value == null) return void 0;
|
|
265
|
+
const timestamp = new Date(value).getTime();
|
|
266
|
+
return Number.isNaN(timestamp) ? void 0 : timestamp;
|
|
267
|
+
}
|
|
268
|
+
function parseDays(value) {
|
|
269
|
+
if (value == null) return void 0;
|
|
270
|
+
const days = Number.parseInt(value, 10);
|
|
271
|
+
return Number.isSafeInteger(days) && Number.isSafeInteger(days * DAY_MS) ? days : void 0;
|
|
272
|
+
}
|
|
273
|
+
function elapsedDays(from, to) {
|
|
274
|
+
return Math.max(1, Math.ceil((to - from) / DAY_MS));
|
|
275
|
+
}
|
|
276
|
+
|
|
210
277
|
// src/api/handlers.ts
|
|
278
|
+
function getSessionAliasKey(agentKey, sessionId) {
|
|
279
|
+
return `${agentKey.toLowerCase()}\0${sessionId}`;
|
|
280
|
+
}
|
|
281
|
+
function getSessionAgentKey(session) {
|
|
282
|
+
return session.slug.split("/")[0]?.toLowerCase() ?? "";
|
|
283
|
+
}
|
|
284
|
+
function loadSessionAliasMap() {
|
|
285
|
+
try {
|
|
286
|
+
return new Map(
|
|
287
|
+
listSessionAliases().map((alias) => [
|
|
288
|
+
getSessionAliasKey(alias.agentKey, alias.sessionId),
|
|
289
|
+
alias.alias
|
|
290
|
+
])
|
|
291
|
+
);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (!(error instanceof StateStorageUnavailableError)) {
|
|
294
|
+
appLogger.warn("api.session_aliases.load_failed", {
|
|
295
|
+
error: error instanceof Error ? error.message : String(error)
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return /* @__PURE__ */ new Map();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function isStateStorageUnavailable(error) {
|
|
302
|
+
return error instanceof StateStorageUnavailableError;
|
|
303
|
+
}
|
|
304
|
+
function withDisplayTitle(session, agentKey, aliases) {
|
|
305
|
+
const alias = aliases.get(getSessionAliasKey(agentKey, session.id));
|
|
306
|
+
return alias ? { ...session, display_title: alias } : session;
|
|
307
|
+
}
|
|
308
|
+
function withBookmarkDisplayTitle(bookmark, aliases) {
|
|
309
|
+
const alias = aliases.get(getSessionAliasKey(bookmark.agentKey, bookmark.sessionId));
|
|
310
|
+
return alias ? { ...bookmark, display_title: alias } : bookmark;
|
|
311
|
+
}
|
|
312
|
+
function withFileActivityDisplayTitle(activity, aliases) {
|
|
313
|
+
return {
|
|
314
|
+
...activity,
|
|
315
|
+
session: withDisplayTitle(activity.session, activity.agent_name, aliases)
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function findAliasSearchResults(query, options, scanResult, aliases) {
|
|
319
|
+
const search = mergeSearchQueryOptions(query, options);
|
|
320
|
+
const needle = search.text.trim().toLowerCase();
|
|
321
|
+
if (!needle || aliases.size === 0) return [];
|
|
322
|
+
return executeSessionSearch(
|
|
323
|
+
"",
|
|
324
|
+
{ ...search.options, limit: Math.max(scanResult.sessions.length, 1) },
|
|
325
|
+
scanResult
|
|
326
|
+
).flatMap((result) => {
|
|
327
|
+
const alias = aliases.get(getSessionAliasKey(result.agentName, result.session.id));
|
|
328
|
+
if (!alias || !alias.toLowerCase().includes(needle)) return [];
|
|
329
|
+
return [
|
|
330
|
+
{
|
|
331
|
+
agentName: result.agentName,
|
|
332
|
+
session: withDisplayTitle(result.session, result.agentName, aliases),
|
|
333
|
+
snippet: `Alias \xB7 ${result.session.directory}`,
|
|
334
|
+
matchType: "title"
|
|
335
|
+
}
|
|
336
|
+
];
|
|
337
|
+
});
|
|
338
|
+
}
|
|
211
339
|
function isRecord(value) {
|
|
212
340
|
return typeof value === "object" && value !== null;
|
|
213
341
|
}
|
|
@@ -227,7 +355,7 @@ function parseBookmarkPayload(value) {
|
|
|
227
355
|
title: value.title,
|
|
228
356
|
directory: value.directory,
|
|
229
357
|
time_created: value.time_created,
|
|
230
|
-
time_updated: value.time_updated,
|
|
358
|
+
time_updated: value.time_updated ?? void 0,
|
|
231
359
|
stats: value.stats
|
|
232
360
|
};
|
|
233
361
|
}
|
|
@@ -265,13 +393,14 @@ function parseSmartTags(values) {
|
|
|
265
393
|
);
|
|
266
394
|
return tags.length > 0 ? [...new Set(tags)] : void 0;
|
|
267
395
|
}
|
|
268
|
-
function parseSearchOptions(c, defaults) {
|
|
396
|
+
function parseSearchOptions(c, defaults, projectIdentity) {
|
|
269
397
|
const params = searchParams(c);
|
|
270
398
|
const limitValue = parseNumberParam(params.get("limit") ?? void 0);
|
|
271
399
|
return {
|
|
272
400
|
agent: optionalQueryValue(params.get("agent") ?? void 0),
|
|
273
401
|
project: optionalQueryValue(params.get("project") ?? void 0),
|
|
274
|
-
|
|
402
|
+
projectKind: projectIdentity?.kind,
|
|
403
|
+
projectKey: projectIdentity?.key,
|
|
275
404
|
cwd: optionalQueryValue(params.get("cwd") ?? void 0),
|
|
276
405
|
tags: parseSmartTags(queryValues(params, "tag", "tags", "signal")),
|
|
277
406
|
tools: queryValues(params, "tool", "tools").map((tool) => tool.toLowerCase()),
|
|
@@ -286,9 +415,6 @@ function parseSearchOptions(c, defaults) {
|
|
|
286
415
|
limit: limitValue && limitValue > 0 ? Math.min(limitValue, 100) : 50
|
|
287
416
|
};
|
|
288
417
|
}
|
|
289
|
-
function filterSessionsByWindow(sessions, from, to) {
|
|
290
|
-
return filterSessionsByActivityWindow(sessions, from, to);
|
|
291
|
-
}
|
|
292
418
|
function filterSessionsByActivityWindow(sessions, from, to) {
|
|
293
419
|
if (from == null && to == null) return sessions;
|
|
294
420
|
return sessions.filter((session) => {
|
|
@@ -310,49 +436,6 @@ function sanitizeClientLogData(value) {
|
|
|
310
436
|
})
|
|
311
437
|
);
|
|
312
438
|
}
|
|
313
|
-
function sessionMatchesCostFilter(session, options) {
|
|
314
|
-
const cost = session.stats.total_cost;
|
|
315
|
-
if (options.costMin != null) {
|
|
316
|
-
if (options.costMinExclusive ? cost <= options.costMin : cost < options.costMin) return false;
|
|
317
|
-
}
|
|
318
|
-
if (options.costMax != null) {
|
|
319
|
-
if (options.costMaxExclusive ? cost >= options.costMax : cost > options.costMax) return false;
|
|
320
|
-
}
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
323
|
-
function mergeSearchLists(left, right) {
|
|
324
|
-
const values = [...left ?? [], ...right ?? []];
|
|
325
|
-
return values.length > 0 ? [...new Set(values)] : void 0;
|
|
326
|
-
}
|
|
327
|
-
function mergeSearchOptions(options, filters) {
|
|
328
|
-
return {
|
|
329
|
-
...options,
|
|
330
|
-
agent: options.agent ?? filters.agent,
|
|
331
|
-
project: options.project ?? filters.project,
|
|
332
|
-
projectKey: options.projectKey ?? filters.projectKey,
|
|
333
|
-
cwd: options.cwd ?? filters.cwd,
|
|
334
|
-
tags: mergeSearchLists(options.tags, filters.tags),
|
|
335
|
-
tools: mergeSearchLists(options.tools, filters.tools),
|
|
336
|
-
file: options.file ?? filters.file,
|
|
337
|
-
fileKind: options.fileKind ?? filters.fileKind,
|
|
338
|
-
costMin: options.costMin ?? filters.costMin,
|
|
339
|
-
costMax: options.costMax ?? filters.costMax,
|
|
340
|
-
costMinExclusive: options.costMinExclusive ?? filters.costMinExclusive,
|
|
341
|
-
costMaxExclusive: options.costMaxExclusive ?? filters.costMaxExclusive
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
function mergeSearchResults(results, limit) {
|
|
345
|
-
const seen = /* @__PURE__ */ new Set();
|
|
346
|
-
const merged = [];
|
|
347
|
-
for (const result of results) {
|
|
348
|
-
const key = `${result.agentName}/${result.session.id}`;
|
|
349
|
-
if (seen.has(key)) continue;
|
|
350
|
-
seen.add(key);
|
|
351
|
-
merged.push(result);
|
|
352
|
-
if (merged.length >= limit) break;
|
|
353
|
-
}
|
|
354
|
-
return merged;
|
|
355
|
-
}
|
|
356
439
|
function getProjectGroupKey(identityKind, identityKey) {
|
|
357
440
|
return `${identityKind}:${identityKey}`;
|
|
358
441
|
}
|
|
@@ -408,65 +491,35 @@ function attachProjectMetrics(projects, sessions) {
|
|
|
408
491
|
};
|
|
409
492
|
});
|
|
410
493
|
}
|
|
411
|
-
function matchesRecentSearchFilters(session, options, projectScope) {
|
|
412
|
-
if (options.projectKey && session.project_identity?.key !== options.projectKey) return false;
|
|
413
|
-
if (projectScope && !matchesProjectScope(session, projectScope)) return false;
|
|
414
|
-
if (options.project) {
|
|
415
|
-
const projectNeedle = options.project.toLowerCase();
|
|
416
|
-
const projectText = [
|
|
417
|
-
session.project_identity?.key,
|
|
418
|
-
session.project_identity?.displayName,
|
|
419
|
-
session.directory
|
|
420
|
-
].filter(Boolean).join("\n").toLowerCase();
|
|
421
|
-
if (!projectText.includes(projectNeedle)) return false;
|
|
422
|
-
}
|
|
423
|
-
if (options.tags?.length && !options.tags.every((tag) => session.smart_tags?.includes(tag))) {
|
|
424
|
-
return false;
|
|
425
|
-
}
|
|
426
|
-
if (!sessionMatchesCostFilter(session, options)) return false;
|
|
427
|
-
return true;
|
|
428
|
-
}
|
|
429
|
-
function recentSearchSessions(scanResult, options) {
|
|
430
|
-
const projectScope = options.cwd ? createProjectScopeMatcher(options.cwd) : null;
|
|
431
|
-
const entries = options.agent ? [[options.agent, scanResult.byAgent[options.agent] ?? []]] : Object.entries(scanResult.byAgent);
|
|
432
|
-
return entries.flatMap(
|
|
433
|
-
([agentName, sessions]) => filterSessionsByActivityWindow(sessions, options.from, options.to).filter((session) => matchesRecentSearchFilters(session, options, projectScope)).map((session) => ({ agentName, session }))
|
|
434
|
-
).toSorted(
|
|
435
|
-
(a, b) => (b.session.time_updated ?? b.session.time_created) - (a.session.time_updated ?? a.session.time_created)
|
|
436
|
-
).slice(0, options.limit).map(({ agentName, session }) => ({
|
|
437
|
-
agentName,
|
|
438
|
-
session,
|
|
439
|
-
snippet: `Recent session \xB7 ${session.directory}`,
|
|
440
|
-
matchType: "recent"
|
|
441
|
-
}));
|
|
442
|
-
}
|
|
443
494
|
function handleGetConfig(c, defaults) {
|
|
444
|
-
|
|
495
|
+
const payload = {
|
|
445
496
|
window: {
|
|
446
497
|
from: defaults.from,
|
|
447
498
|
to: defaults.to,
|
|
448
499
|
days: defaults.days
|
|
449
500
|
}
|
|
450
|
-
}
|
|
501
|
+
};
|
|
502
|
+
return c.json(payload);
|
|
451
503
|
}
|
|
452
504
|
function handleGetScanStatus(c, scanSource) {
|
|
453
505
|
return c.json(scanSource.getScanStatus());
|
|
454
506
|
}
|
|
455
507
|
function handleGetAgents(c, scanSource, defaults = {}) {
|
|
456
508
|
const scanResult = scanSource.getSnapshot();
|
|
457
|
-
const
|
|
509
|
+
const from = parseDateParam(c.req.query("from"), defaults.from);
|
|
510
|
+
const to = parseDateParam(c.req.query("to"), defaults.to);
|
|
458
511
|
const counts = Object.fromEntries(
|
|
459
512
|
Object.entries(scanResult.byAgent).map(([agentName, sessions]) => [
|
|
460
513
|
agentName,
|
|
461
|
-
|
|
514
|
+
filterSessionsByActivityWindow(sessions, from, to).length
|
|
462
515
|
])
|
|
463
516
|
);
|
|
464
|
-
|
|
465
|
-
return c.json(info);
|
|
517
|
+
return c.json(getAgentInfoMap(counts));
|
|
466
518
|
}
|
|
467
519
|
function handleGetProjects(c, scanSource, defaults = {}) {
|
|
468
520
|
const scanResult = scanSource.getSnapshot();
|
|
469
|
-
const
|
|
521
|
+
const from = parseDateParam(c.req.query("from"), defaults.from);
|
|
522
|
+
const to = parseDateParam(c.req.query("to"), defaults.to);
|
|
470
523
|
const sessions = filterSessionsByActivityWindow(scanResult.sessions, from, to);
|
|
471
524
|
return c.json({
|
|
472
525
|
projects: attachProjectMetrics(listCachedProjectGroups(sessions), sessions)
|
|
@@ -477,7 +530,13 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
477
530
|
const agent = c.req.query("agent");
|
|
478
531
|
const q = c.req.query("q")?.toLowerCase();
|
|
479
532
|
const cwd = c.req.query("cwd");
|
|
480
|
-
const
|
|
533
|
+
const projectIdentity = parseProjectIdentityFilter(
|
|
534
|
+
c.req.query("projectKind"),
|
|
535
|
+
c.req.query("projectKey")
|
|
536
|
+
);
|
|
537
|
+
if (projectIdentity === null) {
|
|
538
|
+
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
539
|
+
}
|
|
481
540
|
const tag = c.req.query("tag")?.toLowerCase();
|
|
482
541
|
const from = parseDateParam(c.req.query("from"), defaults.from);
|
|
483
542
|
const to = parseDateParam(c.req.query("to"), defaults.to);
|
|
@@ -487,8 +546,10 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
487
546
|
} else {
|
|
488
547
|
sessions = [...scanResult.sessions];
|
|
489
548
|
}
|
|
490
|
-
if (
|
|
491
|
-
sessions = sessions.filter(
|
|
549
|
+
if (projectIdentity) {
|
|
550
|
+
sessions = sessions.filter(
|
|
551
|
+
(session) => matchesProjectIdentity(session.project_identity, projectIdentity)
|
|
552
|
+
);
|
|
492
553
|
} else if (cwd) {
|
|
493
554
|
const projectScope = createProjectScopeMatcher(cwd);
|
|
494
555
|
sessions = sessions.filter((s) => matchesProjectScope(s, projectScope));
|
|
@@ -497,38 +558,41 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
497
558
|
if (tag) {
|
|
498
559
|
sessions = sessions.filter((s) => s.smart_tags?.includes(tag));
|
|
499
560
|
}
|
|
561
|
+
const aliases = loadSessionAliasMap();
|
|
500
562
|
if (q) {
|
|
501
|
-
sessions = sessions.filter((
|
|
563
|
+
sessions = sessions.filter((session) => {
|
|
564
|
+
const alias = aliases.get(getSessionAliasKey(getSessionAgentKey(session), session.id));
|
|
565
|
+
return session.title.toLowerCase().includes(q) || alias?.toLowerCase().includes(q);
|
|
566
|
+
});
|
|
502
567
|
}
|
|
503
|
-
return c.json({
|
|
568
|
+
return c.json({
|
|
569
|
+
sessions: sessions.map(
|
|
570
|
+
(session) => withDisplayTitle(session, getSessionAgentKey(session), aliases)
|
|
571
|
+
)
|
|
572
|
+
});
|
|
504
573
|
}
|
|
505
574
|
function handleSearchSessions(c, scanSource, defaults = {}) {
|
|
506
575
|
const query = c.req.query("q")?.trim() ?? "";
|
|
507
576
|
const scanResult = scanSource.getSnapshot();
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const textQuery = parsedQuery.text || (parsedQuery.hasQualifiers ? "" : query);
|
|
512
|
-
const needsIndexedSearch = Boolean(
|
|
513
|
-
textQuery || mergedSearchOptions.file || mergedSearchOptions.fileKind || mergedSearchOptions.tools?.length
|
|
577
|
+
const projectIdentity = parseProjectIdentityFilter(
|
|
578
|
+
c.req.query("projectKind"),
|
|
579
|
+
c.req.query("projectKey")
|
|
514
580
|
);
|
|
515
|
-
if (
|
|
516
|
-
return c.json({
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
581
|
+
if (projectIdentity === null) {
|
|
582
|
+
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
583
|
+
}
|
|
584
|
+
const searchOptions = parseSearchOptions(c, defaults, projectIdentity);
|
|
585
|
+
const aliases = loadSessionAliasMap();
|
|
586
|
+
const results = executeSessionSearch(query, searchOptions, scanResult).map((result) => ({
|
|
587
|
+
...result,
|
|
588
|
+
session: withDisplayTitle(result.session, result.agentName, aliases)
|
|
589
|
+
}));
|
|
590
|
+
const aliasResults = findAliasSearchResults(query, searchOptions, scanResult, aliases);
|
|
591
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
592
|
+
for (const result of [...aliasResults, ...results]) {
|
|
593
|
+
deduped.set(`${result.agentName}\0${result.session.id}`, result);
|
|
522
594
|
}
|
|
523
|
-
|
|
524
|
-
const results = mergeSearchResults(
|
|
525
|
-
[
|
|
526
|
-
...fileQuery ? searchFileActivitySessions(fileQuery, mergedSearchOptions) : [],
|
|
527
|
-
...searchSessions(query, mergedSearchOptions)
|
|
528
|
-
],
|
|
529
|
-
mergedSearchOptions.limit ?? 50
|
|
530
|
-
);
|
|
531
|
-
return c.json({ results });
|
|
595
|
+
return c.json({ results: [...deduped.values()].slice(0, searchOptions.limit ?? 50) });
|
|
532
596
|
}
|
|
533
597
|
function parseFileActivityKind(value) {
|
|
534
598
|
if (value === "read" || value === "edit" || value === "write" || value === "delete") {
|
|
@@ -540,14 +604,30 @@ function optionalQueryValue(value) {
|
|
|
540
604
|
const normalized = value?.trim();
|
|
541
605
|
return normalized ? normalized : void 0;
|
|
542
606
|
}
|
|
607
|
+
function parseProjectIdentityFilter(kindValue, keyValue) {
|
|
608
|
+
const kind = optionalQueryValue(kindValue);
|
|
609
|
+
const key = optionalQueryValue(keyValue);
|
|
610
|
+
if (!kind && !key) return void 0;
|
|
611
|
+
if (!kind || !key || !isProjectIdentityKind(kind)) return null;
|
|
612
|
+
return { kind, key };
|
|
613
|
+
}
|
|
543
614
|
function handleGetFileActivity(c, defaults = {}) {
|
|
544
615
|
const limitValue = Number(c.req.query("limit"));
|
|
545
616
|
const limit = Number.isFinite(limitValue) && limitValue > 0 ? Math.min(limitValue, 200) : 50;
|
|
617
|
+
const projectIdentity = parseProjectIdentityFilter(
|
|
618
|
+
c.req.query("projectKind"),
|
|
619
|
+
c.req.query("projectKey")
|
|
620
|
+
);
|
|
621
|
+
if (projectIdentity === null) {
|
|
622
|
+
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
623
|
+
}
|
|
624
|
+
const aliases = loadSessionAliasMap();
|
|
546
625
|
return c.json({
|
|
547
626
|
activity: listFileActivity({
|
|
548
627
|
agent: optionalQueryValue(c.req.query("agent")),
|
|
549
628
|
sessionId: optionalQueryValue(c.req.query("sessionId")),
|
|
550
|
-
|
|
629
|
+
projectKind: projectIdentity?.kind,
|
|
630
|
+
projectKey: projectIdentity?.key,
|
|
551
631
|
project: optionalQueryValue(c.req.query("project")),
|
|
552
632
|
cwd: optionalQueryValue(c.req.query("cwd")),
|
|
553
633
|
path: optionalQueryValue(c.req.query("path")),
|
|
@@ -555,7 +635,7 @@ function handleGetFileActivity(c, defaults = {}) {
|
|
|
555
635
|
from: parseDateParam(c.req.query("from"), defaults.from),
|
|
556
636
|
to: parseDateParam(c.req.query("to"), defaults.to),
|
|
557
637
|
limit
|
|
558
|
-
})
|
|
638
|
+
}).map((activity) => withFileActivityDisplayTitle(activity, aliases))
|
|
559
639
|
});
|
|
560
640
|
}
|
|
561
641
|
async function handleGetSessionData(c, scanSource) {
|
|
@@ -563,6 +643,9 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
563
643
|
const scanResult = scanSource.getSnapshot();
|
|
564
644
|
const agentName = c.req.param("agent");
|
|
565
645
|
const sessionId = c.req.param("id");
|
|
646
|
+
if (!agentName) {
|
|
647
|
+
return c.json({ error: "Missing agent name" }, 400);
|
|
648
|
+
}
|
|
566
649
|
if (!sessionId) {
|
|
567
650
|
return c.json({ error: "Missing session ID" }, 400);
|
|
568
651
|
}
|
|
@@ -599,8 +682,9 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
599
682
|
tag_duration_ms: Math.round(tagDuration),
|
|
600
683
|
duration_ms: Math.round(performance.now() - startedAt)
|
|
601
684
|
});
|
|
685
|
+
const aliases = loadSessionAliasMap();
|
|
602
686
|
return c.json({
|
|
603
|
-
...data,
|
|
687
|
+
...withDisplayTitle(data, agentName, aliases),
|
|
604
688
|
project_identity: projectIdentity,
|
|
605
689
|
smart_tags: smartTags,
|
|
606
690
|
smart_tags_source_updated_at: getSmartTagSourceTimestamp(data),
|
|
@@ -629,9 +713,13 @@ async function handlePostClientLog(c) {
|
|
|
629
713
|
}
|
|
630
714
|
function handleGetBookmarks(c) {
|
|
631
715
|
try {
|
|
632
|
-
|
|
716
|
+
const aliases = loadSessionAliasMap();
|
|
717
|
+
return c.json({
|
|
718
|
+
bookmarks: listBookmarks().map((bookmark) => withBookmarkDisplayTitle(bookmark, aliases)),
|
|
719
|
+
storageAvailable: true
|
|
720
|
+
});
|
|
633
721
|
} catch (error) {
|
|
634
|
-
if (error instanceof
|
|
722
|
+
if (error instanceof StateStorageUnavailableError) {
|
|
635
723
|
return c.json({ bookmarks: [], storageAvailable: false });
|
|
636
724
|
}
|
|
637
725
|
throw error;
|
|
@@ -645,7 +733,7 @@ async function handlePutBookmark(c) {
|
|
|
645
733
|
try {
|
|
646
734
|
return c.json({ bookmark: upsertBookmark(payload), storageAvailable: true });
|
|
647
735
|
} catch (error) {
|
|
648
|
-
if (error instanceof
|
|
736
|
+
if (error instanceof StateStorageUnavailableError) {
|
|
649
737
|
return c.json({ error: "Bookmark storage is unavailable" }, 503);
|
|
650
738
|
}
|
|
651
739
|
throw error;
|
|
@@ -663,7 +751,7 @@ async function handleImportBookmarks(c) {
|
|
|
663
751
|
try {
|
|
664
752
|
return c.json({ bookmarks: importBookmarks(bookmarks), storageAvailable: true });
|
|
665
753
|
} catch (error) {
|
|
666
|
-
if (error instanceof
|
|
754
|
+
if (error instanceof StateStorageUnavailableError) {
|
|
667
755
|
return c.json({ error: "Bookmark storage is unavailable" }, 503);
|
|
668
756
|
}
|
|
669
757
|
throw error;
|
|
@@ -679,49 +767,69 @@ function handleDeleteBookmark(c) {
|
|
|
679
767
|
deleteBookmark(agentKey, sessionId);
|
|
680
768
|
return c.json({ ok: true, storageAvailable: true });
|
|
681
769
|
} catch (error) {
|
|
682
|
-
if (error instanceof
|
|
770
|
+
if (error instanceof StateStorageUnavailableError) {
|
|
683
771
|
return c.json({ error: "Bookmark storage is unavailable" }, 503);
|
|
684
772
|
}
|
|
685
773
|
throw error;
|
|
686
774
|
}
|
|
687
775
|
}
|
|
688
|
-
function
|
|
689
|
-
const
|
|
690
|
-
const
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
776
|
+
async function handlePutSessionAlias(c) {
|
|
777
|
+
const agentKey = c.req.param("agent");
|
|
778
|
+
const sessionId = c.req.param("id");
|
|
779
|
+
const payload = await c.req.json().catch(() => null);
|
|
780
|
+
if (!agentKey || !sessionId || typeof payload?.alias !== "string") {
|
|
781
|
+
return c.json({ error: "Invalid session alias payload" }, 400);
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
return c.json({ alias: upsertSessionAlias(agentKey, sessionId, payload.alias) });
|
|
785
|
+
} catch (error) {
|
|
786
|
+
if (error instanceof TypeError) {
|
|
787
|
+
return c.json({ error: "Session alias must be non-empty and at most 160 characters" }, 400);
|
|
788
|
+
}
|
|
789
|
+
if (isStateStorageUnavailable(error)) {
|
|
790
|
+
return c.json({ error: "Session alias storage is unavailable" }, 503);
|
|
791
|
+
}
|
|
792
|
+
throw error;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function handleDeleteSessionAlias(c) {
|
|
796
|
+
const agentKey = c.req.param("agent");
|
|
797
|
+
const sessionId = c.req.param("id");
|
|
798
|
+
if (!agentKey || !sessionId) {
|
|
799
|
+
return c.json({ error: "Missing session alias identifier" }, 400);
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
deleteSessionAlias(agentKey, sessionId);
|
|
803
|
+
return c.json({ ok: true });
|
|
804
|
+
} catch (error) {
|
|
805
|
+
if (isStateStorageUnavailable(error)) {
|
|
806
|
+
return c.json({ error: "Session alias storage is unavailable" }, 503);
|
|
807
|
+
}
|
|
808
|
+
throw error;
|
|
710
809
|
}
|
|
711
|
-
return { from: fromTs, to: toTs, days };
|
|
712
810
|
}
|
|
713
811
|
function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
714
812
|
const scanResult = scanSource.getSnapshot();
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
c.req.query("
|
|
718
|
-
c.req.query("from"),
|
|
719
|
-
c.req.query("to")
|
|
813
|
+
const projectIdentity = parseProjectIdentityFilter(
|
|
814
|
+
c.req.query("projectKind"),
|
|
815
|
+
c.req.query("projectKey")
|
|
720
816
|
);
|
|
817
|
+
if (projectIdentity === null) {
|
|
818
|
+
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
819
|
+
}
|
|
820
|
+
const { from, to, days } = resolveTimeWindow({
|
|
821
|
+
mode: "dashboard",
|
|
822
|
+
query: {
|
|
823
|
+
days: c.req.query("days"),
|
|
824
|
+
from: c.req.query("from"),
|
|
825
|
+
to: c.req.query("to")
|
|
826
|
+
},
|
|
827
|
+
defaults
|
|
828
|
+
});
|
|
721
829
|
const scope = {
|
|
722
830
|
agent: optionalQueryValue(c.req.query("agent"))?.toLowerCase(),
|
|
723
|
-
projectKind:
|
|
724
|
-
projectKey:
|
|
831
|
+
projectKind: projectIdentity?.kind,
|
|
832
|
+
projectKey: projectIdentity?.key
|
|
725
833
|
};
|
|
726
834
|
const agentInfo = getAgentInfoMap({});
|
|
727
835
|
const agentInfoMap = new Map(agentInfo.map((a) => [a.name, a]));
|
|
@@ -736,6 +844,7 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
736
844
|
...aggregate,
|
|
737
845
|
recentFileActivities: listFileActivity({
|
|
738
846
|
agent: scope.agent,
|
|
847
|
+
projectKind: scope.projectKind,
|
|
739
848
|
projectKey: scope.projectKey,
|
|
740
849
|
from,
|
|
741
850
|
to,
|
|
@@ -743,16 +852,29 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
743
852
|
}),
|
|
744
853
|
window: { from, to, days }
|
|
745
854
|
};
|
|
746
|
-
|
|
855
|
+
const aliases = loadSessionAliasMap();
|
|
856
|
+
return c.json({
|
|
857
|
+
...data,
|
|
858
|
+
recentSessions: data.recentSessions.map(
|
|
859
|
+
(session) => withDisplayTitle(session, session.agentName, aliases)
|
|
860
|
+
),
|
|
861
|
+
recentFileActivities: data.recentFileActivities.map(
|
|
862
|
+
(activity) => withFileActivityDisplayTitle(activity, aliases)
|
|
863
|
+
)
|
|
864
|
+
});
|
|
747
865
|
}
|
|
748
866
|
|
|
749
867
|
// src/api/routes.ts
|
|
750
|
-
function createSseResponse(
|
|
868
|
+
function createSseResponse(eventSource, signal) {
|
|
751
869
|
const encoder = new TextEncoder();
|
|
870
|
+
let cancelStream = () => {
|
|
871
|
+
};
|
|
752
872
|
return new Response(
|
|
753
873
|
new ReadableStream({
|
|
754
874
|
start(controller) {
|
|
875
|
+
let isClosed = false;
|
|
755
876
|
const write = (event, data) => {
|
|
877
|
+
if (isClosed) return;
|
|
756
878
|
controller.enqueue(encoder.encode(`event: ${event}
|
|
757
879
|
`));
|
|
758
880
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
@@ -760,26 +882,36 @@ function createSseResponse(store, signal) {
|
|
|
760
882
|
`));
|
|
761
883
|
};
|
|
762
884
|
write("connected", { timestamp: Date.now() });
|
|
763
|
-
write("scan-status",
|
|
764
|
-
const unsubscribeSessions =
|
|
885
|
+
write("scan-status", eventSource.getScanStatus());
|
|
886
|
+
const unsubscribeSessions = eventSource.subscribe((event) => {
|
|
765
887
|
write(event.type, event);
|
|
766
888
|
});
|
|
767
|
-
const unsubscribeScanStatus =
|
|
889
|
+
const unsubscribeScanStatus = eventSource.subscribeScanStatus((event) => {
|
|
768
890
|
write(event.type, event);
|
|
769
891
|
});
|
|
770
892
|
const heartbeat = setInterval(() => {
|
|
771
|
-
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
|
893
|
+
if (!isClosed) controller.enqueue(encoder.encode(": keepalive\n\n"));
|
|
772
894
|
}, 15e3);
|
|
773
|
-
const
|
|
895
|
+
const cleanup = () => {
|
|
896
|
+
if (isClosed) return false;
|
|
897
|
+
isClosed = true;
|
|
774
898
|
clearInterval(heartbeat);
|
|
775
899
|
unsubscribeSessions();
|
|
776
900
|
unsubscribeScanStatus();
|
|
777
|
-
|
|
901
|
+
signal.removeEventListener("abort", abortStream);
|
|
902
|
+
return true;
|
|
903
|
+
};
|
|
904
|
+
const abortStream = () => {
|
|
905
|
+
if (cleanup()) controller.close();
|
|
906
|
+
};
|
|
907
|
+
cancelStream = () => {
|
|
908
|
+
cleanup();
|
|
778
909
|
};
|
|
779
|
-
signal.
|
|
910
|
+
if (signal.aborted) abortStream();
|
|
911
|
+
else signal.addEventListener("abort", abortStream, { once: true });
|
|
780
912
|
},
|
|
781
913
|
cancel() {
|
|
782
|
-
|
|
914
|
+
cancelStream();
|
|
783
915
|
}
|
|
784
916
|
}),
|
|
785
917
|
{
|
|
@@ -791,7 +923,7 @@ function createSseResponse(store, signal) {
|
|
|
791
923
|
}
|
|
792
924
|
);
|
|
793
925
|
}
|
|
794
|
-
function createApiRoutes(scanSource,
|
|
926
|
+
function createApiRoutes(scanSource, eventSource, options = {}) {
|
|
795
927
|
const api = new Hono();
|
|
796
928
|
const listDefaults = {
|
|
797
929
|
from: options.defaultSessionFrom,
|
|
@@ -799,8 +931,8 @@ function createApiRoutes(scanSource, store, options = {}) {
|
|
|
799
931
|
days: options.defaultSessionDays
|
|
800
932
|
};
|
|
801
933
|
api.get("/config", (c) => handleGetConfig(c, listDefaults));
|
|
802
|
-
if (
|
|
803
|
-
api.get("/status", (c) => handleGetScanStatus(c,
|
|
934
|
+
if (eventSource) {
|
|
935
|
+
api.get("/status", (c) => handleGetScanStatus(c, eventSource));
|
|
804
936
|
}
|
|
805
937
|
api.get("/agents", (c) => handleGetAgents(c, scanSource, listDefaults));
|
|
806
938
|
api.get("/projects", (c) => handleGetProjects(c, scanSource, listDefaults));
|
|
@@ -813,14 +945,55 @@ function createApiRoutes(scanSource, store, options = {}) {
|
|
|
813
945
|
api.put("/bookmarks", (c) => handlePutBookmark(c));
|
|
814
946
|
api.post("/bookmarks/import", (c) => handleImportBookmarks(c));
|
|
815
947
|
api.delete("/bookmarks/:agent/:id", (c) => handleDeleteBookmark(c));
|
|
948
|
+
api.put("/session-aliases/:agent/:id", (c) => handlePutSessionAlias(c));
|
|
949
|
+
api.delete("/session-aliases/:agent/:id", (c) => handleDeleteSessionAlias(c));
|
|
816
950
|
api.post("/logs", (c) => handlePostClientLog(c));
|
|
817
|
-
if (
|
|
818
|
-
api.get("/events", (c) => createSseResponse(
|
|
951
|
+
if (eventSource) {
|
|
952
|
+
api.get("/events", (c) => createSseResponse(eventSource, c.req.raw.signal));
|
|
819
953
|
}
|
|
820
954
|
return api;
|
|
821
955
|
}
|
|
822
956
|
|
|
957
|
+
// src/remote-access.ts
|
|
958
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
959
|
+
import { isIP } from "net";
|
|
960
|
+
var REMOTE_ACCESS_QUERY_PARAM = "access_token";
|
|
961
|
+
function createRemoteAccessToken() {
|
|
962
|
+
return randomBytes(32).toString("base64url");
|
|
963
|
+
}
|
|
964
|
+
function isLoopbackHostname(hostname) {
|
|
965
|
+
const normalized = hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
966
|
+
if (normalized === "localhost" || normalized === "::1") return true;
|
|
967
|
+
return isIP(normalized) === 4 && normalized.startsWith("127.");
|
|
968
|
+
}
|
|
969
|
+
function tokenMatches(actual, expected) {
|
|
970
|
+
if (!actual) return false;
|
|
971
|
+
const actualBuffer = Buffer.from(actual);
|
|
972
|
+
const expectedBuffer = Buffer.from(expected);
|
|
973
|
+
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
|
|
974
|
+
}
|
|
975
|
+
function bearerToken(c) {
|
|
976
|
+
const authorization = c.req.header("Authorization");
|
|
977
|
+
if (!authorization?.startsWith("Bearer ")) return void 0;
|
|
978
|
+
return authorization.slice("Bearer ".length);
|
|
979
|
+
}
|
|
980
|
+
function requestToken(c) {
|
|
981
|
+
const bearer = bearerToken(c);
|
|
982
|
+
if (bearer) return bearer;
|
|
983
|
+
if (c.req.method !== "GET") return void 0;
|
|
984
|
+
return c.req.query(REMOTE_ACCESS_QUERY_PARAM);
|
|
985
|
+
}
|
|
986
|
+
function remoteAccessAuth(expectedToken) {
|
|
987
|
+
return async (c, next) => {
|
|
988
|
+
if (!tokenMatches(requestToken(c), expectedToken)) {
|
|
989
|
+
return c.json({ error: "Remote access authentication required" }, 401);
|
|
990
|
+
}
|
|
991
|
+
await next();
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
823
995
|
// src/server.ts
|
|
996
|
+
var MAX_API_REQUEST_BYTES = 1024 * 1024;
|
|
824
997
|
function findWebDistPath() {
|
|
825
998
|
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
826
999
|
const packagedPath = resolve(__dirname2, "web");
|
|
@@ -862,6 +1035,14 @@ function getListeningPort(server, fallback) {
|
|
|
862
1035
|
}
|
|
863
1036
|
async function createServer(port, store, options = {}) {
|
|
864
1037
|
const app = new Hono2();
|
|
1038
|
+
const hostname = options.hostname ?? "127.0.0.1";
|
|
1039
|
+
const isLoopback = isLoopbackHostname(hostname);
|
|
1040
|
+
const remoteAccessToken = !isLoopback ? options.remoteAccessToken ?? (options.remoteAccess ? createRemoteAccessToken() : null) : null;
|
|
1041
|
+
if (!isLoopback && !remoteAccessToken) {
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
`Refusing to expose CodeSesh on ${hostname} without authentication. Add --remote-access to continue.`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
865
1046
|
app.use("*", async (c, next) => {
|
|
866
1047
|
const startedAt = performance.now();
|
|
867
1048
|
let thrown;
|
|
@@ -882,26 +1063,28 @@ async function createServer(port, store, options = {}) {
|
|
|
882
1063
|
});
|
|
883
1064
|
}
|
|
884
1065
|
});
|
|
1066
|
+
if (remoteAccessToken) {
|
|
1067
|
+
app.use("/api/*", remoteAccessAuth(remoteAccessToken));
|
|
1068
|
+
}
|
|
1069
|
+
app.use(
|
|
1070
|
+
"/api/*",
|
|
1071
|
+
bodyLimit({
|
|
1072
|
+
maxSize: MAX_API_REQUEST_BYTES,
|
|
1073
|
+
onError: (c) => c.json({ error: "Request body too large" }, 413)
|
|
1074
|
+
})
|
|
1075
|
+
);
|
|
885
1076
|
const routeOptions = {
|
|
886
1077
|
defaultSessionFrom: options.defaultSessionFrom,
|
|
887
1078
|
defaultSessionTo: options.defaultSessionTo,
|
|
888
1079
|
defaultSessionDays: options.defaultSessionDays
|
|
889
1080
|
};
|
|
890
|
-
app.route(
|
|
891
|
-
"/api",
|
|
892
|
-
createApiRoutes(
|
|
893
|
-
store,
|
|
894
|
-
"subscribe" in store ? store : void 0,
|
|
895
|
-
routeOptions
|
|
896
|
-
)
|
|
897
|
-
);
|
|
1081
|
+
app.route("/api", createApiRoutes(store, store, routeOptions));
|
|
898
1082
|
const webDistPath = findWebDistPath();
|
|
899
1083
|
if (webDistPath) {
|
|
900
1084
|
app.use("/*", serveStatic({ root: webDistPath }));
|
|
901
1085
|
app.get("/*", serveStatic({ root: webDistPath, path: "index.html" }));
|
|
902
1086
|
}
|
|
903
1087
|
const attempts = Math.max(1, options.portFallbackAttempts ?? 1);
|
|
904
|
-
const hostname = options.hostname ?? "127.0.0.1";
|
|
905
1088
|
let server = null;
|
|
906
1089
|
let actualPort = port;
|
|
907
1090
|
for (let offset = 0; offset < attempts; offset += 1) {
|
|
@@ -917,9 +1100,7 @@ async function createServer(port, store, options = {}) {
|
|
|
917
1100
|
if (isAddressInUse(error) && offset < attempts - 1) {
|
|
918
1101
|
continue;
|
|
919
1102
|
}
|
|
920
|
-
|
|
921
|
-
await store.shutdown();
|
|
922
|
-
}
|
|
1103
|
+
await store.shutdown();
|
|
923
1104
|
if (isAddressInUse(error) && attempts > 1) {
|
|
924
1105
|
throw new Error(
|
|
925
1106
|
`\u7AEF\u53E3 ${port}-${port + attempts - 1} \u5747\u5DF2\u88AB\u5360\u7528\uFF0C\u8BF7\u5173\u95ED\u73B0\u6709\u8FDB\u7A0B\u6216\u6539\u7528 --port \u6307\u5B9A\u5176\u4ED6\u7AEF\u53E3\u3002`
|
|
@@ -928,16 +1109,19 @@ async function createServer(port, store, options = {}) {
|
|
|
928
1109
|
throw new Error(getServerStartupErrorMessage(error, candidatePort));
|
|
929
1110
|
}
|
|
930
1111
|
}
|
|
931
|
-
const
|
|
932
|
-
const url =
|
|
933
|
-
appLogger.info("server.listen", {
|
|
1112
|
+
const baseUrl = isLoopback ? `http://localhost:${actualPort}` : `http://${hostname}:${actualPort}`;
|
|
1113
|
+
const url = remoteAccessToken ? `${baseUrl}/?${REMOTE_ACCESS_QUERY_PARAM}=${encodeURIComponent(remoteAccessToken)}` : baseUrl;
|
|
1114
|
+
appLogger.info("server.listen", {
|
|
1115
|
+
port: actualPort,
|
|
1116
|
+
requested_port: port,
|
|
1117
|
+
hostname,
|
|
1118
|
+
remote_access: Boolean(remoteAccessToken)
|
|
1119
|
+
});
|
|
934
1120
|
if (!isLoopback) {
|
|
935
|
-
appLogger.warn("server.listen.
|
|
936
|
-
console.warn(
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
`
|
|
940
|
-
);
|
|
1121
|
+
appLogger.warn("server.listen.remote_access", { hostname, port: actualPort });
|
|
1122
|
+
console.warn(`
|
|
1123
|
+
\u26A0 \u8FDC\u7A0B\u8BBF\u95EE\u5DF2\u542F\u7528\u3002\u4EFB\u4F55\u6301\u6709\u542F\u52A8 URL \u7684\u4EBA\u90FD\u53EF\u4EE5\u8BFB\u53D6\u4F60\u7684 AI \u4F1A\u8BDD\u8BB0\u5F55\u3002
|
|
1124
|
+
`);
|
|
941
1125
|
}
|
|
942
1126
|
return {
|
|
943
1127
|
url,
|
|
@@ -950,148 +1134,1228 @@ async function createServer(port, store, options = {}) {
|
|
|
950
1134
|
}
|
|
951
1135
|
server.close(() => resolve4());
|
|
952
1136
|
});
|
|
953
|
-
|
|
954
|
-
await store.shutdown();
|
|
955
|
-
}
|
|
1137
|
+
await store.shutdown();
|
|
956
1138
|
}
|
|
957
1139
|
};
|
|
958
1140
|
}
|
|
959
1141
|
|
|
960
1142
|
// src/live-scan.ts
|
|
961
|
-
import { existsSync as
|
|
1143
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1144
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1145
|
+
|
|
1146
|
+
// src/search-index-job-runner.ts
|
|
1147
|
+
import { existsSync as existsSync3 } from "fs";
|
|
962
1148
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
963
1149
|
import { Worker } from "worker_threads";
|
|
964
1150
|
|
|
965
|
-
// src/
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1151
|
+
// src/pending-search-index-jobs.ts
|
|
1152
|
+
var PendingSearchIndexJobBatch = class {
|
|
1153
|
+
constructor(id, context, jobs, waiter) {
|
|
1154
|
+
this.id = id;
|
|
1155
|
+
this.context = context;
|
|
1156
|
+
this.merge(context, jobs, waiter);
|
|
1157
|
+
}
|
|
1158
|
+
id;
|
|
1159
|
+
context;
|
|
1160
|
+
jobsByAgent = /* @__PURE__ */ new Map();
|
|
1161
|
+
waiters = [];
|
|
1162
|
+
settled = false;
|
|
1163
|
+
get jobs() {
|
|
1164
|
+
const jobs = [];
|
|
1165
|
+
for (const pending of this.jobsByAgent.values()) {
|
|
1166
|
+
if (pending.full) jobs.push(pending.full);
|
|
1167
|
+
if (pending.changes) jobs.push(changesJobFromPending(pending.changes));
|
|
1168
|
+
}
|
|
1169
|
+
return jobs;
|
|
1170
|
+
}
|
|
1171
|
+
get changeCount() {
|
|
1172
|
+
let count = 0;
|
|
1173
|
+
for (const pending of this.jobsByAgent.values()) {
|
|
1174
|
+
if (!pending.changes) continue;
|
|
1175
|
+
count += pending.changes.changesBySessionId.size + pending.changes.removedSessionIds.size;
|
|
1176
|
+
}
|
|
1177
|
+
return count;
|
|
1178
|
+
}
|
|
1179
|
+
merge(context, jobs, waiter) {
|
|
1180
|
+
this.context = context;
|
|
1181
|
+
this.waiters.push(waiter);
|
|
1182
|
+
for (const job of jobs) this.mergeJob(job);
|
|
1183
|
+
}
|
|
1184
|
+
settle(error) {
|
|
1185
|
+
if (this.settled) return false;
|
|
1186
|
+
this.settled = true;
|
|
1187
|
+
for (const waiter of this.waiters) {
|
|
1188
|
+
if (error) waiter.reject(error);
|
|
1189
|
+
else waiter.resolve();
|
|
1190
|
+
}
|
|
1191
|
+
this.waiters.length = 0;
|
|
1192
|
+
return true;
|
|
976
1193
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
if (
|
|
981
|
-
|
|
1194
|
+
mergeJob(job) {
|
|
1195
|
+
const pending = this.jobsByAgent.get(job.agentName) ?? {};
|
|
1196
|
+
this.jobsByAgent.set(job.agentName, pending);
|
|
1197
|
+
if (job.kind === "full") {
|
|
1198
|
+
pending.full = job;
|
|
1199
|
+
pending.changes = void 0;
|
|
1200
|
+
return;
|
|
982
1201
|
}
|
|
983
|
-
|
|
1202
|
+
pending.changes ??= createPendingChanges(job);
|
|
1203
|
+
mergeChanges(pending.changes, job);
|
|
984
1204
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
function isRecursiveWatchSupported(platform = process.platform, nodeVersion = process.versions.node) {
|
|
992
|
-
if (platform === "darwin" || platform === "win32") {
|
|
993
|
-
return true;
|
|
1205
|
+
};
|
|
1206
|
+
var PendingSearchIndexJobs = class {
|
|
1207
|
+
pendingBatch = null;
|
|
1208
|
+
get batchCount() {
|
|
1209
|
+
return this.pendingBatch ? 1 : 0;
|
|
994
1210
|
}
|
|
995
|
-
|
|
996
|
-
return
|
|
1211
|
+
get jobCount() {
|
|
1212
|
+
return this.pendingBatch?.jobs.length ?? 0;
|
|
997
1213
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1214
|
+
get changeCount() {
|
|
1215
|
+
return this.pendingBatch?.changeCount ?? 0;
|
|
1216
|
+
}
|
|
1217
|
+
enqueue(id, context, jobs) {
|
|
1218
|
+
if (jobs.length === 0) return Promise.resolve();
|
|
1219
|
+
return new Promise((resolve4, reject) => {
|
|
1220
|
+
const waiter = { resolve: resolve4, reject };
|
|
1221
|
+
if (this.pendingBatch) {
|
|
1222
|
+
this.pendingBatch.merge(context, jobs, waiter);
|
|
1223
|
+
} else {
|
|
1224
|
+
this.pendingBatch = new PendingSearchIndexJobBatch(id, context, jobs, waiter);
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
take() {
|
|
1229
|
+
const batch = this.pendingBatch;
|
|
1230
|
+
this.pendingBatch = null;
|
|
1231
|
+
return batch;
|
|
1232
|
+
}
|
|
1233
|
+
settle(batch, error) {
|
|
1234
|
+
return batch instanceof PendingSearchIndexJobBatch && batch.settle(error);
|
|
1235
|
+
}
|
|
1236
|
+
rejectAll(error) {
|
|
1237
|
+
const batch = this.take();
|
|
1238
|
+
if (batch) this.settle(batch, error);
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
function createPendingChanges(job) {
|
|
1242
|
+
return {
|
|
1243
|
+
context: job.context,
|
|
1244
|
+
agentName: job.agentName,
|
|
1245
|
+
changesBySessionId: /* @__PURE__ */ new Map(),
|
|
1246
|
+
removedSessionIds: /* @__PURE__ */ new Set(),
|
|
1247
|
+
meta: {},
|
|
1248
|
+
searchIndexOptions: job.searchIndexOptions
|
|
1249
|
+
};
|
|
1000
1250
|
}
|
|
1001
|
-
function
|
|
1002
|
-
|
|
1251
|
+
function mergeChanges(pending, job) {
|
|
1252
|
+
pending.context = job.context;
|
|
1253
|
+
pending.searchIndexOptions = mergeSearchIndexOptions(
|
|
1254
|
+
pending.searchIndexOptions,
|
|
1255
|
+
job.searchIndexOptions
|
|
1256
|
+
);
|
|
1257
|
+
for (const sessionId of job.removedSessionIds) {
|
|
1258
|
+
pending.changesBySessionId.delete(sessionId);
|
|
1259
|
+
pending.removedSessionIds.add(sessionId);
|
|
1260
|
+
delete pending.meta[sessionId];
|
|
1261
|
+
}
|
|
1262
|
+
for (const change of job.changes) {
|
|
1263
|
+
const sessionId = change.session.id;
|
|
1264
|
+
pending.removedSessionIds.delete(sessionId);
|
|
1265
|
+
pending.changesBySessionId.set(sessionId, change);
|
|
1266
|
+
if (Object.hasOwn(job.meta, sessionId)) pending.meta[sessionId] = job.meta[sessionId];
|
|
1267
|
+
else delete pending.meta[sessionId];
|
|
1268
|
+
}
|
|
1269
|
+
for (const [sessionId, meta] of Object.entries(job.meta)) {
|
|
1270
|
+
if (!pending.removedSessionIds.has(sessionId)) pending.meta[sessionId] = meta;
|
|
1271
|
+
}
|
|
1003
1272
|
}
|
|
1004
|
-
function
|
|
1005
|
-
|
|
1006
|
-
|
|
1273
|
+
function mergeSearchIndexOptions(current, incoming) {
|
|
1274
|
+
if (!current) return incoming;
|
|
1275
|
+
if (!incoming) return current;
|
|
1276
|
+
return { ...current, ...incoming };
|
|
1007
1277
|
}
|
|
1008
|
-
function
|
|
1009
|
-
return
|
|
1278
|
+
function changesJobFromPending(pending) {
|
|
1279
|
+
return {
|
|
1280
|
+
kind: "changes",
|
|
1281
|
+
context: pending.context,
|
|
1282
|
+
agentName: pending.agentName,
|
|
1283
|
+
changes: [...pending.changesBySessionId.values()],
|
|
1284
|
+
removedSessionIds: [...pending.removedSessionIds],
|
|
1285
|
+
meta: pending.meta,
|
|
1286
|
+
...pending.searchIndexOptions ? { searchIndexOptions: pending.searchIndexOptions } : {}
|
|
1287
|
+
};
|
|
1010
1288
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1289
|
+
|
|
1290
|
+
// src/search-index-job-runner.ts
|
|
1291
|
+
var SHUTDOWN_ERROR_MESSAGE = "Live scan store shut down";
|
|
1292
|
+
var SearchIndexJobRunner = class {
|
|
1293
|
+
worker = null;
|
|
1294
|
+
activeBatch = null;
|
|
1295
|
+
nextBatchId = 1;
|
|
1296
|
+
pendingJobs = new PendingSearchIndexJobs();
|
|
1297
|
+
isShuttingDown = false;
|
|
1298
|
+
hasCheckedFtsIntegrity = false;
|
|
1299
|
+
enqueue(context, jobs) {
|
|
1300
|
+
if (jobs.length === 0) return Promise.resolve();
|
|
1301
|
+
if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE));
|
|
1302
|
+
const batchId = this.nextBatchId++;
|
|
1303
|
+
const completion = this.pendingJobs.enqueue(batchId, context, jobs);
|
|
1304
|
+
if (this.worker) {
|
|
1305
|
+
const snapshot = this.snapshot();
|
|
1306
|
+
appLogger.debug("search_index.worker_queued", {
|
|
1307
|
+
batch_id: batchId,
|
|
1308
|
+
context,
|
|
1309
|
+
jobs: jobs.length,
|
|
1310
|
+
pending_batches: snapshot.pendingBatches,
|
|
1311
|
+
pending_jobs: snapshot.pendingJobs,
|
|
1312
|
+
pending_changes: snapshot.pendingChanges
|
|
1313
|
+
});
|
|
1314
|
+
} else {
|
|
1315
|
+
this.startNextBatch();
|
|
1017
1316
|
}
|
|
1317
|
+
return completion;
|
|
1018
1318
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
return isAbsolute(filenameText) ? filenameText : join2(watchPath, filenameText);
|
|
1026
|
-
}
|
|
1027
|
-
function resolveAgentWatchTargets(agentName) {
|
|
1028
|
-
const roots = resolveProviderRoots();
|
|
1029
|
-
const cursorDataPath = getCursorDataPath();
|
|
1030
|
-
switch (agentName) {
|
|
1031
|
-
case "claudecode":
|
|
1032
|
-
return [
|
|
1033
|
-
{ root: roots.claudeRoot, path: join2(roots.claudeRoot, "projects") },
|
|
1034
|
-
{ path: "data/claudecode" }
|
|
1035
|
-
];
|
|
1036
|
-
case "codex":
|
|
1037
|
-
return [
|
|
1038
|
-
{ path: join2(roots.codexRoot, "sessions") },
|
|
1039
|
-
{ path: join2(roots.codexRoot, "session_index.jsonl") }
|
|
1040
|
-
];
|
|
1041
|
-
case "pi":
|
|
1042
|
-
return [
|
|
1043
|
-
{ root: roots.piRoot, path: join2(roots.piRoot, "agent", "sessions") },
|
|
1044
|
-
{ root: "data/pi", path: "data/pi" }
|
|
1045
|
-
];
|
|
1046
|
-
case "cursor":
|
|
1047
|
-
return cursorDataPath ? [
|
|
1048
|
-
{
|
|
1049
|
-
root: cursorDataPath,
|
|
1050
|
-
path: join2(cursorDataPath, "globalStorage", "state.vscdb")
|
|
1051
|
-
},
|
|
1052
|
-
{ root: cursorDataPath, path: join2(cursorDataPath, "workspaceStorage") }
|
|
1053
|
-
] : [];
|
|
1054
|
-
case "kimi":
|
|
1055
|
-
return [
|
|
1056
|
-
{ root: roots.kimiRoot, path: join2(roots.kimiRoot, "sessions") },
|
|
1057
|
-
{ path: "data/kimi" }
|
|
1058
|
-
];
|
|
1059
|
-
case "opencode":
|
|
1060
|
-
return [
|
|
1061
|
-
{ root: roots.opencodeRoot, path: join2(roots.opencodeRoot, "opencode.db") },
|
|
1062
|
-
{ root: "data/opencode", path: "data/opencode/opencode.db" }
|
|
1063
|
-
];
|
|
1064
|
-
case "zcode":
|
|
1065
|
-
return roots.zcodeRoot ? [{ root: roots.zcodeRoot, path: join2(roots.zcodeRoot, "cli", "db", "db.sqlite") }] : [];
|
|
1066
|
-
default:
|
|
1067
|
-
return [];
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
var SessionWatcher = class {
|
|
1071
|
-
watchers = [];
|
|
1072
|
-
fallbackWatchScopes = /* @__PURE__ */ new Map();
|
|
1073
|
-
stablePaths = /* @__PURE__ */ new Map();
|
|
1074
|
-
listeners = /* @__PURE__ */ new Set();
|
|
1075
|
-
/** Register a listener fired (after write-stability polling) with the changed agent set. */
|
|
1076
|
-
onAgentsChanged(cb) {
|
|
1077
|
-
this.listeners.add(cb);
|
|
1078
|
-
return () => {
|
|
1079
|
-
this.listeners.delete(cb);
|
|
1319
|
+
snapshot() {
|
|
1320
|
+
return {
|
|
1321
|
+
activeBatchId: this.activeBatch?.id,
|
|
1322
|
+
pendingBatches: this.pendingJobs.batchCount,
|
|
1323
|
+
pendingJobs: this.pendingJobs.jobCount,
|
|
1324
|
+
pendingChanges: this.pendingJobs.changeCount
|
|
1080
1325
|
};
|
|
1081
1326
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1327
|
+
async shutdown() {
|
|
1328
|
+
this.isShuttingDown = true;
|
|
1329
|
+
const activeBatch = this.activeBatch;
|
|
1330
|
+
const worker = this.worker;
|
|
1331
|
+
this.activeBatch = null;
|
|
1332
|
+
this.worker = null;
|
|
1333
|
+
const shutdownError = new Error(SHUTDOWN_ERROR_MESSAGE);
|
|
1334
|
+
if (activeBatch) this.settle(activeBatch, shutdownError);
|
|
1335
|
+
this.pendingJobs.rejectAll(shutdownError);
|
|
1336
|
+
if (worker) await worker.terminate();
|
|
1337
|
+
}
|
|
1338
|
+
startNextBatch() {
|
|
1339
|
+
if (this.isShuttingDown || this.worker) return;
|
|
1340
|
+
const batch = this.pendingJobs.take();
|
|
1341
|
+
if (!batch) return;
|
|
1342
|
+
appLogger.info("search_index.worker_dequeued", {
|
|
1343
|
+
batch_id: batch.id,
|
|
1344
|
+
context: batch.context,
|
|
1345
|
+
pending_batches: this.pendingJobs.batchCount
|
|
1346
|
+
});
|
|
1347
|
+
this.startBatch(batch);
|
|
1348
|
+
}
|
|
1349
|
+
startBatch(batch) {
|
|
1350
|
+
if (this.isShuttingDown) {
|
|
1351
|
+
this.settle(batch, new Error(SHUTDOWN_ERROR_MESSAGE));
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
const workerUrl = this.workerUrl();
|
|
1355
|
+
if (!workerUrl) {
|
|
1356
|
+
appLogger.warn("search_index.worker_missing", { context: batch.context });
|
|
1357
|
+
this.settle(batch);
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
appLogger.info("search_index.worker_started", {
|
|
1361
|
+
batch_id: batch.id,
|
|
1362
|
+
context: batch.context,
|
|
1363
|
+
jobs: batch.jobs.length
|
|
1364
|
+
});
|
|
1365
|
+
const worker = new Worker(workerUrl, {
|
|
1366
|
+
workerData: {
|
|
1367
|
+
context: batch.context,
|
|
1368
|
+
jobs: batch.jobs,
|
|
1369
|
+
agentNames: [],
|
|
1370
|
+
sessionsByAgent: {},
|
|
1371
|
+
metaByAgent: {},
|
|
1372
|
+
skipFtsIntegrityCheck: this.hasCheckedFtsIntegrity
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
worker.unref();
|
|
1376
|
+
this.worker = worker;
|
|
1377
|
+
this.activeBatch = batch;
|
|
1378
|
+
worker.on("message", (message) => {
|
|
1379
|
+
if (message.type === "sync-result") {
|
|
1380
|
+
logSearchIndexSync(message.context, message.result);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
if (message.type !== "done") return;
|
|
1384
|
+
appLogger.info(`${message.context}.done`, {
|
|
1385
|
+
duration_ms: Math.round(message.durationMs),
|
|
1386
|
+
sessions: message.sessions
|
|
1387
|
+
});
|
|
1388
|
+
this.hasCheckedFtsIntegrity = true;
|
|
1389
|
+
this.settle(batch);
|
|
1390
|
+
});
|
|
1391
|
+
worker.on("error", (error) => {
|
|
1392
|
+
appLogger.error("search_index.worker_error", { context: batch.context, error });
|
|
1393
|
+
this.settle(batch, error);
|
|
1394
|
+
});
|
|
1395
|
+
worker.on("exit", (code) => this.finishWorker(worker, batch, code));
|
|
1396
|
+
}
|
|
1397
|
+
finishWorker(worker, batch, code) {
|
|
1398
|
+
appLogger.info("search_index.worker_exited", {
|
|
1399
|
+
batch_id: batch.id,
|
|
1400
|
+
context: batch.context,
|
|
1401
|
+
code,
|
|
1402
|
+
shutting_down: this.isShuttingDown || void 0
|
|
1403
|
+
});
|
|
1404
|
+
if (this.worker === worker) this.worker = null;
|
|
1405
|
+
if (this.activeBatch === batch) this.activeBatch = null;
|
|
1406
|
+
const error = code === 0 ? new Error("Search index worker exited before completing its batch") : new Error(`Search index worker exited with code ${code}`);
|
|
1407
|
+
if (code !== 0) appLogger.warn("search_index.worker_exit", { context: batch.context, code });
|
|
1408
|
+
this.settle(batch, error);
|
|
1409
|
+
this.startNextBatch();
|
|
1410
|
+
}
|
|
1411
|
+
settle(batch, error) {
|
|
1412
|
+
if (!this.pendingJobs.settle(batch, error)) return;
|
|
1413
|
+
appLogger.info("search_index.worker_settled", {
|
|
1414
|
+
batch_id: batch.id,
|
|
1415
|
+
context: batch.context,
|
|
1416
|
+
result: error ? "rejected" : "resolved"
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
workerUrl() {
|
|
1420
|
+
const workerUrl = new URL("./search-index-worker.js", import.meta.url);
|
|
1421
|
+
if (workerUrl.protocol === "file:" && !existsSync3(fileURLToPath2(workerUrl))) return null;
|
|
1422
|
+
return workerUrl;
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
// src/scan-status-model.ts
|
|
1427
|
+
var ScanStatusModel = class {
|
|
1428
|
+
status = {
|
|
1429
|
+
active: false,
|
|
1430
|
+
phase: "idle",
|
|
1431
|
+
pendingAgents: [],
|
|
1432
|
+
scanningAgents: [],
|
|
1433
|
+
completedAgents: [],
|
|
1434
|
+
agentStatuses: {},
|
|
1435
|
+
totalAgents: 0,
|
|
1436
|
+
updatedAt: Date.now(),
|
|
1437
|
+
backfill: { active: false, pendingAgents: [], completedAgents: [], failedAgents: [] }
|
|
1438
|
+
};
|
|
1439
|
+
snapshot() {
|
|
1440
|
+
return {
|
|
1441
|
+
type: "scan-status",
|
|
1442
|
+
...this.status,
|
|
1443
|
+
pendingAgents: [...this.status.pendingAgents],
|
|
1444
|
+
scanningAgents: [...this.status.scanningAgents],
|
|
1445
|
+
completedAgents: [...this.status.completedAgents],
|
|
1446
|
+
agentStatuses: Object.fromEntries(
|
|
1447
|
+
Object.entries(this.status.agentStatuses).map(([agentName, status]) => [
|
|
1448
|
+
agentName,
|
|
1449
|
+
{ ...status }
|
|
1450
|
+
])
|
|
1451
|
+
),
|
|
1452
|
+
backfill: {
|
|
1453
|
+
...this.status.backfill,
|
|
1454
|
+
pendingAgents: [...this.status.backfill.pendingAgents],
|
|
1455
|
+
completedAgents: [...this.status.backfill.completedAgents],
|
|
1456
|
+
failedAgents: [...this.status.backfill.failedAgents]
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
startBatch(agentNames, phase, sessionCounts) {
|
|
1461
|
+
const uniqueAgentNames = [...new Set(agentNames)];
|
|
1462
|
+
const now = Date.now();
|
|
1463
|
+
const agentStatuses = Object.fromEntries(
|
|
1464
|
+
uniqueAgentNames.map((agentName) => [
|
|
1465
|
+
agentName,
|
|
1466
|
+
{
|
|
1467
|
+
agentName,
|
|
1468
|
+
status: "pending",
|
|
1469
|
+
processed: 0,
|
|
1470
|
+
sessions: sessionCounts[agentName] ?? 0,
|
|
1471
|
+
updatedAt: now
|
|
1472
|
+
}
|
|
1473
|
+
])
|
|
1474
|
+
);
|
|
1475
|
+
return this.set({
|
|
1476
|
+
...this.status,
|
|
1477
|
+
active: uniqueAgentNames.length > 0,
|
|
1478
|
+
phase: uniqueAgentNames.length > 0 ? phase : "idle",
|
|
1479
|
+
pendingAgents: uniqueAgentNames,
|
|
1480
|
+
scanningAgents: [],
|
|
1481
|
+
completedAgents: [],
|
|
1482
|
+
agentStatuses,
|
|
1483
|
+
totalAgents: uniqueAgentNames.length,
|
|
1484
|
+
startedAt: uniqueAgentNames.length > 0 ? now : void 0,
|
|
1485
|
+
updatedAt: now,
|
|
1486
|
+
completedAt: uniqueAgentNames.length > 0 ? void 0 : now
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
setPhase(phase) {
|
|
1490
|
+
if (!this.status.active) return null;
|
|
1491
|
+
return this.set({ ...this.status, phase, updatedAt: Date.now() });
|
|
1492
|
+
}
|
|
1493
|
+
beginAgent(agentName, sessionCount) {
|
|
1494
|
+
if (!this.status.active)
|
|
1495
|
+
this.startBatch([agentName], "scanning", { [agentName]: sessionCount });
|
|
1496
|
+
const pendingAgents = this.status.pendingAgents.filter((agent) => agent !== agentName);
|
|
1497
|
+
const scanningAgents = [.../* @__PURE__ */ new Set([...this.status.scanningAgents, agentName])];
|
|
1498
|
+
const completedAgents = this.status.completedAgents.filter((agent) => agent !== agentName);
|
|
1499
|
+
const existingStatus = this.status.agentStatuses[agentName];
|
|
1500
|
+
const now = Date.now();
|
|
1501
|
+
return this.set({
|
|
1502
|
+
...this.status,
|
|
1503
|
+
active: true,
|
|
1504
|
+
phase: this.status.phase === "initializing" ? "initializing" : "scanning",
|
|
1505
|
+
pendingAgents,
|
|
1506
|
+
scanningAgents,
|
|
1507
|
+
completedAgents,
|
|
1508
|
+
agentStatuses: {
|
|
1509
|
+
...this.status.agentStatuses,
|
|
1510
|
+
[agentName]: {
|
|
1511
|
+
agentName,
|
|
1512
|
+
status: "scanning",
|
|
1513
|
+
total: existingStatus?.total,
|
|
1514
|
+
processed: existingStatus?.processed ?? 0,
|
|
1515
|
+
sessions: existingStatus?.sessions ?? sessionCount,
|
|
1516
|
+
startedAt: existingStatus?.startedAt ?? now,
|
|
1517
|
+
updatedAt: now
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
totalAgents: Math.max(this.status.totalAgents, pendingAgents.length + scanningAgents.length),
|
|
1521
|
+
updatedAt: now,
|
|
1522
|
+
completedAt: void 0
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
updateAgent(agentName, progress) {
|
|
1526
|
+
const status = this.status.agentStatuses[agentName];
|
|
1527
|
+
if (!status || status.status !== "scanning") return null;
|
|
1528
|
+
const now = Date.now();
|
|
1529
|
+
return this.set({
|
|
1530
|
+
...this.status,
|
|
1531
|
+
agentStatuses: {
|
|
1532
|
+
...this.status.agentStatuses,
|
|
1533
|
+
[agentName]: {
|
|
1534
|
+
...status,
|
|
1535
|
+
total: progress.total ?? status.total,
|
|
1536
|
+
processed: progress.processed ?? status.processed,
|
|
1537
|
+
sessions: progress.sessions ?? status.sessions,
|
|
1538
|
+
updatedAt: now
|
|
1539
|
+
}
|
|
1540
|
+
},
|
|
1541
|
+
updatedAt: now
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
finishAgent(agentName, sessionCount) {
|
|
1545
|
+
const pendingAgents = this.status.pendingAgents.filter((agent) => agent !== agentName);
|
|
1546
|
+
const scanningAgents = this.status.scanningAgents.filter((agent) => agent !== agentName);
|
|
1547
|
+
const completedAgents = [.../* @__PURE__ */ new Set([...this.status.completedAgents, agentName])];
|
|
1548
|
+
const isActive = pendingAgents.length > 0 || scanningAgents.length > 0;
|
|
1549
|
+
const now = Date.now();
|
|
1550
|
+
const previousStatus = this.status.agentStatuses[agentName];
|
|
1551
|
+
const total = previousStatus?.total ?? previousStatus?.processed;
|
|
1552
|
+
return this.set({
|
|
1553
|
+
...this.status,
|
|
1554
|
+
active: isActive,
|
|
1555
|
+
phase: isActive ? "scanning" : "idle",
|
|
1556
|
+
pendingAgents,
|
|
1557
|
+
scanningAgents,
|
|
1558
|
+
completedAgents,
|
|
1559
|
+
agentStatuses: {
|
|
1560
|
+
...this.status.agentStatuses,
|
|
1561
|
+
[agentName]: {
|
|
1562
|
+
agentName,
|
|
1563
|
+
status: "complete",
|
|
1564
|
+
total,
|
|
1565
|
+
processed: total,
|
|
1566
|
+
sessions: sessionCount ?? previousStatus?.sessions ?? 0,
|
|
1567
|
+
startedAt: previousStatus?.startedAt,
|
|
1568
|
+
updatedAt: now,
|
|
1569
|
+
completedAt: now
|
|
1570
|
+
}
|
|
1571
|
+
},
|
|
1572
|
+
updatedAt: now,
|
|
1573
|
+
completedAt: isActive ? void 0 : now
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
finishBatch() {
|
|
1577
|
+
const now = Date.now();
|
|
1578
|
+
return this.set({
|
|
1579
|
+
...this.status,
|
|
1580
|
+
active: false,
|
|
1581
|
+
phase: "idle",
|
|
1582
|
+
pendingAgents: [],
|
|
1583
|
+
scanningAgents: [],
|
|
1584
|
+
agentStatuses: Object.fromEntries(
|
|
1585
|
+
Object.entries(this.status.agentStatuses).map(([agentName, status]) => [
|
|
1586
|
+
agentName,
|
|
1587
|
+
{ ...status, status: "complete", completedAt: status.completedAt ?? now, updatedAt: now }
|
|
1588
|
+
])
|
|
1589
|
+
),
|
|
1590
|
+
updatedAt: now,
|
|
1591
|
+
completedAt: now
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
updateBackfill(patch) {
|
|
1595
|
+
return this.set({
|
|
1596
|
+
...this.status,
|
|
1597
|
+
backfill: { ...this.status.backfill, ...patch },
|
|
1598
|
+
updatedAt: Date.now()
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
set(status) {
|
|
1602
|
+
this.status = status;
|
|
1603
|
+
return this.snapshot();
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1607
|
+
// src/agent-sync-engine.ts
|
|
1608
|
+
var REFRESH_DEBOUNCE_MS = 200;
|
|
1609
|
+
var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
|
|
1610
|
+
var PENDING_REFRESH_DELAY_MS = 100;
|
|
1611
|
+
var MAX_ADAPTIVE_REFRESH_DELAY_MS = 3e4;
|
|
1612
|
+
var ADAPTIVE_REFRESH_DELAY_MULTIPLIER = 4;
|
|
1613
|
+
var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
|
|
1614
|
+
var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1615
|
+
function buildRefreshDiff(agentName, previousSessions, nextSessions, candidateChangedIds = []) {
|
|
1616
|
+
const { changes, removedSessionIds, counts } = computeSessionDiff(
|
|
1617
|
+
previousSessions,
|
|
1618
|
+
nextSessions,
|
|
1619
|
+
candidateChangedIds,
|
|
1620
|
+
sessionSignature
|
|
1621
|
+
);
|
|
1622
|
+
if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) {
|
|
1623
|
+
return { event: null, changedSessions: changes, removedSessionIds };
|
|
1624
|
+
}
|
|
1625
|
+
return {
|
|
1626
|
+
changedSessions: changes,
|
|
1627
|
+
removedSessionIds,
|
|
1628
|
+
event: {
|
|
1629
|
+
type: "sessions-updated",
|
|
1630
|
+
changedAgents: [agentName],
|
|
1631
|
+
newSessions: counts.new,
|
|
1632
|
+
updatedSessions: counts.updated,
|
|
1633
|
+
removedSessions: counts.removed,
|
|
1634
|
+
totalSessions: nextSessions.length,
|
|
1635
|
+
timestamp: Date.now(),
|
|
1636
|
+
changedSessionHeads: changes.map(({ session }) => ({ agentName, session })),
|
|
1637
|
+
removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
function restoreAgentCacheMeta(agent, cached) {
|
|
1642
|
+
agent.setSessionMetaMap(new Map(Object.entries(cached.meta)));
|
|
1643
|
+
}
|
|
1644
|
+
var AgentSyncEngine = class {
|
|
1645
|
+
constructor(options) {
|
|
1646
|
+
this.options = options;
|
|
1647
|
+
}
|
|
1648
|
+
options;
|
|
1649
|
+
refreshStates = /* @__PURE__ */ new Map();
|
|
1650
|
+
operationGenerations = /* @__PURE__ */ new Map();
|
|
1651
|
+
operationTails = /* @__PURE__ */ new Map();
|
|
1652
|
+
backfillQueue = [];
|
|
1653
|
+
currentBackfillAgent;
|
|
1654
|
+
completedBackfillAgents = [];
|
|
1655
|
+
failedBackfillAgents = [];
|
|
1656
|
+
sessionsChangedListeners = /* @__PURE__ */ new Set();
|
|
1657
|
+
statusChangedListeners = /* @__PURE__ */ new Set();
|
|
1658
|
+
scanStatus = new ScanStatusModel();
|
|
1659
|
+
searchIndexJobs = new SearchIndexJobRunner();
|
|
1660
|
+
backgroundRefreshTimer = null;
|
|
1661
|
+
isShuttingDown = false;
|
|
1662
|
+
initialize(cacheTimestamps = {}) {
|
|
1663
|
+
for (const agent of this.options.snapshot().agents) {
|
|
1664
|
+
this.state(agent.name).lastRefreshAt = cacheTimestamps[agent.name] ?? Date.now();
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
status() {
|
|
1668
|
+
return this.scanStatus.snapshot();
|
|
1669
|
+
}
|
|
1670
|
+
subscribeSessionsChanged(listener) {
|
|
1671
|
+
this.sessionsChangedListeners.add(listener);
|
|
1672
|
+
return () => this.sessionsChangedListeners.delete(listener);
|
|
1673
|
+
}
|
|
1674
|
+
subscribeStatusChanged(listener) {
|
|
1675
|
+
this.statusChangedListeners.add(listener);
|
|
1676
|
+
return () => this.statusChangedListeners.delete(listener);
|
|
1677
|
+
}
|
|
1678
|
+
async syncInitialIndex() {
|
|
1679
|
+
await this.searchIndexJobs.enqueue(
|
|
1680
|
+
"scan.initial",
|
|
1681
|
+
this.buildFullSearchIndexJobs("scan.initial")
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
handleAgentsChanged(agentNames) {
|
|
1685
|
+
const snapshot = this.options.snapshot();
|
|
1686
|
+
for (const agentName of agentNames) {
|
|
1687
|
+
this.state(agentName).pendingPathCount += 1;
|
|
1688
|
+
const delayMs = (snapshot.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
|
|
1689
|
+
this.scheduleRefresh(agentName, delayMs);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
startBackgroundRefresh() {
|
|
1693
|
+
if (this.backgroundRefreshTimer) return;
|
|
1694
|
+
const agentNames = this.options.snapshot().agents.map((agent) => agent.name);
|
|
1695
|
+
this.startScanBatch(agentNames, "scanning");
|
|
1696
|
+
this.backgroundRefreshTimer = setTimeout(() => {
|
|
1697
|
+
this.backgroundRefreshTimer = null;
|
|
1698
|
+
for (const agentName of agentNames) this.scheduleRefresh(agentName, 0);
|
|
1699
|
+
if (agentNames.length === 0) this.finishScanBatch();
|
|
1700
|
+
}, 0);
|
|
1701
|
+
}
|
|
1702
|
+
async refresh(agentName) {
|
|
1703
|
+
await this.runCoalescedRefresh(agentName);
|
|
1704
|
+
}
|
|
1705
|
+
async shutdown() {
|
|
1706
|
+
this.isShuttingDown = true;
|
|
1707
|
+
const activeOperations = {
|
|
1708
|
+
agent_operations: this.operationTails.size,
|
|
1709
|
+
refreshes: [...this.refreshStates.values()].filter((state) => state.isRunning).length,
|
|
1710
|
+
backfill_running: this.currentBackfillAgent != null || void 0,
|
|
1711
|
+
scan_workers: this.options.workerRunner.activeCount
|
|
1712
|
+
};
|
|
1713
|
+
if (activeOperations.agent_operations > 0 || activeOperations.scan_workers > 0) {
|
|
1714
|
+
appLogger.warn("scan.shutdown.active_operations", activeOperations);
|
|
1715
|
+
}
|
|
1716
|
+
for (const state of this.refreshStates.values()) {
|
|
1717
|
+
if (!state.timer) continue;
|
|
1718
|
+
clearTimeout(state.timer);
|
|
1719
|
+
state.timer = null;
|
|
1720
|
+
state.timerDeadline = 0;
|
|
1721
|
+
}
|
|
1722
|
+
if (this.backgroundRefreshTimer) {
|
|
1723
|
+
clearTimeout(this.backgroundRefreshTimer);
|
|
1724
|
+
this.backgroundRefreshTimer = null;
|
|
1725
|
+
}
|
|
1726
|
+
this.backfillQueue.length = 0;
|
|
1727
|
+
this.currentBackfillAgent = void 0;
|
|
1728
|
+
const searchIndexSnapshot = this.searchIndexJobs.snapshot();
|
|
1729
|
+
appLogger.info("search_index.shutdown.started", {
|
|
1730
|
+
active_batch_id: searchIndexSnapshot.activeBatchId,
|
|
1731
|
+
pending_batches: searchIndexSnapshot.pendingBatches
|
|
1732
|
+
});
|
|
1733
|
+
await this.searchIndexJobs.shutdown();
|
|
1734
|
+
await this.options.workerRunner.shutdown();
|
|
1735
|
+
await Promise.allSettled(this.operationTails.values());
|
|
1736
|
+
const stoppedSearchIndexSnapshot = this.searchIndexJobs.snapshot();
|
|
1737
|
+
appLogger.info("search_index.shutdown.completed", {
|
|
1738
|
+
active_batch_id: searchIndexSnapshot.activeBatchId,
|
|
1739
|
+
pending_batches: stoppedSearchIndexSnapshot.pendingBatches
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
startScanBatch(agentNames, phase) {
|
|
1743
|
+
const snapshot = this.options.snapshot();
|
|
1744
|
+
const sessionCounts = Object.fromEntries(
|
|
1745
|
+
agentNames.map((agentName) => [agentName, snapshot.byAgent[agentName]?.length ?? 0])
|
|
1746
|
+
);
|
|
1747
|
+
this.publishStatus(this.scanStatus.startBatch(agentNames, phase, sessionCounts));
|
|
1748
|
+
}
|
|
1749
|
+
setScanPhase(phase) {
|
|
1750
|
+
this.publishStatus(this.scanStatus.setPhase(phase));
|
|
1751
|
+
}
|
|
1752
|
+
beginAgentScan(agentName) {
|
|
1753
|
+
const snapshot = this.options.snapshot();
|
|
1754
|
+
if (!this.scanStatus.snapshot().active) this.startScanBatch([agentName], "scanning");
|
|
1755
|
+
this.publishStatus(
|
|
1756
|
+
this.scanStatus.beginAgent(agentName, snapshot.byAgent[agentName]?.length ?? 0)
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
updateAgentScanProgress(agentName, progress) {
|
|
1760
|
+
this.publishStatus(this.scanStatus.updateAgent(agentName, progress));
|
|
1761
|
+
}
|
|
1762
|
+
finishAgentScan(agentName) {
|
|
1763
|
+
const count = this.options.snapshot().byAgent[agentName]?.length;
|
|
1764
|
+
this.publishStatus(this.scanStatus.finishAgent(agentName, count));
|
|
1765
|
+
}
|
|
1766
|
+
finishScanBatch() {
|
|
1767
|
+
this.publishStatus(this.scanStatus.finishBatch());
|
|
1768
|
+
}
|
|
1769
|
+
publishBackfillStatus() {
|
|
1770
|
+
this.publishStatus(this.scanStatus.updateBackfill(this.backfillStatus()));
|
|
1771
|
+
}
|
|
1772
|
+
publishStatus(event) {
|
|
1773
|
+
if (!event || this.isShuttingDown) return;
|
|
1774
|
+
for (const listener of this.statusChangedListeners) listener(event);
|
|
1775
|
+
}
|
|
1776
|
+
emitSessionsChanged(change) {
|
|
1777
|
+
if (this.isShuttingDown) return;
|
|
1778
|
+
for (const listener of this.sessionsChangedListeners) listener(change);
|
|
1779
|
+
}
|
|
1780
|
+
scheduleRefresh(agentName, delayMs) {
|
|
1781
|
+
if (this.isShuttingDown) return;
|
|
1782
|
+
const state = this.state(agentName);
|
|
1783
|
+
const adaptiveDelayMs = Math.min(
|
|
1784
|
+
state.lastRefreshDurationMs * ADAPTIVE_REFRESH_DELAY_MULTIPLIER,
|
|
1785
|
+
MAX_ADAPTIVE_REFRESH_DELAY_MS
|
|
1786
|
+
);
|
|
1787
|
+
const effectiveDelayMs = Math.max(delayMs, adaptiveDelayMs);
|
|
1788
|
+
const deadline = Date.now() + effectiveDelayMs;
|
|
1789
|
+
if (state.timer) {
|
|
1790
|
+
if (deadline >= state.timerDeadline) return;
|
|
1791
|
+
clearTimeout(state.timer);
|
|
1792
|
+
}
|
|
1793
|
+
appLogger.debug("scan.refresh.schedule", { agent: agentName, delay_ms: effectiveDelayMs });
|
|
1794
|
+
state.timerDeadline = deadline;
|
|
1795
|
+
state.timer = setTimeout(() => {
|
|
1796
|
+
state.timer = null;
|
|
1797
|
+
void this.runCoalescedRefresh(agentName);
|
|
1798
|
+
}, effectiveDelayMs);
|
|
1799
|
+
}
|
|
1800
|
+
async runCoalescedRefresh(agentName) {
|
|
1801
|
+
const state = this.state(agentName);
|
|
1802
|
+
if (state.isRunning) {
|
|
1803
|
+
appLogger.debug("scan.refresh.pending", { agent: agentName });
|
|
1804
|
+
state.hasPendingRerun = true;
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
state.isRunning = true;
|
|
1808
|
+
try {
|
|
1809
|
+
await this.serialize(agentName, "refresh", () => this.performRefresh(agentName));
|
|
1810
|
+
} finally {
|
|
1811
|
+
state.isRunning = false;
|
|
1812
|
+
if (state.hasPendingRerun && !this.isShuttingDown) {
|
|
1813
|
+
state.hasPendingRerun = false;
|
|
1814
|
+
this.scheduleRefresh(agentName, PENDING_REFRESH_DELAY_MS);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
async performRefresh(agentName) {
|
|
1819
|
+
this.beginAgentScan(agentName);
|
|
1820
|
+
try {
|
|
1821
|
+
return await this.runRefresh(agentName);
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
appLogger.error("scan.refresh.error", { agent: agentName, error });
|
|
1824
|
+
console.error(`[${agentName}] Session refresh failed:`, error);
|
|
1825
|
+
return "failed";
|
|
1826
|
+
} finally {
|
|
1827
|
+
this.finishAgentScan(agentName);
|
|
1828
|
+
const agent = this.findAgent(agentName);
|
|
1829
|
+
if (agent && this.needsBackfill(agent)) this.enqueueBackfill(agentName);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
async runRefresh(agentName) {
|
|
1833
|
+
const startedAt = performance.now();
|
|
1834
|
+
const state = this.state(agentName);
|
|
1835
|
+
const pendingPathCount = state.pendingPathCount;
|
|
1836
|
+
state.pendingPathCount = 0;
|
|
1837
|
+
const agent = this.findAgent(agentName);
|
|
1838
|
+
if (!agent) {
|
|
1839
|
+
appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
|
|
1840
|
+
return "skipped";
|
|
1841
|
+
}
|
|
1842
|
+
const previousSessions = this.options.snapshot().byAgent[agentName] ?? [];
|
|
1843
|
+
const cached = loadCachedSessions(agentName);
|
|
1844
|
+
const refreshBaseline = cached?.sessions ?? previousSessions;
|
|
1845
|
+
const cacheTimestamp = cached?.timestamp ?? state.lastRefreshAt;
|
|
1846
|
+
if (cached) restoreAgentCacheMeta(agent, cached);
|
|
1847
|
+
const isInitialized = isAgentCacheInitialized(agentName);
|
|
1848
|
+
const availabilityStartedAt = performance.now();
|
|
1849
|
+
const isAvailable = agent.isAvailable();
|
|
1850
|
+
const availabilityDuration = performance.now() - availabilityStartedAt;
|
|
1851
|
+
let strategyResult;
|
|
1852
|
+
if (!isAvailable) {
|
|
1853
|
+
strategyResult = this.refreshUnavailableAgent(agentName);
|
|
1854
|
+
} else if (!isInitialized) {
|
|
1855
|
+
strategyResult = await this.initializeAgent(agent, previousSessions);
|
|
1856
|
+
} else if (cached && agent instanceof FileSystemSessionSource) {
|
|
1857
|
+
strategyResult = await this.syncAgentSources(agent, cached, startedAt);
|
|
1858
|
+
} else if (refreshBaseline.length > 0) {
|
|
1859
|
+
strategyResult = await this.refreshChangedAgent(
|
|
1860
|
+
agent,
|
|
1861
|
+
refreshBaseline,
|
|
1862
|
+
cacheTimestamp,
|
|
1863
|
+
startedAt
|
|
1864
|
+
);
|
|
1865
|
+
} else {
|
|
1866
|
+
strategyResult = await this.scanAgentFully(agent, previousSessions);
|
|
1867
|
+
}
|
|
1868
|
+
if (strategyResult.status === "unchanged") return "unchanged";
|
|
1869
|
+
const nextSessions = attachMissingProjectIdentities(strategyResult.nextSessions);
|
|
1870
|
+
const diffStartedAt = performance.now();
|
|
1871
|
+
const diff = buildRefreshDiff(
|
|
1872
|
+
agentName,
|
|
1873
|
+
previousSessions,
|
|
1874
|
+
nextSessions,
|
|
1875
|
+
strategyResult.preciseChangedIds ?? []
|
|
1876
|
+
);
|
|
1877
|
+
const diffDuration = performance.now() - diffStartedAt;
|
|
1878
|
+
const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
|
|
1879
|
+
const persistentChanges = strategyResult.persistenceDiff?.changedSessions ?? diff.changedSessions;
|
|
1880
|
+
const persistentRemovedSessionIds = strategyResult.persistenceDiff?.removedSessionIds ?? diff.removedSessionIds;
|
|
1881
|
+
const changedSessionIds = strategyResult.usedIncrementalScan ? new Set(persistentChanges.map(({ session }) => session.id)) : void 0;
|
|
1882
|
+
const persistStartedAt = performance.now();
|
|
1883
|
+
const persistentJob = strategyResult.usedIncrementalScan ? {
|
|
1884
|
+
kind: "changes",
|
|
1885
|
+
context: "scan.refresh",
|
|
1886
|
+
agentName,
|
|
1887
|
+
changes: persistentChanges,
|
|
1888
|
+
removedSessionIds: persistentRemovedSessionIds,
|
|
1889
|
+
meta: buildAgentCacheMeta(agent, changedSessionIds),
|
|
1890
|
+
...searchIndexOptions ? { searchIndexOptions } : {}
|
|
1891
|
+
} : strategyResult.fullScanSessions ? {
|
|
1892
|
+
kind: "full",
|
|
1893
|
+
context: "scan.refresh",
|
|
1894
|
+
agentName,
|
|
1895
|
+
sessions: strategyResult.fullScanSessions,
|
|
1896
|
+
meta: buildAgentCacheMeta(agent),
|
|
1897
|
+
saveCache: true,
|
|
1898
|
+
...searchIndexOptions ? { searchIndexOptions } : {}
|
|
1899
|
+
} : null;
|
|
1900
|
+
if (persistentJob) {
|
|
1901
|
+
const persist = this.searchIndexJobs.enqueue("scan.refresh", [persistentJob]);
|
|
1902
|
+
if (!isInitialized && persistentJob.kind === "full") {
|
|
1903
|
+
await persist;
|
|
1904
|
+
} else {
|
|
1905
|
+
void persist.catch((error) => {
|
|
1906
|
+
appLogger.error("scan.refresh.persist.error", { agent: agentName, error });
|
|
1907
|
+
console.error(`[${agentName}] Session persistence failed:`, error);
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
const persistDuration = performance.now() - persistStartedAt;
|
|
1912
|
+
logSearchIndexSync("scan.refresh", null, { pending_paths: pendingPathCount });
|
|
1913
|
+
this.emitSessionsChanged({ agentName, sessions: nextSessions, event: diff.event });
|
|
1914
|
+
const totalDurationMs = performance.now() - startedAt;
|
|
1915
|
+
state.lastRefreshDurationMs = totalDurationMs;
|
|
1916
|
+
appLogger.info("scan.refresh.done", {
|
|
1917
|
+
agent: agentName,
|
|
1918
|
+
duration_ms: Math.round(totalDurationMs),
|
|
1919
|
+
sessions: nextSessions.length,
|
|
1920
|
+
new_sessions: diff.event?.newSessions ?? 0,
|
|
1921
|
+
updated_sessions: diff.event?.updatedSessions ?? 0,
|
|
1922
|
+
removed_sessions: diff.event?.removedSessions ?? 0,
|
|
1923
|
+
pending_paths: pendingPathCount,
|
|
1924
|
+
availability_ms: Math.round(availabilityDuration),
|
|
1925
|
+
check_ms: Math.round(strategyResult.checkDuration),
|
|
1926
|
+
scan_ms: Math.round(strategyResult.scanDuration),
|
|
1927
|
+
diff_ms: Math.round(diffDuration),
|
|
1928
|
+
persist_ms: Math.round(persistDuration),
|
|
1929
|
+
search_index_ms: 0,
|
|
1930
|
+
persistent_index_worker_job: persistentJob?.kind,
|
|
1931
|
+
persistent_index_skipped: !persistentJob || void 0
|
|
1932
|
+
});
|
|
1933
|
+
return "committed";
|
|
1934
|
+
}
|
|
1935
|
+
refreshUnavailableAgent(agentName) {
|
|
1936
|
+
this.state(agentName).lastRefreshAt = Date.now();
|
|
1937
|
+
return this.refreshStrategyResult([]);
|
|
1938
|
+
}
|
|
1939
|
+
async initializeAgent(agent, previousSessions) {
|
|
1940
|
+
this.setScanPhase("initializing");
|
|
1941
|
+
const scanStartedAt = performance.now();
|
|
1942
|
+
const result = await this.runWorker(agent, previousSessions, null, this.startupScanOptions());
|
|
1943
|
+
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
1944
|
+
const sessions = attachMissingProjectIdentities(result.sessions);
|
|
1945
|
+
this.state(agent.name).lastRefreshAt = Date.now();
|
|
1946
|
+
return this.refreshStrategyResult(sessions, {
|
|
1947
|
+
fullScanSessions: sessions,
|
|
1948
|
+
scanDuration: performance.now() - scanStartedAt
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
async syncAgentSources(agent, cached, refreshStartedAt) {
|
|
1952
|
+
const scanStartedAt = performance.now();
|
|
1953
|
+
const result = await this.runWorker(agent, cached.sessions, null, this.startupScanOptions(), {
|
|
1954
|
+
sourceSync: true,
|
|
1955
|
+
meta: cached.meta
|
|
1956
|
+
});
|
|
1957
|
+
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
1958
|
+
const sessions = attachMissingProjectIdentities(result.sessions);
|
|
1959
|
+
const preciseChangedIds = result.changedIds ?? [];
|
|
1960
|
+
const persistenceDiff = buildRefreshDiff(
|
|
1961
|
+
agent.name,
|
|
1962
|
+
cached.sessions,
|
|
1963
|
+
sessions,
|
|
1964
|
+
preciseChangedIds
|
|
1965
|
+
);
|
|
1966
|
+
this.state(agent.name).lastRefreshAt = Date.now();
|
|
1967
|
+
if (preciseChangedIds.length === 0) this.logUnchangedRefresh(agent.name, refreshStartedAt);
|
|
1968
|
+
return this.refreshStrategyResult(sessions, {
|
|
1969
|
+
preciseChangedIds,
|
|
1970
|
+
usedIncrementalScan: true,
|
|
1971
|
+
persistenceDiff,
|
|
1972
|
+
scanDuration: performance.now() - scanStartedAt
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
async refreshChangedAgent(agent, baseline, cacheTimestamp, refreshStartedAt) {
|
|
1976
|
+
const checkStartedAt = performance.now();
|
|
1977
|
+
const checkResult = await Promise.resolve(agent.checkForChanges(cacheTimestamp, baseline));
|
|
1978
|
+
const checkDuration = performance.now() - checkStartedAt;
|
|
1979
|
+
this.state(agent.name).lastRefreshAt = checkResult.timestamp;
|
|
1980
|
+
if (!checkResult.hasChanges) {
|
|
1981
|
+
this.logUnchangedRefresh(agent.name, refreshStartedAt);
|
|
1982
|
+
return this.refreshStrategyResult(baseline, { status: "unchanged", checkDuration });
|
|
1983
|
+
}
|
|
1984
|
+
const preciseChangedIds = checkResult.changedIds ?? null;
|
|
1985
|
+
const scanStartedAt = performance.now();
|
|
1986
|
+
const sessions = attachMissingProjectIdentities(
|
|
1987
|
+
await Promise.resolve(agent.incrementalScan(baseline, checkResult.changedIds ?? []))
|
|
1988
|
+
);
|
|
1989
|
+
return this.refreshStrategyResult(sessions, {
|
|
1990
|
+
preciseChangedIds,
|
|
1991
|
+
usedIncrementalScan: Array.isArray(checkResult.changedIds),
|
|
1992
|
+
persistenceDiff: buildRefreshDiff(agent.name, baseline, sessions, preciseChangedIds ?? []),
|
|
1993
|
+
checkDuration,
|
|
1994
|
+
scanDuration: performance.now() - scanStartedAt
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
async scanAgentFully(agent, previousSessions) {
|
|
1998
|
+
const scanStartedAt = performance.now();
|
|
1999
|
+
const result = await this.runWorker(agent, previousSessions, null, {});
|
|
2000
|
+
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2001
|
+
const sessions = attachMissingProjectIdentities(result.sessions);
|
|
2002
|
+
this.state(agent.name).lastRefreshAt = Date.now();
|
|
2003
|
+
return this.refreshStrategyResult(sessions, {
|
|
2004
|
+
fullScanSessions: sessions,
|
|
2005
|
+
scanDuration: performance.now() - scanStartedAt
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
refreshStrategyResult(nextSessions, overrides = {}) {
|
|
2009
|
+
return {
|
|
2010
|
+
status: "continue",
|
|
2011
|
+
nextSessions,
|
|
2012
|
+
fullScanSessions: null,
|
|
2013
|
+
preciseChangedIds: null,
|
|
2014
|
+
usedIncrementalScan: false,
|
|
2015
|
+
persistenceDiff: null,
|
|
2016
|
+
checkDuration: 0,
|
|
2017
|
+
scanDuration: 0,
|
|
2018
|
+
...overrides
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
runWorker(agent, previousSessions, changedIds, scanOptions, workerOptions = {}) {
|
|
2022
|
+
return this.options.workerRunner.run(agent.name, {
|
|
2023
|
+
previousSessions,
|
|
2024
|
+
changedIds,
|
|
2025
|
+
scanOptions,
|
|
2026
|
+
sourceSync: workerOptions.sourceSync,
|
|
2027
|
+
meta: workerOptions.meta ?? buildAgentCacheMeta(agent),
|
|
2028
|
+
onProgress: (progress) => this.updateAgentScanProgress(agent.name, progress)
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
needsBackfill(agent) {
|
|
2032
|
+
const startupScanOptions = this.startupScanOptions();
|
|
2033
|
+
if (startupScanOptions.from == null && startupScanOptions.to == null) return false;
|
|
2034
|
+
if (!agent.isAvailable()) return false;
|
|
2035
|
+
const lastSyncAt = getAgentLastFullSyncAt(agent.name);
|
|
2036
|
+
return lastSyncAt == null || Date.now() - lastSyncAt > BACKFILL_INTERVAL_MS;
|
|
2037
|
+
}
|
|
2038
|
+
enqueueBackfill(agentName) {
|
|
2039
|
+
if (this.isShuttingDown || this.currentBackfillAgent === agentName || this.backfillQueue.includes(agentName)) {
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
this.backfillQueue.push(agentName);
|
|
2043
|
+
this.publishBackfillStatus();
|
|
2044
|
+
this.pumpBackfillQueue();
|
|
2045
|
+
}
|
|
2046
|
+
pumpBackfillQueue() {
|
|
2047
|
+
if (this.isShuttingDown || this.currentBackfillAgent) return;
|
|
2048
|
+
const agentName = this.backfillQueue.shift();
|
|
2049
|
+
if (!agentName) return;
|
|
2050
|
+
this.currentBackfillAgent = agentName;
|
|
2051
|
+
this.publishBackfillStatus();
|
|
2052
|
+
void this.serialize(agentName, "backfill", () => this.performBackfill(agentName)).then(
|
|
2053
|
+
(result) => {
|
|
2054
|
+
if (this.isShuttingDown) return;
|
|
2055
|
+
this.currentBackfillAgent = void 0;
|
|
2056
|
+
if (result === "committed") {
|
|
2057
|
+
if (!this.completedBackfillAgents.includes(agentName)) {
|
|
2058
|
+
this.completedBackfillAgents.push(agentName);
|
|
2059
|
+
}
|
|
2060
|
+
this.failedBackfillAgents = this.failedBackfillAgents.filter(
|
|
2061
|
+
(failedAgent) => failedAgent !== agentName
|
|
2062
|
+
);
|
|
2063
|
+
} else if (!this.failedBackfillAgents.includes(agentName)) {
|
|
2064
|
+
this.failedBackfillAgents.push(agentName);
|
|
2065
|
+
}
|
|
2066
|
+
this.publishBackfillStatus();
|
|
2067
|
+
this.pumpBackfillQueue();
|
|
2068
|
+
}
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
async performBackfill(agentName) {
|
|
2072
|
+
const startedAt = performance.now();
|
|
2073
|
+
const agent = this.findAgent(agentName);
|
|
2074
|
+
if (!agent || !agent.isAvailable()) return "skipped";
|
|
2075
|
+
const snapshot = this.options.snapshot();
|
|
2076
|
+
const cached = loadCachedSessions(agentName);
|
|
2077
|
+
const baseline = cached?.sessions ?? snapshot.byAgent[agentName] ?? [];
|
|
2078
|
+
const meta = cached?.meta ?? buildAgentCacheMeta(agent);
|
|
2079
|
+
if (cached) restoreAgentCacheMeta(agent, cached);
|
|
2080
|
+
try {
|
|
2081
|
+
const result = await this.runWorker(
|
|
2082
|
+
agent,
|
|
2083
|
+
baseline,
|
|
2084
|
+
null,
|
|
2085
|
+
{},
|
|
2086
|
+
{
|
|
2087
|
+
sourceSync: agent instanceof FileSystemSessionSource,
|
|
2088
|
+
meta
|
|
2089
|
+
}
|
|
2090
|
+
);
|
|
2091
|
+
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2092
|
+
const fullSessions = attachMissingProjectIdentities(result.sessions);
|
|
2093
|
+
const diff = buildRefreshDiff(
|
|
2094
|
+
agentName,
|
|
2095
|
+
snapshot.byAgent[agentName] ?? [],
|
|
2096
|
+
fullSessions,
|
|
2097
|
+
result.changedIds ?? []
|
|
2098
|
+
);
|
|
2099
|
+
await this.searchIndexJobs.enqueue("scan.backfill", [
|
|
2100
|
+
{
|
|
2101
|
+
kind: "full",
|
|
2102
|
+
context: "scan.backfill",
|
|
2103
|
+
agentName,
|
|
2104
|
+
sessions: fullSessions,
|
|
2105
|
+
meta: buildAgentCacheMeta(agent),
|
|
2106
|
+
saveCache: true
|
|
2107
|
+
}
|
|
2108
|
+
]);
|
|
2109
|
+
markAgentFullSyncCompleted(agentName);
|
|
2110
|
+
this.emitSessionsChanged({ agentName, sessions: fullSessions, event: diff.event });
|
|
2111
|
+
appLogger.info("scan.backfill.done", {
|
|
2112
|
+
agent: agentName,
|
|
2113
|
+
duration_ms: Math.round(performance.now() - startedAt),
|
|
2114
|
+
sessions: fullSessions.length,
|
|
2115
|
+
changed: result.changedIds?.length ?? 0
|
|
2116
|
+
});
|
|
2117
|
+
return "committed";
|
|
2118
|
+
} catch (error) {
|
|
2119
|
+
appLogger.error("scan.backfill.error", { agent: agentName, error });
|
|
2120
|
+
console.error(`[${agentName}] Backfill failed:`, error);
|
|
2121
|
+
return "failed";
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
serialize(agentName, kind, operation) {
|
|
2125
|
+
const previous = this.operationTails.get(agentName) ?? Promise.resolve();
|
|
2126
|
+
const run = previous.then(async () => {
|
|
2127
|
+
if (this.isShuttingDown) return "skipped";
|
|
2128
|
+
const lifecycle = this.beginOperation(agentName, kind);
|
|
2129
|
+
try {
|
|
2130
|
+
const result = await operation();
|
|
2131
|
+
this.completeOperation(lifecycle, result);
|
|
2132
|
+
return result;
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
this.completeOperation(lifecycle, "failed");
|
|
2135
|
+
throw error;
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
const tail = run.then(
|
|
2139
|
+
() => void 0,
|
|
2140
|
+
() => void 0
|
|
2141
|
+
);
|
|
2142
|
+
this.operationTails.set(agentName, tail);
|
|
2143
|
+
void tail.finally(() => {
|
|
2144
|
+
if (this.operationTails.get(agentName) === tail) this.operationTails.delete(agentName);
|
|
2145
|
+
});
|
|
2146
|
+
return run;
|
|
2147
|
+
}
|
|
2148
|
+
state(agentName) {
|
|
2149
|
+
const existing = this.refreshStates.get(agentName);
|
|
2150
|
+
if (existing) return existing;
|
|
2151
|
+
const state = {
|
|
2152
|
+
timer: null,
|
|
2153
|
+
timerDeadline: 0,
|
|
2154
|
+
isRunning: false,
|
|
2155
|
+
hasPendingRerun: false,
|
|
2156
|
+
lastRefreshAt: 0,
|
|
2157
|
+
lastRefreshDurationMs: 0,
|
|
2158
|
+
pendingPathCount: 0
|
|
2159
|
+
};
|
|
2160
|
+
this.refreshStates.set(agentName, state);
|
|
2161
|
+
return state;
|
|
2162
|
+
}
|
|
2163
|
+
beginOperation(agentName, kind) {
|
|
2164
|
+
const generation = (this.operationGenerations.get(agentName) ?? 0) + 1;
|
|
2165
|
+
const startedAt = Date.now();
|
|
2166
|
+
this.operationGenerations.set(agentName, generation);
|
|
2167
|
+
appLogger.info("scan.agent_operation.started", {
|
|
2168
|
+
agent: agentName,
|
|
2169
|
+
operation: kind,
|
|
2170
|
+
generation,
|
|
2171
|
+
started_at: startedAt
|
|
2172
|
+
});
|
|
2173
|
+
return { agentName, kind, generation, startedAt };
|
|
2174
|
+
}
|
|
2175
|
+
completeOperation(lifecycle, result) {
|
|
2176
|
+
const completedAt = Date.now();
|
|
2177
|
+
appLogger.info("scan.agent_operation.completed", {
|
|
2178
|
+
agent: lifecycle.agentName,
|
|
2179
|
+
operation: lifecycle.kind,
|
|
2180
|
+
generation: lifecycle.generation,
|
|
2181
|
+
started_at: lifecycle.startedAt,
|
|
2182
|
+
completed_at: completedAt,
|
|
2183
|
+
duration_ms: completedAt - lifecycle.startedAt,
|
|
2184
|
+
result
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
backfillStatus() {
|
|
2188
|
+
return {
|
|
2189
|
+
active: this.currentBackfillAgent != null || this.backfillQueue.length > 0,
|
|
2190
|
+
pendingAgents: [...this.backfillQueue],
|
|
2191
|
+
currentAgent: this.currentBackfillAgent,
|
|
2192
|
+
completedAgents: [...this.completedBackfillAgents],
|
|
2193
|
+
failedAgents: [...this.failedBackfillAgents]
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
buildFullSearchIndexJobs(context) {
|
|
2197
|
+
const snapshot = this.options.snapshot();
|
|
2198
|
+
return snapshot.agents.map((agent) => {
|
|
2199
|
+
const cached = loadCachedSessions(agent.name);
|
|
2200
|
+
return cached ? {
|
|
2201
|
+
kind: "full",
|
|
2202
|
+
context,
|
|
2203
|
+
agentName: agent.name,
|
|
2204
|
+
sessions: cached.sessions,
|
|
2205
|
+
meta: cached.meta
|
|
2206
|
+
} : {
|
|
2207
|
+
kind: "full",
|
|
2208
|
+
context,
|
|
2209
|
+
agentName: agent.name,
|
|
2210
|
+
sessions: snapshot.byAgent[agent.name] ?? [],
|
|
2211
|
+
meta: buildAgentCacheMeta(agent)
|
|
2212
|
+
};
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
findAgent(agentName) {
|
|
2216
|
+
return this.options.snapshot().agents.find((agent) => agent.name === agentName);
|
|
2217
|
+
}
|
|
2218
|
+
startupScanOptions() {
|
|
2219
|
+
return this.options.startupScanOptions ?? {};
|
|
2220
|
+
}
|
|
2221
|
+
logUnchangedRefresh(agentName, startedAt) {
|
|
2222
|
+
appLogger.debug("scan.refresh.unchanged", {
|
|
2223
|
+
agent: agentName,
|
|
2224
|
+
duration_ms: Math.round(performance.now() - startedAt)
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
|
|
2229
|
+
// src/session-watcher.ts
|
|
2230
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, watch } from "fs";
|
|
2231
|
+
import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
|
|
2232
|
+
var WRITE_STABILITY_THRESHOLD_MS = 250;
|
|
2233
|
+
var WRITE_STABILITY_POLL_MS = 100;
|
|
2234
|
+
function toAbsolutePath(path) {
|
|
2235
|
+
return isAbsolute(path) ? path : resolve2(path);
|
|
2236
|
+
}
|
|
2237
|
+
function closestWatchablePath(targetPath) {
|
|
2238
|
+
if (!isAbsolute(targetPath) && !existsSync4(targetPath)) {
|
|
2239
|
+
return null;
|
|
2240
|
+
}
|
|
2241
|
+
let current = toAbsolutePath(targetPath);
|
|
2242
|
+
while (!existsSync4(current)) {
|
|
2243
|
+
const parent = dirname2(current);
|
|
2244
|
+
if (parent === current) {
|
|
2245
|
+
return null;
|
|
2246
|
+
}
|
|
2247
|
+
current = parent;
|
|
2248
|
+
}
|
|
2249
|
+
return current;
|
|
2250
|
+
}
|
|
2251
|
+
function getWatchRoot(path) {
|
|
2252
|
+
const stat = statSync2(path);
|
|
2253
|
+
return stat.isDirectory() ? path : dirname2(path);
|
|
2254
|
+
}
|
|
2255
|
+
function isRecursiveWatchSupported(platform = process.platform, nodeVersion = process.versions.node) {
|
|
2256
|
+
if (platform === "darwin" || platform === "win32") {
|
|
2257
|
+
return true;
|
|
2258
|
+
}
|
|
2259
|
+
if (platform !== "linux" && platform !== "aix" && platform !== "ibmi") {
|
|
2260
|
+
return false;
|
|
2261
|
+
}
|
|
2262
|
+
const [major = 0, minor = 0] = nodeVersion.split(".").map((part) => Number(part));
|
|
2263
|
+
return major > 19 || major === 19 && minor >= 1;
|
|
2264
|
+
}
|
|
2265
|
+
function isRecursiveWatchUnavailable(error) {
|
|
2266
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM";
|
|
2267
|
+
}
|
|
2268
|
+
function isSameOrChildPath(parentPath, childPath) {
|
|
2269
|
+
const path = relative(parentPath, childPath);
|
|
2270
|
+
return path === "" || !path.startsWith("..") && !isAbsolute(path);
|
|
2271
|
+
}
|
|
2272
|
+
function isRelatedPath(changedPath, targetPath) {
|
|
2273
|
+
return isSameOrChildPath(targetPath, changedPath) || isSameOrChildPath(changedPath, targetPath);
|
|
2274
|
+
}
|
|
2275
|
+
function mergeScopes(target, scopes) {
|
|
2276
|
+
for (const scope of scopes) {
|
|
2277
|
+
if (!target.some(
|
|
2278
|
+
(item) => item.agentName === scope.agentName && item.targetPath === scope.targetPath
|
|
2279
|
+
)) {
|
|
2280
|
+
target.push(scope);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
function resolveWatchEventPath(watchPath, filename) {
|
|
2285
|
+
const filenameText = filename?.toString();
|
|
2286
|
+
if (!filenameText) {
|
|
2287
|
+
return watchPath;
|
|
2288
|
+
}
|
|
2289
|
+
return isAbsolute(filenameText) ? filenameText : join2(watchPath, filenameText);
|
|
2290
|
+
}
|
|
2291
|
+
function resolveAgentWatchTargets(agentName) {
|
|
2292
|
+
const roots = resolveProviderRoots();
|
|
2293
|
+
const cursorDataPath = getCursorDataPath();
|
|
2294
|
+
switch (agentName) {
|
|
2295
|
+
case "claudecode":
|
|
2296
|
+
return [
|
|
2297
|
+
{ root: roots.claudeRoot, path: join2(roots.claudeRoot, "projects") },
|
|
2298
|
+
{ path: "data/claudecode" }
|
|
2299
|
+
];
|
|
2300
|
+
case "codex":
|
|
2301
|
+
return [
|
|
2302
|
+
{ path: join2(roots.codexRoot, "sessions") },
|
|
2303
|
+
{ path: join2(roots.codexRoot, "session_index.jsonl") }
|
|
2304
|
+
];
|
|
2305
|
+
case "pi":
|
|
2306
|
+
return [
|
|
2307
|
+
{ root: roots.piRoot, path: join2(roots.piRoot, "agent", "sessions") },
|
|
2308
|
+
{ root: "data/pi", path: "data/pi" }
|
|
2309
|
+
];
|
|
2310
|
+
case "cursor":
|
|
2311
|
+
return cursorDataPath ? [
|
|
2312
|
+
{
|
|
2313
|
+
root: cursorDataPath,
|
|
2314
|
+
path: join2(cursorDataPath, "globalStorage", "state.vscdb")
|
|
2315
|
+
},
|
|
2316
|
+
{ root: cursorDataPath, path: join2(cursorDataPath, "workspaceStorage") }
|
|
2317
|
+
] : [];
|
|
2318
|
+
case "kimi":
|
|
2319
|
+
return [
|
|
2320
|
+
{ root: roots.kimiRoot, path: join2(roots.kimiRoot, "sessions") },
|
|
2321
|
+
{ path: "data/kimi" }
|
|
2322
|
+
];
|
|
2323
|
+
case "opencode":
|
|
2324
|
+
return [
|
|
2325
|
+
{ root: roots.opencodeRoot, path: join2(roots.opencodeRoot, "opencode.db") },
|
|
2326
|
+
{ root: "data/opencode", path: "data/opencode/opencode.db" }
|
|
2327
|
+
];
|
|
2328
|
+
case "zcode":
|
|
2329
|
+
return roots.zcodeRoot ? [{ root: roots.zcodeRoot, path: join2(roots.zcodeRoot, "cli", "db", "db.sqlite") }] : [];
|
|
2330
|
+
default:
|
|
2331
|
+
return [];
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
var SessionWatcher = class {
|
|
2335
|
+
watchers = [];
|
|
2336
|
+
fallbackWatchScopes = /* @__PURE__ */ new Map();
|
|
2337
|
+
stablePaths = /* @__PURE__ */ new Map();
|
|
2338
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2339
|
+
/** Register a listener fired (after write-stability polling) with the changed agent set. */
|
|
2340
|
+
onAgentsChanged(cb) {
|
|
2341
|
+
this.listeners.add(cb);
|
|
2342
|
+
return () => {
|
|
2343
|
+
this.listeners.delete(cb);
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
/** Begin watching the given agent names' data directories. */
|
|
2347
|
+
start(agentNames) {
|
|
2348
|
+
const scopesByRoot = /* @__PURE__ */ new Map();
|
|
2349
|
+
for (const agentName of agentNames) {
|
|
2350
|
+
const watchTargets = resolveAgentWatchTargets(agentName);
|
|
2351
|
+
if (watchTargets.length === 0) {
|
|
2352
|
+
appLogger.debug("watch.skip", { agent: agentName });
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2355
|
+
for (const target of watchTargets) {
|
|
2356
|
+
const watchRootPath = closestWatchablePath(target.root ?? target.path);
|
|
2357
|
+
if (!watchRootPath) continue;
|
|
2358
|
+
let rootPath;
|
|
1095
2359
|
try {
|
|
1096
2360
|
rootPath = getWatchRoot(watchRootPath);
|
|
1097
2361
|
} catch (error) {
|
|
@@ -1280,44 +2544,78 @@ var SessionWatcher = class {
|
|
|
1280
2544
|
}
|
|
1281
2545
|
};
|
|
1282
2546
|
|
|
1283
|
-
// src/
|
|
1284
|
-
|
|
1285
|
-
var
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
2547
|
+
// src/worker-runner.ts
|
|
2548
|
+
import { Worker as Worker2 } from "worker_threads";
|
|
2549
|
+
var ThreadWorkerRunner = class {
|
|
2550
|
+
constructor(workerUrl) {
|
|
2551
|
+
this.workerUrl = workerUrl;
|
|
2552
|
+
}
|
|
2553
|
+
workerUrl;
|
|
2554
|
+
workers = /* @__PURE__ */ new Set();
|
|
2555
|
+
get activeCount() {
|
|
2556
|
+
return this.workers.size;
|
|
2557
|
+
}
|
|
2558
|
+
run(agentName, payload) {
|
|
2559
|
+
return new Promise((resolve4, reject) => {
|
|
2560
|
+
const worker = new Worker2(this.workerUrl, {
|
|
2561
|
+
workerData: {
|
|
2562
|
+
agentName,
|
|
2563
|
+
previousSessions: payload.previousSessions,
|
|
2564
|
+
changedIds: payload.changedIds,
|
|
2565
|
+
sourceSync: payload.sourceSync,
|
|
2566
|
+
scanOptions: payload.scanOptions,
|
|
2567
|
+
meta: payload.meta
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
worker.unref();
|
|
2571
|
+
this.workers.add(worker);
|
|
2572
|
+
let settled = false;
|
|
2573
|
+
const finish = (callback, terminate = true) => {
|
|
2574
|
+
if (settled) return;
|
|
2575
|
+
settled = true;
|
|
2576
|
+
this.workers.delete(worker);
|
|
2577
|
+
if (terminate) void worker.terminate();
|
|
2578
|
+
callback();
|
|
2579
|
+
};
|
|
2580
|
+
worker.on("message", (message) => {
|
|
2581
|
+
if (message.type === "progress") {
|
|
2582
|
+
payload.onProgress?.(message.progress);
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
if (message.type === "done") {
|
|
2586
|
+
finish(
|
|
2587
|
+
() => resolve4({
|
|
2588
|
+
sessions: message.sessions,
|
|
2589
|
+
meta: message.meta,
|
|
2590
|
+
changedIds: message.changedIds
|
|
2591
|
+
})
|
|
2592
|
+
);
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
finish(() => reject(new Error(message.error)));
|
|
2596
|
+
});
|
|
2597
|
+
worker.once("error", (error) => {
|
|
2598
|
+
finish(() => reject(error));
|
|
2599
|
+
});
|
|
2600
|
+
worker.once("exit", (code) => {
|
|
2601
|
+
if (settled) return;
|
|
2602
|
+
appLogger.warn("scan.refresh_worker.exit_before_done", { agent: agentName, code });
|
|
2603
|
+
finish(
|
|
2604
|
+
() => reject(new Error(`Scan refresh worker exited before completing (code ${code})`)),
|
|
2605
|
+
false
|
|
2606
|
+
);
|
|
2607
|
+
});
|
|
2608
|
+
});
|
|
1301
2609
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
totalSessions: nextSessions.length,
|
|
1312
|
-
timestamp: Date.now(),
|
|
1313
|
-
changedSessionHeads: changes.map(({ session }) => ({ agentName, session })),
|
|
1314
|
-
removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
|
|
1315
|
-
}
|
|
1316
|
-
};
|
|
1317
|
-
}
|
|
1318
|
-
function restoreAgentCacheMeta(agent, meta) {
|
|
1319
|
-
agent.setSessionMetaMap(new Map(Object.entries(meta)));
|
|
1320
|
-
}
|
|
2610
|
+
async shutdown() {
|
|
2611
|
+
const workers = [...this.workers];
|
|
2612
|
+
await Promise.allSettled(workers.map((worker) => worker.terminate()));
|
|
2613
|
+
this.workers.clear();
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
|
|
2617
|
+
// src/live-scan.ts
|
|
2618
|
+
var NEW_SESSION_EVENT_WINDOW_MS = 250;
|
|
1321
2619
|
function mergeEvents(previous, next) {
|
|
1322
2620
|
const changedSessionHeads = /* @__PURE__ */ new Map();
|
|
1323
2621
|
const removedSessionRefs = /* @__PURE__ */ new Map();
|
|
@@ -1349,957 +2647,172 @@ function mergeEvents(previous, next) {
|
|
|
1349
2647
|
};
|
|
1350
2648
|
}
|
|
1351
2649
|
var LiveScanStore = class {
|
|
1352
|
-
constructor(watchEnabled = true, scanOptions = {}, startupScanOptions = {}, storeOptions = {}) {
|
|
1353
|
-
this.watchEnabled = watchEnabled;
|
|
1354
|
-
this.scanOptions = scanOptions;
|
|
1355
|
-
this.startupScanOptions = startupScanOptions;
|
|
1356
|
-
this.storeOptions = storeOptions;
|
|
1357
|
-
}
|
|
1358
2650
|
watchEnabled;
|
|
1359
2651
|
scanOptions;
|
|
1360
2652
|
startupScanOptions;
|
|
1361
|
-
|
|
2653
|
+
deferInitialRefresh;
|
|
2654
|
+
syncEngine;
|
|
1362
2655
|
agents = [];
|
|
1363
2656
|
byAgent = {};
|
|
1364
2657
|
sessions = [];
|
|
1365
2658
|
listeners = /* @__PURE__ */ new Set();
|
|
1366
|
-
scanStatusListeners = /* @__PURE__ */ new Set();
|
|
1367
|
-
scanStatus = {
|
|
1368
|
-
active: false,
|
|
1369
|
-
phase: "idle",
|
|
1370
|
-
pendingAgents: [],
|
|
1371
|
-
scanningAgents: [],
|
|
1372
|
-
completedAgents: [],
|
|
1373
|
-
agentStatuses: {},
|
|
1374
|
-
totalAgents: 0,
|
|
1375
|
-
updatedAt: Date.now(),
|
|
1376
|
-
backfill: { active: false, pendingAgents: [], completedAgents: [] }
|
|
1377
|
-
};
|
|
1378
|
-
backfillQueue = [];
|
|
1379
|
-
backfillRunning = false;
|
|
1380
|
-
refreshStates = /* @__PURE__ */ new Map();
|
|
1381
2659
|
watcher = null;
|
|
1382
2660
|
pendingEvent = null;
|
|
1383
2661
|
pendingEventTimer = null;
|
|
1384
|
-
|
|
1385
|
-
searchIndexWorker = null;
|
|
1386
|
-
pendingSearchIndexJobs = [];
|
|
2662
|
+
shutdownPromise = null;
|
|
1387
2663
|
shuttingDown = false;
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
pendingPathCount: 0
|
|
1401
|
-
};
|
|
1402
|
-
this.refreshStates.set(agentName, state);
|
|
1403
|
-
}
|
|
1404
|
-
return state;
|
|
2664
|
+
constructor(options = {}) {
|
|
2665
|
+
this.watchEnabled = options.watchEnabled ?? true;
|
|
2666
|
+
this.scanOptions = options.scanOptions ?? {};
|
|
2667
|
+
this.startupScanOptions = options.startupScanOptions ?? {};
|
|
2668
|
+
this.deferInitialRefresh = options.deferInitialRefresh === true;
|
|
2669
|
+
const workerRunner = options.workerRunner ?? new ThreadWorkerRunner(new URL("./scan-refresh-worker.js", import.meta.url));
|
|
2670
|
+
this.syncEngine = new AgentSyncEngine({
|
|
2671
|
+
snapshot: () => this.getSnapshot(),
|
|
2672
|
+
startupScanOptions: this.startupScanOptions,
|
|
2673
|
+
workerRunner
|
|
2674
|
+
});
|
|
2675
|
+
this.syncEngine.subscribeSessionsChanged((change) => this.applySessionsChanged(change));
|
|
1405
2676
|
}
|
|
1406
2677
|
async initialize() {
|
|
1407
2678
|
const startedAt = performance.now();
|
|
1408
|
-
const deferInitialRefresh = this.storeOptions.deferInitialRefresh === true;
|
|
1409
2679
|
appLogger.info("scan.initial.start", {
|
|
1410
2680
|
watch_enabled: this.watchEnabled,
|
|
1411
2681
|
agents: this.scanOptions.agents,
|
|
1412
2682
|
use_cache: this.scanOptions.useCache ?? true,
|
|
1413
2683
|
startup_from: this.startupScanOptions.from,
|
|
1414
2684
|
startup_to: this.startupScanOptions.to,
|
|
1415
|
-
deferred: deferInitialRefresh || void 0
|
|
2685
|
+
deferred: this.deferInitialRefresh || void 0
|
|
1416
2686
|
});
|
|
1417
2687
|
const initialResult = await scanSessions({
|
|
1418
2688
|
...this.scanOptions,
|
|
1419
|
-
...deferInitialRefresh ? this.startupScanOptions : {},
|
|
1420
2689
|
useCache: this.scanOptions.useCache ?? true,
|
|
1421
2690
|
smartRefresh: false,
|
|
1422
|
-
cacheOnly: deferInitialRefresh,
|
|
1423
|
-
writeCache: deferInitialRefresh ? false : this.scanOptions.writeCache,
|
|
2691
|
+
cacheOnly: this.deferInitialRefresh,
|
|
2692
|
+
writeCache: this.deferInitialRefresh ? false : this.scanOptions.writeCache,
|
|
1424
2693
|
smartTagWorkerUrl: this.getSmartTagWorkerUrl() ?? void 0,
|
|
1425
|
-
includeSmartTags: deferInitialRefresh ? false : void 0
|
|
2694
|
+
includeSmartTags: this.deferInitialRefresh ? false : void 0
|
|
1426
2695
|
});
|
|
1427
2696
|
this.applyScanResult(initialResult);
|
|
2697
|
+
this.syncEngine.initialize(initialResult.cacheTimestamps);
|
|
1428
2698
|
const indexStartedAt = performance.now();
|
|
1429
|
-
if (!deferInitialRefresh)
|
|
1430
|
-
await this.enqueueSearchIndexJobs(
|
|
1431
|
-
"scan.initial",
|
|
1432
|
-
this.buildFullSearchIndexJobs("scan.initial")
|
|
1433
|
-
);
|
|
1434
|
-
}
|
|
2699
|
+
if (!this.deferInitialRefresh) await this.syncEngine.syncInitialIndex();
|
|
1435
2700
|
const indexDuration = performance.now() - indexStartedAt;
|
|
1436
2701
|
appLogger.info("scan.initial.done", {
|
|
1437
2702
|
duration_ms: Math.round(performance.now() - startedAt),
|
|
1438
|
-
index_ms: deferInitialRefresh ? void 0 : Math.round(indexDuration),
|
|
1439
|
-
deferred: deferInitialRefresh || void 0,
|
|
2703
|
+
index_ms: this.deferInitialRefresh ? void 0 : Math.round(indexDuration),
|
|
2704
|
+
deferred: this.deferInitialRefresh || void 0,
|
|
1440
2705
|
sessions: this.sessions.length,
|
|
1441
2706
|
agents: Object.fromEntries(
|
|
1442
2707
|
Object.entries(this.byAgent).map(([key, value]) => [key, value.length])
|
|
1443
2708
|
),
|
|
1444
2709
|
agent_timings: initialResult.timings ? Object.fromEntries(
|
|
1445
|
-
Object.entries(initialResult.timings).map(([name,
|
|
2710
|
+
Object.entries(initialResult.timings).map(([name, timing]) => [
|
|
1446
2711
|
name,
|
|
1447
2712
|
{
|
|
1448
|
-
total_ms: Math.round(
|
|
1449
|
-
cache_load_ms:
|
|
1450
|
-
check_changes_ms:
|
|
1451
|
-
scan_ms:
|
|
1452
|
-
identity_ms:
|
|
1453
|
-
tags_ms:
|
|
2713
|
+
total_ms: Math.round(timing.total),
|
|
2714
|
+
cache_load_ms: timing.cacheLoad != null ? Math.round(timing.cacheLoad) : void 0,
|
|
2715
|
+
check_changes_ms: timing.checkChanges != null ? Math.round(timing.checkChanges) : void 0,
|
|
2716
|
+
scan_ms: timing.scan != null ? Math.round(timing.scan) : void 0,
|
|
2717
|
+
identity_ms: timing.identity != null ? Math.round(timing.identity) : void 0,
|
|
2718
|
+
tags_ms: timing.tags != null ? Math.round(timing.tags) : void 0
|
|
1454
2719
|
}
|
|
1455
2720
|
])
|
|
1456
2721
|
) : void 0
|
|
1457
2722
|
});
|
|
1458
|
-
if (this.watchEnabled)
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
this.getRefreshState(agentName).pendingPathCount += 1;
|
|
1463
|
-
const delayMs = (this.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
|
|
1464
|
-
this.scheduleRefresh(agentName, delayMs);
|
|
1465
|
-
}
|
|
1466
|
-
});
|
|
1467
|
-
this.watcher.start(this.agents.map((agent) => agent.name));
|
|
1468
|
-
}
|
|
2723
|
+
if (!this.watchEnabled) return;
|
|
2724
|
+
this.watcher = new SessionWatcher();
|
|
2725
|
+
this.watcher.onAgentsChanged((agentNames) => this.syncEngine.handleAgentsChanged(agentNames));
|
|
2726
|
+
this.watcher.start(this.agents.map((agent) => agent.name));
|
|
1469
2727
|
}
|
|
1470
2728
|
startBackgroundRefresh() {
|
|
1471
|
-
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
const agentNames = this.agents.map((agent) => agent.name);
|
|
1475
|
-
this.startScanBatch(agentNames, "scanning");
|
|
1476
|
-
this.backgroundRefreshTimer = setTimeout(() => {
|
|
1477
|
-
this.backgroundRefreshTimer = null;
|
|
1478
|
-
for (const agentName of agentNames) {
|
|
1479
|
-
this.scheduleRefresh(agentName, 0);
|
|
1480
|
-
}
|
|
1481
|
-
if (agentNames.length === 0) {
|
|
1482
|
-
this.finishScanBatch();
|
|
1483
|
-
}
|
|
1484
|
-
for (const agent of this.agents) {
|
|
1485
|
-
if (this.needsBackfill(agent)) {
|
|
1486
|
-
this.enqueueBackfill(agent.name);
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
}, 0);
|
|
2729
|
+
this.syncEngine.startBackgroundRefresh();
|
|
1490
2730
|
}
|
|
1491
2731
|
getSnapshot() {
|
|
1492
|
-
return {
|
|
1493
|
-
sessions: this.sessions,
|
|
1494
|
-
byAgent: this.byAgent,
|
|
1495
|
-
agents: this.agents
|
|
1496
|
-
};
|
|
2732
|
+
return { sessions: this.sessions, byAgent: this.byAgent, agents: this.agents };
|
|
1497
2733
|
}
|
|
1498
2734
|
getScanStatus() {
|
|
1499
|
-
return
|
|
1500
|
-
type: "scan-status",
|
|
1501
|
-
...this.scanStatus,
|
|
1502
|
-
pendingAgents: [...this.scanStatus.pendingAgents],
|
|
1503
|
-
scanningAgents: [...this.scanStatus.scanningAgents],
|
|
1504
|
-
completedAgents: [...this.scanStatus.completedAgents],
|
|
1505
|
-
agentStatuses: Object.fromEntries(
|
|
1506
|
-
Object.entries(this.scanStatus.agentStatuses).map(([agentName, status]) => [
|
|
1507
|
-
agentName,
|
|
1508
|
-
{ ...status }
|
|
1509
|
-
])
|
|
1510
|
-
),
|
|
1511
|
-
backfill: {
|
|
1512
|
-
...this.scanStatus.backfill,
|
|
1513
|
-
pendingAgents: [...this.scanStatus.backfill.pendingAgents],
|
|
1514
|
-
completedAgents: [...this.scanStatus.backfill.completedAgents]
|
|
1515
|
-
}
|
|
1516
|
-
};
|
|
2735
|
+
return this.syncEngine.status();
|
|
1517
2736
|
}
|
|
1518
2737
|
subscribe(listener) {
|
|
1519
2738
|
this.listeners.add(listener);
|
|
1520
|
-
return () =>
|
|
1521
|
-
this.listeners.delete(listener);
|
|
1522
|
-
};
|
|
2739
|
+
return () => this.listeners.delete(listener);
|
|
1523
2740
|
}
|
|
1524
2741
|
subscribeScanStatus(listener) {
|
|
1525
|
-
this.
|
|
1526
|
-
return () => {
|
|
1527
|
-
this.scanStatusListeners.delete(listener);
|
|
1528
|
-
};
|
|
2742
|
+
return this.syncEngine.subscribeStatusChanged(listener);
|
|
1529
2743
|
}
|
|
1530
|
-
|
|
2744
|
+
shutdown() {
|
|
2745
|
+
this.shutdownPromise ??= this.performShutdown();
|
|
2746
|
+
return this.shutdownPromise;
|
|
2747
|
+
}
|
|
2748
|
+
async performShutdown() {
|
|
1531
2749
|
this.shuttingDown = true;
|
|
1532
|
-
for (const state of this.refreshStates.values()) {
|
|
1533
|
-
if (state.timer) {
|
|
1534
|
-
clearTimeout(state.timer);
|
|
1535
|
-
state.timer = null;
|
|
1536
|
-
state.timerDeadline = 0;
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
2750
|
if (this.pendingEventTimer) {
|
|
1540
2751
|
clearTimeout(this.pendingEventTimer);
|
|
1541
2752
|
this.pendingEventTimer = null;
|
|
1542
2753
|
}
|
|
1543
|
-
|
|
1544
|
-
clearTimeout(this.backgroundRefreshTimer);
|
|
1545
|
-
this.backgroundRefreshTimer = null;
|
|
1546
|
-
}
|
|
1547
|
-
if (this.searchIndexWorker) {
|
|
1548
|
-
await this.searchIndexWorker.terminate();
|
|
1549
|
-
this.searchIndexWorker = null;
|
|
1550
|
-
}
|
|
1551
|
-
for (const batch of this.pendingSearchIndexJobs) {
|
|
1552
|
-
batch.reject(new Error("Live scan store shut down"));
|
|
1553
|
-
}
|
|
1554
|
-
this.pendingSearchIndexJobs = [];
|
|
2754
|
+
await this.syncEngine.shutdown();
|
|
1555
2755
|
this.pendingEvent = null;
|
|
1556
2756
|
if (this.watcher) {
|
|
1557
2757
|
await this.watcher.dispose();
|
|
1558
2758
|
this.watcher = null;
|
|
1559
2759
|
}
|
|
1560
2760
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
this.
|
|
1567
|
-
}
|
|
1568
|
-
emitNow(event) {
|
|
1569
|
-
for (const listener of this.listeners) {
|
|
1570
|
-
listener(event);
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
emitScanStatus() {
|
|
1574
|
-
const event = this.getScanStatus();
|
|
1575
|
-
for (const listener of this.scanStatusListeners) {
|
|
1576
|
-
listener(event);
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
updateScanStatus(next) {
|
|
1580
|
-
this.scanStatus = next;
|
|
1581
|
-
this.emitScanStatus();
|
|
1582
|
-
}
|
|
1583
|
-
startScanBatch(agentNames, phase) {
|
|
1584
|
-
const uniqueAgentNames = [...new Set(agentNames)];
|
|
1585
|
-
const now = Date.now();
|
|
1586
|
-
const agentStatuses = Object.fromEntries(
|
|
1587
|
-
uniqueAgentNames.map((agentName) => [
|
|
1588
|
-
agentName,
|
|
1589
|
-
{
|
|
1590
|
-
agentName,
|
|
1591
|
-
status: "pending",
|
|
1592
|
-
processed: 0,
|
|
1593
|
-
sessions: this.byAgent[agentName]?.length ?? 0,
|
|
1594
|
-
updatedAt: now
|
|
1595
|
-
}
|
|
1596
|
-
])
|
|
1597
|
-
);
|
|
1598
|
-
this.updateScanStatus({
|
|
1599
|
-
...this.scanStatus,
|
|
1600
|
-
active: uniqueAgentNames.length > 0,
|
|
1601
|
-
phase: uniqueAgentNames.length > 0 ? phase : "idle",
|
|
1602
|
-
pendingAgents: uniqueAgentNames,
|
|
1603
|
-
scanningAgents: [],
|
|
1604
|
-
completedAgents: [],
|
|
1605
|
-
agentStatuses,
|
|
1606
|
-
totalAgents: uniqueAgentNames.length,
|
|
1607
|
-
startedAt: uniqueAgentNames.length > 0 ? now : void 0,
|
|
1608
|
-
updatedAt: now,
|
|
1609
|
-
completedAt: uniqueAgentNames.length > 0 ? void 0 : now
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
|
-
setScanPhase(phase) {
|
|
1613
|
-
if (!this.scanStatus.active) return;
|
|
1614
|
-
this.updateScanStatus({
|
|
1615
|
-
...this.scanStatus,
|
|
1616
|
-
phase,
|
|
1617
|
-
updatedAt: Date.now()
|
|
1618
|
-
});
|
|
1619
|
-
}
|
|
1620
|
-
beginAgentScan(agentName) {
|
|
1621
|
-
if (!this.scanStatus.active) {
|
|
1622
|
-
this.startScanBatch([agentName], "scanning");
|
|
1623
|
-
}
|
|
1624
|
-
const pendingAgents = this.scanStatus.pendingAgents.filter((agent) => agent !== agentName);
|
|
1625
|
-
const scanningAgents = [.../* @__PURE__ */ new Set([...this.scanStatus.scanningAgents, agentName])];
|
|
1626
|
-
const completedAgents = this.scanStatus.completedAgents.filter((agent) => agent !== agentName);
|
|
1627
|
-
const existingStatus = this.scanStatus.agentStatuses[agentName];
|
|
1628
|
-
const agentStatuses = {
|
|
1629
|
-
...this.scanStatus.agentStatuses,
|
|
1630
|
-
[agentName]: {
|
|
1631
|
-
agentName,
|
|
1632
|
-
status: "scanning",
|
|
1633
|
-
total: existingStatus?.total,
|
|
1634
|
-
processed: existingStatus?.processed ?? 0,
|
|
1635
|
-
sessions: existingStatus?.sessions ?? this.byAgent[agentName]?.length ?? 0,
|
|
1636
|
-
startedAt: existingStatus?.startedAt ?? Date.now(),
|
|
1637
|
-
updatedAt: Date.now()
|
|
1638
|
-
}
|
|
1639
|
-
};
|
|
1640
|
-
this.updateScanStatus({
|
|
1641
|
-
...this.scanStatus,
|
|
1642
|
-
active: true,
|
|
1643
|
-
phase: this.scanStatus.phase === "initializing" ? "initializing" : "scanning",
|
|
1644
|
-
pendingAgents,
|
|
1645
|
-
scanningAgents,
|
|
1646
|
-
completedAgents,
|
|
1647
|
-
agentStatuses,
|
|
1648
|
-
totalAgents: Math.max(
|
|
1649
|
-
this.scanStatus.totalAgents,
|
|
1650
|
-
pendingAgents.length + scanningAgents.length
|
|
1651
|
-
),
|
|
1652
|
-
updatedAt: Date.now(),
|
|
1653
|
-
completedAt: void 0
|
|
1654
|
-
});
|
|
1655
|
-
}
|
|
1656
|
-
updateAgentScanProgress(agentName, progress) {
|
|
1657
|
-
const status = this.scanStatus.agentStatuses[agentName];
|
|
1658
|
-
if (!status || status.status !== "scanning") return;
|
|
1659
|
-
this.updateScanStatus({
|
|
1660
|
-
...this.scanStatus,
|
|
1661
|
-
agentStatuses: {
|
|
1662
|
-
...this.scanStatus.agentStatuses,
|
|
1663
|
-
[agentName]: {
|
|
1664
|
-
...status,
|
|
1665
|
-
total: progress.total ?? status.total,
|
|
1666
|
-
processed: progress.processed ?? status.processed,
|
|
1667
|
-
sessions: progress.sessions ?? status.sessions,
|
|
1668
|
-
updatedAt: Date.now()
|
|
1669
|
-
}
|
|
1670
|
-
},
|
|
1671
|
-
updatedAt: Date.now()
|
|
1672
|
-
});
|
|
1673
|
-
}
|
|
1674
|
-
finishAgentScan(agentName) {
|
|
1675
|
-
const pendingAgents = this.scanStatus.pendingAgents.filter((agent) => agent !== agentName);
|
|
1676
|
-
const scanningAgents = this.scanStatus.scanningAgents.filter((agent) => agent !== agentName);
|
|
1677
|
-
const completedAgents = [.../* @__PURE__ */ new Set([...this.scanStatus.completedAgents, agentName])];
|
|
1678
|
-
const active = pendingAgents.length > 0 || scanningAgents.length > 0;
|
|
1679
|
-
const now = Date.now();
|
|
1680
|
-
const previousStatus = this.scanStatus.agentStatuses[agentName];
|
|
1681
|
-
const sessions = this.byAgent[agentName]?.length ?? previousStatus?.sessions ?? 0;
|
|
1682
|
-
const total = previousStatus?.total ?? previousStatus?.processed;
|
|
1683
|
-
this.updateScanStatus({
|
|
1684
|
-
...this.scanStatus,
|
|
1685
|
-
active,
|
|
1686
|
-
phase: active ? "scanning" : "idle",
|
|
1687
|
-
pendingAgents,
|
|
1688
|
-
scanningAgents,
|
|
1689
|
-
completedAgents,
|
|
1690
|
-
agentStatuses: {
|
|
1691
|
-
...this.scanStatus.agentStatuses,
|
|
1692
|
-
[agentName]: {
|
|
1693
|
-
agentName,
|
|
1694
|
-
status: "complete",
|
|
1695
|
-
total,
|
|
1696
|
-
processed: total,
|
|
1697
|
-
sessions,
|
|
1698
|
-
startedAt: previousStatus?.startedAt,
|
|
1699
|
-
updatedAt: now,
|
|
1700
|
-
completedAt: now
|
|
1701
|
-
}
|
|
1702
|
-
},
|
|
1703
|
-
updatedAt: now,
|
|
1704
|
-
completedAt: active ? void 0 : now
|
|
1705
|
-
});
|
|
1706
|
-
}
|
|
1707
|
-
finishScanBatch() {
|
|
1708
|
-
const now = Date.now();
|
|
1709
|
-
this.updateScanStatus({
|
|
1710
|
-
...this.scanStatus,
|
|
1711
|
-
active: false,
|
|
1712
|
-
phase: "idle",
|
|
1713
|
-
pendingAgents: [],
|
|
1714
|
-
scanningAgents: [],
|
|
1715
|
-
agentStatuses: Object.fromEntries(
|
|
1716
|
-
Object.entries(this.scanStatus.agentStatuses).map(([agentName, status]) => [
|
|
1717
|
-
agentName,
|
|
1718
|
-
{ ...status, status: "complete", completedAt: status.completedAt ?? now, updatedAt: now }
|
|
1719
|
-
])
|
|
1720
|
-
),
|
|
1721
|
-
updatedAt: now,
|
|
1722
|
-
completedAt: now
|
|
1723
|
-
});
|
|
1724
|
-
}
|
|
1725
|
-
updateBackfillStatus(patch) {
|
|
1726
|
-
this.updateScanStatus({
|
|
1727
|
-
...this.scanStatus,
|
|
1728
|
-
backfill: { ...this.scanStatus.backfill, ...patch },
|
|
1729
|
-
updatedAt: Date.now()
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1732
|
-
/**
|
|
1733
|
-
* Only FileSystemSessionSource agents pay the O(history) enumeration cost this
|
|
1734
|
-
* guards against: database agents already do a cheap single-file mtime check.
|
|
1735
|
-
* With no startup window configured, the regular refresh path already walks
|
|
1736
|
-
* full history, so backfill would be redundant.
|
|
1737
|
-
*/
|
|
1738
|
-
needsBackfill(agent) {
|
|
1739
|
-
if (this.startupScanOptions.from == null && this.startupScanOptions.to == null) return false;
|
|
1740
|
-
if (!(agent instanceof FileSystemSessionSource)) return false;
|
|
1741
|
-
if (!agent.isAvailable()) return false;
|
|
1742
|
-
const lastSyncAt = getAgentLastFullSyncAt(agent.name);
|
|
1743
|
-
return lastSyncAt == null || Date.now() - lastSyncAt > BACKFILL_INTERVAL_MS;
|
|
1744
|
-
}
|
|
1745
|
-
enqueueBackfill(agentName) {
|
|
1746
|
-
if (this.backfillQueue.includes(agentName) || this.scanStatus.backfill.currentAgent === agentName) {
|
|
1747
|
-
return;
|
|
1748
|
-
}
|
|
1749
|
-
this.backfillQueue.push(agentName);
|
|
1750
|
-
this.updateBackfillStatus({ active: true, pendingAgents: [...this.backfillQueue] });
|
|
1751
|
-
this.pumpBackfillQueue();
|
|
1752
|
-
}
|
|
1753
|
-
pumpBackfillQueue() {
|
|
1754
|
-
if (this.backfillRunning) return;
|
|
1755
|
-
const agentName = this.backfillQueue.shift();
|
|
1756
|
-
if (!agentName) return;
|
|
1757
|
-
this.backfillRunning = true;
|
|
1758
|
-
this.updateBackfillStatus({ currentAgent: agentName, pendingAgents: [...this.backfillQueue] });
|
|
1759
|
-
void this.runBackfill(agentName).finally(() => {
|
|
1760
|
-
this.backfillRunning = false;
|
|
1761
|
-
this.updateBackfillStatus({
|
|
1762
|
-
currentAgent: void 0,
|
|
1763
|
-
completedAgents: [.../* @__PURE__ */ new Set([...this.scanStatus.backfill.completedAgents, agentName])],
|
|
1764
|
-
active: this.backfillQueue.length > 0
|
|
1765
|
-
});
|
|
1766
|
-
this.pumpBackfillQueue();
|
|
1767
|
-
});
|
|
2761
|
+
applySessionsChanged(change) {
|
|
2762
|
+
this.byAgent[change.agentName] = sortSessions(change.sessions);
|
|
2763
|
+
this.rebuildSessions();
|
|
2764
|
+
if (!change.event) return;
|
|
2765
|
+
change.event.totalSessions = this.sessions.length;
|
|
2766
|
+
this.emit(change.event);
|
|
1768
2767
|
}
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
return;
|
|
1775
|
-
}
|
|
1776
|
-
const cached = loadCachedSessions(agentName);
|
|
1777
|
-
const baseline = cached?.sessions ?? this.byAgent[agentName] ?? [];
|
|
1778
|
-
const meta = cached?.meta ?? buildAgentCacheMeta(agent);
|
|
1779
|
-
if (cached) {
|
|
1780
|
-
restoreAgentCacheMeta(agent, cached.meta);
|
|
1781
|
-
}
|
|
1782
|
-
try {
|
|
1783
|
-
const result = await this.scanAgentInWorker(
|
|
1784
|
-
agent,
|
|
1785
|
-
baseline,
|
|
1786
|
-
null,
|
|
1787
|
-
{},
|
|
1788
|
-
{ sourceSync: true, meta }
|
|
1789
|
-
);
|
|
1790
|
-
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
1791
|
-
const fullSessions = attachMissingProjectIdentities(result.sessions);
|
|
1792
|
-
const filtered = this.applyFilters(fullSessions);
|
|
1793
|
-
const diff = buildRefreshDiff(
|
|
1794
|
-
agentName,
|
|
1795
|
-
this.byAgent[agentName] ?? [],
|
|
1796
|
-
filtered,
|
|
1797
|
-
result.changedIds ?? []
|
|
1798
|
-
);
|
|
1799
|
-
this.byAgent[agentName] = sortSessions(filtered);
|
|
1800
|
-
this.rebuildSessions();
|
|
1801
|
-
await this.enqueueSearchIndexJobs("scan.backfill", [
|
|
1802
|
-
{
|
|
1803
|
-
kind: "full",
|
|
1804
|
-
context: "scan.backfill",
|
|
1805
|
-
agentName,
|
|
1806
|
-
sessions: fullSessions,
|
|
1807
|
-
meta: buildAgentCacheMeta(agent),
|
|
1808
|
-
saveCache: true
|
|
1809
|
-
}
|
|
1810
|
-
]);
|
|
1811
|
-
markAgentFullSyncCompleted(agentName);
|
|
1812
|
-
if (diff.event) {
|
|
1813
|
-
diff.event.totalSessions = this.sessions.length;
|
|
1814
|
-
this.emit(diff.event);
|
|
1815
|
-
}
|
|
1816
|
-
appLogger.info("scan.backfill.done", {
|
|
1817
|
-
agent: agentName,
|
|
1818
|
-
duration_ms: Math.round(performance.now() - startedAt),
|
|
1819
|
-
sessions: fullSessions.length,
|
|
1820
|
-
changed: result.changedIds?.length ?? 0
|
|
1821
|
-
});
|
|
1822
|
-
} catch (error) {
|
|
1823
|
-
appLogger.error("scan.backfill.error", { agent: agentName, error });
|
|
1824
|
-
console.error(`[${agentName}] Backfill failed:`, error);
|
|
2768
|
+
emit(event) {
|
|
2769
|
+
if (this.shuttingDown) return;
|
|
2770
|
+
if (this.pendingEvent || event.newSessions > 0) {
|
|
2771
|
+
this.queueEvent(event);
|
|
2772
|
+
return;
|
|
1825
2773
|
}
|
|
2774
|
+
this.emitNow(event);
|
|
2775
|
+
}
|
|
2776
|
+
emitNow(event) {
|
|
2777
|
+
for (const listener of this.listeners) listener(event);
|
|
1826
2778
|
}
|
|
1827
2779
|
queueEvent(event) {
|
|
1828
2780
|
this.pendingEvent = this.pendingEvent ? mergeEvents(this.pendingEvent, event) : event;
|
|
1829
|
-
if (this.pendingEventTimer)
|
|
1830
|
-
return;
|
|
1831
|
-
}
|
|
2781
|
+
if (this.pendingEventTimer) return;
|
|
1832
2782
|
this.pendingEventTimer = setTimeout(() => {
|
|
1833
2783
|
const pending = this.pendingEvent;
|
|
1834
2784
|
this.pendingEvent = null;
|
|
1835
2785
|
this.pendingEventTimer = null;
|
|
1836
|
-
if (pending)
|
|
1837
|
-
this.emitNow(pending);
|
|
1838
|
-
}
|
|
2786
|
+
if (pending) this.emitNow(pending);
|
|
1839
2787
|
}, NEW_SESSION_EVENT_WINDOW_MS);
|
|
1840
2788
|
}
|
|
1841
2789
|
rebuildSessions() {
|
|
1842
2790
|
this.sessions = sortSessions(Object.values(this.byAgent).flat());
|
|
1843
2791
|
}
|
|
1844
|
-
getSearchIndexWorkerUrl() {
|
|
1845
|
-
const workerUrl = new URL("./search-index-worker.js", import.meta.url);
|
|
1846
|
-
if (workerUrl.protocol === "file:" && !existsSync4(fileURLToPath2(workerUrl))) {
|
|
1847
|
-
return null;
|
|
1848
|
-
}
|
|
1849
|
-
return workerUrl;
|
|
1850
|
-
}
|
|
1851
2792
|
getSmartTagWorkerUrl() {
|
|
1852
2793
|
const workerUrl = new URL("./smart-tag-worker.js", import.meta.url);
|
|
1853
|
-
if (workerUrl.protocol === "file:" && !
|
|
1854
|
-
return null;
|
|
1855
|
-
}
|
|
2794
|
+
if (workerUrl.protocol === "file:" && !existsSync5(fileURLToPath3(workerUrl))) return null;
|
|
1856
2795
|
return workerUrl;
|
|
1857
2796
|
}
|
|
1858
|
-
getScanRefreshWorkerUrl() {
|
|
1859
|
-
return new URL("./scan-refresh-worker.js", import.meta.url);
|
|
1860
|
-
}
|
|
1861
|
-
scanAgentInWorker(agent, previousSessions, changedIds, scanOptions, workerOptions = {}) {
|
|
1862
|
-
const workerUrl = this.getScanRefreshWorkerUrl();
|
|
1863
|
-
return new Promise((resolve4, reject) => {
|
|
1864
|
-
const worker = new Worker(workerUrl, {
|
|
1865
|
-
workerData: {
|
|
1866
|
-
agentName: agent.name,
|
|
1867
|
-
previousSessions,
|
|
1868
|
-
changedIds,
|
|
1869
|
-
sourceSync: workerOptions.sourceSync,
|
|
1870
|
-
scanOptions,
|
|
1871
|
-
meta: workerOptions.meta ?? buildAgentCacheMeta(agent)
|
|
1872
|
-
}
|
|
1873
|
-
});
|
|
1874
|
-
worker.unref();
|
|
1875
|
-
let settled = false;
|
|
1876
|
-
const finish = (callback) => {
|
|
1877
|
-
if (settled) return;
|
|
1878
|
-
settled = true;
|
|
1879
|
-
void worker.terminate();
|
|
1880
|
-
callback();
|
|
1881
|
-
};
|
|
1882
|
-
worker.on("message", (message) => {
|
|
1883
|
-
if (message.type === "progress") {
|
|
1884
|
-
this.updateAgentScanProgress(agent.name, message.progress);
|
|
1885
|
-
return;
|
|
1886
|
-
}
|
|
1887
|
-
if (message.type === "done") {
|
|
1888
|
-
finish(
|
|
1889
|
-
() => resolve4({
|
|
1890
|
-
sessions: message.sessions,
|
|
1891
|
-
meta: message.meta,
|
|
1892
|
-
changedIds: message.changedIds
|
|
1893
|
-
})
|
|
1894
|
-
);
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
|
-
finish(() => reject(new Error(message.error)));
|
|
1898
|
-
});
|
|
1899
|
-
worker.once("error", (error) => {
|
|
1900
|
-
finish(() => reject(error));
|
|
1901
|
-
});
|
|
1902
|
-
worker.once("exit", (code) => {
|
|
1903
|
-
if (!settled && code !== 0) {
|
|
1904
|
-
finish(() => reject(new Error(`Scan refresh worker exited with code ${code}`)));
|
|
1905
|
-
}
|
|
1906
|
-
});
|
|
1907
|
-
});
|
|
1908
|
-
}
|
|
1909
|
-
buildFullSearchIndexJobs(context) {
|
|
1910
|
-
return this.agents.map((agent) => {
|
|
1911
|
-
const cached = loadCachedSessions(agent.name);
|
|
1912
|
-
if (cached) {
|
|
1913
|
-
return {
|
|
1914
|
-
kind: "full",
|
|
1915
|
-
context,
|
|
1916
|
-
agentName: agent.name,
|
|
1917
|
-
sessions: cached.sessions,
|
|
1918
|
-
meta: cached.meta
|
|
1919
|
-
};
|
|
1920
|
-
}
|
|
1921
|
-
return {
|
|
1922
|
-
kind: "full",
|
|
1923
|
-
context,
|
|
1924
|
-
agentName: agent.name,
|
|
1925
|
-
sessions: this.byAgent[agent.name] ?? [],
|
|
1926
|
-
meta: buildAgentCacheMeta(agent)
|
|
1927
|
-
};
|
|
1928
|
-
});
|
|
1929
|
-
}
|
|
1930
|
-
enqueueSearchIndexJobs(context, jobs) {
|
|
1931
|
-
if (jobs.length === 0) return Promise.resolve();
|
|
1932
|
-
return new Promise((resolve4, reject) => {
|
|
1933
|
-
const batch = { context, jobs, resolve: resolve4, reject };
|
|
1934
|
-
if (this.searchIndexWorker) {
|
|
1935
|
-
this.pendingSearchIndexJobs.push(batch);
|
|
1936
|
-
appLogger.debug("search_index.worker_queued", {
|
|
1937
|
-
context,
|
|
1938
|
-
jobs: jobs.length,
|
|
1939
|
-
pending_jobs: this.pendingSearchIndexJobs.length
|
|
1940
|
-
});
|
|
1941
|
-
return;
|
|
1942
|
-
}
|
|
1943
|
-
this.startSearchIndexJobBatch(batch);
|
|
1944
|
-
});
|
|
1945
|
-
}
|
|
1946
|
-
startSearchIndexJobBatch(batch) {
|
|
1947
|
-
const workerUrl = this.getSearchIndexWorkerUrl();
|
|
1948
|
-
if (!workerUrl) {
|
|
1949
|
-
appLogger.warn("search_index.worker_missing", { context: batch.context });
|
|
1950
|
-
batch.resolve();
|
|
1951
|
-
return;
|
|
1952
|
-
}
|
|
1953
|
-
let settled = false;
|
|
1954
|
-
const worker = new Worker(workerUrl, {
|
|
1955
|
-
workerData: {
|
|
1956
|
-
context: batch.context,
|
|
1957
|
-
jobs: batch.jobs,
|
|
1958
|
-
agentNames: [],
|
|
1959
|
-
sessionsByAgent: {},
|
|
1960
|
-
metaByAgent: {},
|
|
1961
|
-
skipFtsIntegrityCheck: this.ftsIntegrityChecked
|
|
1962
|
-
}
|
|
1963
|
-
});
|
|
1964
|
-
worker.unref();
|
|
1965
|
-
this.searchIndexWorker = worker;
|
|
1966
|
-
worker.on("message", (message) => {
|
|
1967
|
-
if (message.type === "sync-result") {
|
|
1968
|
-
logSearchIndexSync(message.context, message.result);
|
|
1969
|
-
} else if (message.type === "done") {
|
|
1970
|
-
appLogger.info(`${message.context}.done`, {
|
|
1971
|
-
duration_ms: Math.round(message.durationMs),
|
|
1972
|
-
sessions: message.sessions
|
|
1973
|
-
});
|
|
1974
|
-
settled = true;
|
|
1975
|
-
this.ftsIntegrityChecked = true;
|
|
1976
|
-
batch.resolve();
|
|
1977
|
-
}
|
|
1978
|
-
});
|
|
1979
|
-
worker.on("error", (error) => {
|
|
1980
|
-
appLogger.error("search_index.worker_error", { context: batch.context, error });
|
|
1981
|
-
if (!settled) {
|
|
1982
|
-
settled = true;
|
|
1983
|
-
batch.reject(error);
|
|
1984
|
-
}
|
|
1985
|
-
});
|
|
1986
|
-
worker.on("exit", (code) => {
|
|
1987
|
-
this.searchIndexWorker = null;
|
|
1988
|
-
if (code !== 0) {
|
|
1989
|
-
appLogger.warn("search_index.worker_exit", { context: batch.context, code });
|
|
1990
|
-
if (!settled) {
|
|
1991
|
-
settled = true;
|
|
1992
|
-
batch.reject(new Error(`Search index worker exited with code ${code}`));
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
if (this.pendingSearchIndexJobs.length > 0) {
|
|
1996
|
-
const pendingBatch = this.pendingSearchIndexJobs.shift();
|
|
1997
|
-
this.startSearchIndexJobBatch(pendingBatch);
|
|
1998
|
-
}
|
|
1999
|
-
});
|
|
2000
|
-
}
|
|
2001
2797
|
applyScanResult(result) {
|
|
2002
|
-
const knownAgents = createRegisteredAgents();
|
|
2003
2798
|
const agentMap = /* @__PURE__ */ new Map();
|
|
2004
2799
|
const allowedAgents = this.getAllowedAgents();
|
|
2005
|
-
for (const agent of result.agents)
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
for (const agent of knownAgents) {
|
|
2009
|
-
if (!agentMap.has(agent.name)) {
|
|
2010
|
-
agentMap.set(agent.name, agent);
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
this.agents = [...agentMap.values()].filter((agent) => {
|
|
2014
|
-
if (!allowedAgents) {
|
|
2015
|
-
return true;
|
|
2016
|
-
}
|
|
2017
|
-
return allowedAgents.has(agent.name.toLowerCase());
|
|
2018
|
-
});
|
|
2019
|
-
this.byAgent = {};
|
|
2020
|
-
for (const agent of this.agents) {
|
|
2021
|
-
this.byAgent[agent.name] = sortSessions(result.byAgent[agent.name] ?? []);
|
|
2022
|
-
this.getRefreshState(agent.name).lastRefreshAt = result.cacheTimestamps?.[agent.name] ?? Date.now();
|
|
2800
|
+
for (const agent of result.agents) agentMap.set(agent.name, agent);
|
|
2801
|
+
for (const agent of createRegisteredAgents()) {
|
|
2802
|
+
if (!agentMap.has(agent.name)) agentMap.set(agent.name, agent);
|
|
2023
2803
|
}
|
|
2804
|
+
this.agents = [...agentMap.values()].filter(
|
|
2805
|
+
(agent) => !allowedAgents || allowedAgents.has(agent.name.toLowerCase())
|
|
2806
|
+
);
|
|
2807
|
+
this.byAgent = Object.fromEntries(
|
|
2808
|
+
this.agents.map((agent) => [agent.name, sortSessions(result.byAgent[agent.name] ?? [])])
|
|
2809
|
+
);
|
|
2024
2810
|
this.rebuildSessions();
|
|
2025
2811
|
}
|
|
2026
2812
|
getAllowedAgents() {
|
|
2027
|
-
if (!this.scanOptions.agents?.length)
|
|
2028
|
-
return null;
|
|
2029
|
-
}
|
|
2813
|
+
if (!this.scanOptions.agents?.length) return null;
|
|
2030
2814
|
return new Set(this.scanOptions.agents.map((agent) => agent.toLowerCase()));
|
|
2031
2815
|
}
|
|
2032
|
-
applyFilters(sessions) {
|
|
2033
|
-
return filterSessions(sessions, { ...this.scanOptions, ...this.startupScanOptions });
|
|
2034
|
-
}
|
|
2035
|
-
async refreshInitialIndex() {
|
|
2036
|
-
const startedAt = performance.now();
|
|
2037
|
-
const context = "scan.initial.background";
|
|
2038
|
-
try {
|
|
2039
|
-
await this.enqueueSearchIndexJobs(context, this.buildFullSearchIndexJobs(context));
|
|
2040
|
-
appLogger.info(`${context}.complete`, {
|
|
2041
|
-
duration_ms: Math.round(performance.now() - startedAt),
|
|
2042
|
-
sessions: this.sessions.length
|
|
2043
|
-
});
|
|
2044
|
-
} catch (error) {
|
|
2045
|
-
if (this.shuttingDown) {
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
appLogger.error(`${context}.error`, { error });
|
|
2049
|
-
console.error("[search] Background index sync failed:", error);
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
/**
|
|
2053
|
-
* Throttles rather than debounces: a pending timer only gets replaced by a
|
|
2054
|
-
* request with an earlier deadline. Plain debounce (reset on every call)
|
|
2055
|
-
* would let a steady stream of events — each arriving before the adaptive
|
|
2056
|
-
* backoff elapses — push the deadline out forever and starve the refresh.
|
|
2057
|
-
*/
|
|
2058
|
-
scheduleRefresh(agentName, delayMs = REFRESH_DEBOUNCE_MS) {
|
|
2059
|
-
const state = this.getRefreshState(agentName);
|
|
2060
|
-
const adaptiveDelayMs = Math.min(
|
|
2061
|
-
state.lastRefreshDurationMs * ADAPTIVE_REFRESH_DELAY_MULTIPLIER,
|
|
2062
|
-
MAX_ADAPTIVE_REFRESH_DELAY_MS
|
|
2063
|
-
);
|
|
2064
|
-
const effectiveDelayMs = Math.max(delayMs, adaptiveDelayMs);
|
|
2065
|
-
const deadline = Date.now() + effectiveDelayMs;
|
|
2066
|
-
if (state.timer !== null) {
|
|
2067
|
-
if (deadline >= state.timerDeadline) {
|
|
2068
|
-
return;
|
|
2069
|
-
}
|
|
2070
|
-
clearTimeout(state.timer);
|
|
2071
|
-
}
|
|
2072
|
-
appLogger.debug("scan.refresh.schedule", { agent: agentName, delay_ms: effectiveDelayMs });
|
|
2073
|
-
state.timerDeadline = deadline;
|
|
2074
|
-
state.timer = setTimeout(() => {
|
|
2075
|
-
state.timer = null;
|
|
2076
|
-
void this.refreshAgent(agentName);
|
|
2077
|
-
}, effectiveDelayMs);
|
|
2078
|
-
}
|
|
2079
|
-
async refreshAgent(agentName) {
|
|
2080
|
-
const state = this.getRefreshState(agentName);
|
|
2081
|
-
if (state.inFlight) {
|
|
2082
|
-
appLogger.debug("scan.refresh.pending", { agent: agentName });
|
|
2083
|
-
state.pendingRerun = true;
|
|
2084
|
-
return;
|
|
2085
|
-
}
|
|
2086
|
-
state.inFlight = true;
|
|
2087
|
-
this.beginAgentScan(agentName);
|
|
2088
|
-
try {
|
|
2089
|
-
await this.runRefresh(agentName);
|
|
2090
|
-
} catch (error) {
|
|
2091
|
-
appLogger.error("scan.refresh.error", { agent: agentName, error });
|
|
2092
|
-
console.error(`[${agentName}] Session refresh failed:`, error);
|
|
2093
|
-
} finally {
|
|
2094
|
-
state.inFlight = false;
|
|
2095
|
-
this.finishAgentScan(agentName);
|
|
2096
|
-
if (state.pendingRerun) {
|
|
2097
|
-
state.pendingRerun = false;
|
|
2098
|
-
this.scheduleRefresh(agentName, PENDING_REFRESH_DELAY_MS);
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
async runRefresh(agentName) {
|
|
2103
|
-
const startedAt = performance.now();
|
|
2104
|
-
const state = this.getRefreshState(agentName);
|
|
2105
|
-
const pendingPathCount = state.pendingPathCount;
|
|
2106
|
-
state.pendingPathCount = 0;
|
|
2107
|
-
const agent = this.agents.find((item) => item.name === agentName);
|
|
2108
|
-
if (!agent) {
|
|
2109
|
-
appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
|
|
2110
|
-
return;
|
|
2111
|
-
}
|
|
2112
|
-
const previousSessions = this.byAgent[agentName] ?? [];
|
|
2113
|
-
const cached = loadCachedSessions(agentName);
|
|
2114
|
-
const refreshBaseline = cached?.sessions ?? previousSessions;
|
|
2115
|
-
const cacheTimestamp = cached?.timestamp ?? state.lastRefreshAt;
|
|
2116
|
-
if (cached) {
|
|
2117
|
-
restoreAgentCacheMeta(agent, cached.meta);
|
|
2118
|
-
}
|
|
2119
|
-
const isInitialized = isAgentCacheInitialized(agentName);
|
|
2120
|
-
let nextSessions = previousSessions;
|
|
2121
|
-
let fullScanSessions = null;
|
|
2122
|
-
let preciseChangedIds = null;
|
|
2123
|
-
let usedIncrementalScan = false;
|
|
2124
|
-
let persistenceDiff = null;
|
|
2125
|
-
let availabilityDuration = 0;
|
|
2126
|
-
let checkDuration = 0;
|
|
2127
|
-
let scanDuration = 0;
|
|
2128
|
-
let filterDuration = 0;
|
|
2129
|
-
let diffDuration = 0;
|
|
2130
|
-
let persistDuration = 0;
|
|
2131
|
-
let searchIndexDuration = 0;
|
|
2132
|
-
let persistentJobKind;
|
|
2133
|
-
const availabilityStartedAt = performance.now();
|
|
2134
|
-
const isAvailable = agent.isAvailable();
|
|
2135
|
-
availabilityDuration = performance.now() - availabilityStartedAt;
|
|
2136
|
-
if (!isAvailable) {
|
|
2137
|
-
nextSessions = [];
|
|
2138
|
-
state.lastRefreshAt = Date.now();
|
|
2139
|
-
} else if (!isInitialized) {
|
|
2140
|
-
this.setScanPhase("initializing");
|
|
2141
|
-
const scanStartedAt = performance.now();
|
|
2142
|
-
const result = await this.scanAgentInWorker(
|
|
2143
|
-
agent,
|
|
2144
|
-
previousSessions,
|
|
2145
|
-
null,
|
|
2146
|
-
this.startupScanOptions
|
|
2147
|
-
);
|
|
2148
|
-
nextSessions = result.sessions;
|
|
2149
|
-
agent.setSessionMetaMap?.(new Map(Object.entries(result.meta)));
|
|
2150
|
-
fullScanSessions = attachMissingProjectIdentities(nextSessions);
|
|
2151
|
-
nextSessions = fullScanSessions;
|
|
2152
|
-
scanDuration = performance.now() - scanStartedAt;
|
|
2153
|
-
state.lastRefreshAt = Date.now();
|
|
2154
|
-
} else if (cached && agent instanceof FileSystemSessionSource) {
|
|
2155
|
-
const scanStartedAt = performance.now();
|
|
2156
|
-
const result = await this.scanAgentInWorker(
|
|
2157
|
-
agent,
|
|
2158
|
-
cached.sessions,
|
|
2159
|
-
null,
|
|
2160
|
-
this.startupScanOptions,
|
|
2161
|
-
{
|
|
2162
|
-
sourceSync: true,
|
|
2163
|
-
meta: cached.meta
|
|
2164
|
-
}
|
|
2165
|
-
);
|
|
2166
|
-
nextSessions = result.sessions;
|
|
2167
|
-
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2168
|
-
preciseChangedIds = result.changedIds ?? [];
|
|
2169
|
-
usedIncrementalScan = true;
|
|
2170
|
-
persistenceDiff = buildRefreshDiff(
|
|
2171
|
-
agentName,
|
|
2172
|
-
cached.sessions,
|
|
2173
|
-
attachMissingProjectIdentities(nextSessions),
|
|
2174
|
-
preciseChangedIds
|
|
2175
|
-
);
|
|
2176
|
-
scanDuration = performance.now() - scanStartedAt;
|
|
2177
|
-
state.lastRefreshAt = Date.now();
|
|
2178
|
-
if (preciseChangedIds.length === 0) {
|
|
2179
|
-
appLogger.debug("scan.refresh.unchanged", {
|
|
2180
|
-
agent: agentName,
|
|
2181
|
-
duration_ms: Math.round(performance.now() - startedAt)
|
|
2182
|
-
});
|
|
2183
|
-
}
|
|
2184
|
-
} else if (refreshBaseline.length > 0) {
|
|
2185
|
-
const checkStartedAt = performance.now();
|
|
2186
|
-
const checkResult = await Promise.resolve(
|
|
2187
|
-
agent.checkForChanges(cacheTimestamp, refreshBaseline)
|
|
2188
|
-
);
|
|
2189
|
-
checkDuration = performance.now() - checkStartedAt;
|
|
2190
|
-
state.lastRefreshAt = checkResult.timestamp;
|
|
2191
|
-
if (!checkResult.hasChanges) {
|
|
2192
|
-
appLogger.debug("scan.refresh.unchanged", {
|
|
2193
|
-
agent: agentName,
|
|
2194
|
-
duration_ms: Math.round(performance.now() - startedAt)
|
|
2195
|
-
});
|
|
2196
|
-
return;
|
|
2197
|
-
}
|
|
2198
|
-
preciseChangedIds = checkResult.changedIds ?? null;
|
|
2199
|
-
usedIncrementalScan = Array.isArray(checkResult.changedIds);
|
|
2200
|
-
const scanStartedAt = performance.now();
|
|
2201
|
-
nextSessions = await Promise.resolve(
|
|
2202
|
-
agent.incrementalScan(refreshBaseline, checkResult.changedIds ?? [])
|
|
2203
|
-
);
|
|
2204
|
-
const nextBaseline = attachMissingProjectIdentities(nextSessions);
|
|
2205
|
-
persistenceDiff = buildRefreshDiff(
|
|
2206
|
-
agentName,
|
|
2207
|
-
refreshBaseline,
|
|
2208
|
-
nextBaseline,
|
|
2209
|
-
preciseChangedIds ?? []
|
|
2210
|
-
);
|
|
2211
|
-
nextSessions = nextBaseline;
|
|
2212
|
-
scanDuration = performance.now() - scanStartedAt;
|
|
2213
|
-
} else {
|
|
2214
|
-
const scanStartedAt = performance.now();
|
|
2215
|
-
const result = await this.scanAgentInWorker(agent, previousSessions, null, {});
|
|
2216
|
-
nextSessions = result.sessions;
|
|
2217
|
-
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2218
|
-
fullScanSessions = attachMissingProjectIdentities(nextSessions);
|
|
2219
|
-
nextSessions = fullScanSessions;
|
|
2220
|
-
scanDuration = performance.now() - scanStartedAt;
|
|
2221
|
-
state.lastRefreshAt = Date.now();
|
|
2222
|
-
}
|
|
2223
|
-
nextSessions = attachMissingProjectIdentities(nextSessions);
|
|
2224
|
-
const filterStartedAt = performance.now();
|
|
2225
|
-
nextSessions = this.applyFilters(nextSessions);
|
|
2226
|
-
filterDuration = performance.now() - filterStartedAt;
|
|
2227
|
-
const diffStartedAt = performance.now();
|
|
2228
|
-
const diff = buildRefreshDiff(
|
|
2229
|
-
agentName,
|
|
2230
|
-
previousSessions,
|
|
2231
|
-
nextSessions,
|
|
2232
|
-
preciseChangedIds ?? []
|
|
2233
|
-
);
|
|
2234
|
-
diffDuration = performance.now() - diffStartedAt;
|
|
2235
|
-
const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
|
|
2236
|
-
const canPersistIncrementally = usedIncrementalScan;
|
|
2237
|
-
const persistentChanges = persistenceDiff?.changedSessions ?? diff.changedSessions;
|
|
2238
|
-
const persistentRemovedSessionIds = persistenceDiff?.removedSessionIds ?? diff.removedSessionIds;
|
|
2239
|
-
const changedSessionIds = canPersistIncrementally ? new Set(persistentChanges.map(({ session }) => session.id)) : void 0;
|
|
2240
|
-
const cacheMeta = buildAgentCacheMeta(agent, changedSessionIds);
|
|
2241
|
-
const persistStartedAt = performance.now();
|
|
2242
|
-
const persistentJob = canPersistIncrementally ? {
|
|
2243
|
-
kind: "changes",
|
|
2244
|
-
context: "scan.refresh",
|
|
2245
|
-
agentName,
|
|
2246
|
-
changes: persistentChanges,
|
|
2247
|
-
removedSessionIds: persistentRemovedSessionIds,
|
|
2248
|
-
meta: cacheMeta,
|
|
2249
|
-
...searchIndexOptions ? { searchIndexOptions } : {}
|
|
2250
|
-
} : fullScanSessions ? {
|
|
2251
|
-
kind: "full",
|
|
2252
|
-
context: "scan.refresh",
|
|
2253
|
-
agentName,
|
|
2254
|
-
sessions: fullScanSessions,
|
|
2255
|
-
meta: buildAgentCacheMeta(agent),
|
|
2256
|
-
saveCache: true,
|
|
2257
|
-
...searchIndexOptions ? { searchIndexOptions } : {}
|
|
2258
|
-
} : null;
|
|
2259
|
-
if (persistentJob) {
|
|
2260
|
-
persistentJobKind = persistentJob.kind;
|
|
2261
|
-
const persist = this.enqueueSearchIndexJobs("scan.refresh", [persistentJob]);
|
|
2262
|
-
if (!isInitialized && persistentJob.kind === "full") {
|
|
2263
|
-
await persist;
|
|
2264
|
-
} else {
|
|
2265
|
-
void persist.catch((error) => {
|
|
2266
|
-
appLogger.error("scan.refresh.persist.error", { agent: agentName, error });
|
|
2267
|
-
console.error(`[${agentName}] Session persistence failed:`, error);
|
|
2268
|
-
});
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
|
-
persistDuration = performance.now() - persistStartedAt;
|
|
2272
|
-
const searchIndexStartedAt = performance.now();
|
|
2273
|
-
searchIndexDuration = performance.now() - searchIndexStartedAt;
|
|
2274
|
-
logSearchIndexSync("scan.refresh", null, { pending_paths: pendingPathCount });
|
|
2275
|
-
const event = diff.event;
|
|
2276
|
-
this.byAgent[agentName] = sortSessions(nextSessions);
|
|
2277
|
-
this.rebuildSessions();
|
|
2278
|
-
if (event) {
|
|
2279
|
-
event.totalSessions = this.sessions.length;
|
|
2280
|
-
this.emit(event);
|
|
2281
|
-
}
|
|
2282
|
-
const totalDurationMs = performance.now() - startedAt;
|
|
2283
|
-
state.lastRefreshDurationMs = totalDurationMs;
|
|
2284
|
-
appLogger.info("scan.refresh.done", {
|
|
2285
|
-
agent: agentName,
|
|
2286
|
-
duration_ms: Math.round(totalDurationMs),
|
|
2287
|
-
sessions: nextSessions.length,
|
|
2288
|
-
new_sessions: event?.newSessions ?? 0,
|
|
2289
|
-
updated_sessions: event?.updatedSessions ?? 0,
|
|
2290
|
-
removed_sessions: event?.removedSessions ?? 0,
|
|
2291
|
-
pending_paths: pendingPathCount,
|
|
2292
|
-
availability_ms: Math.round(availabilityDuration),
|
|
2293
|
-
check_ms: Math.round(checkDuration),
|
|
2294
|
-
scan_ms: Math.round(scanDuration),
|
|
2295
|
-
filter_ms: Math.round(filterDuration),
|
|
2296
|
-
diff_ms: Math.round(diffDuration),
|
|
2297
|
-
persist_ms: Math.round(persistDuration),
|
|
2298
|
-
search_index_ms: Math.round(searchIndexDuration),
|
|
2299
|
-
persistent_index_worker_job: persistentJobKind,
|
|
2300
|
-
persistent_index_skipped: !persistentJob || void 0
|
|
2301
|
-
});
|
|
2302
|
-
}
|
|
2303
2816
|
};
|
|
2304
2817
|
|
|
2305
2818
|
// src/output.ts
|
|
@@ -2308,8 +2821,8 @@ import { consola } from "consola";
|
|
|
2308
2821
|
// src/version.ts
|
|
2309
2822
|
import { readFileSync } from "fs";
|
|
2310
2823
|
import { resolve as resolve3, dirname as dirname3 } from "path";
|
|
2311
|
-
import { fileURLToPath as
|
|
2312
|
-
var __dirname = dirname3(
|
|
2824
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2825
|
+
var __dirname = dirname3(fileURLToPath4(import.meta.url));
|
|
2313
2826
|
var pkg = JSON.parse(readFileSync(resolve3(__dirname, "../package.json"), "utf-8"));
|
|
2314
2827
|
var VERSION = pkg.version;
|
|
2315
2828
|
|
|
@@ -2347,18 +2860,23 @@ function hasExplicitPortArg(argv) {
|
|
|
2347
2860
|
}
|
|
2348
2861
|
|
|
2349
2862
|
// src/index.ts
|
|
2350
|
-
function parseDateToTimestamp(dateStr) {
|
|
2351
|
-
const date = new Date(dateStr);
|
|
2352
|
-
if (Number.isNaN(date.getTime())) {
|
|
2353
|
-
throw new Error(`Invalid date: ${dateStr}`);
|
|
2354
|
-
}
|
|
2355
|
-
return date.getTime();
|
|
2356
|
-
}
|
|
2357
2863
|
function parseSessionUri(uri) {
|
|
2358
2864
|
const match = uri.match(/^([a-z]+):\/\/(.+)$/i);
|
|
2359
2865
|
if (!match) return null;
|
|
2360
2866
|
return { agent: match[1], sessionId: match[2] };
|
|
2361
2867
|
}
|
|
2868
|
+
function appendStartupPath(startupUrl, path) {
|
|
2869
|
+
const url = new URL(startupUrl);
|
|
2870
|
+
url.pathname = path;
|
|
2871
|
+
return url.toString();
|
|
2872
|
+
}
|
|
2873
|
+
function redactStartupUrl(startupUrl) {
|
|
2874
|
+
const url = new URL(startupUrl);
|
|
2875
|
+
for (const key of url.searchParams.keys()) {
|
|
2876
|
+
url.searchParams.set(key, "[redacted]");
|
|
2877
|
+
}
|
|
2878
|
+
return url.toString();
|
|
2879
|
+
}
|
|
2362
2880
|
var main = defineCommand({
|
|
2363
2881
|
meta: {
|
|
2364
2882
|
name: "codesesh",
|
|
@@ -2377,6 +2895,11 @@ var main = defineCommand({
|
|
|
2377
2895
|
description: "HTTP server bind address (default 127.0.0.1, local access only)",
|
|
2378
2896
|
default: "127.0.0.1"
|
|
2379
2897
|
},
|
|
2898
|
+
"remote-access": {
|
|
2899
|
+
type: "boolean",
|
|
2900
|
+
description: "Allow authenticated access when binding to a non-loopback address",
|
|
2901
|
+
default: false
|
|
2902
|
+
},
|
|
2380
2903
|
agent: {
|
|
2381
2904
|
type: "string",
|
|
2382
2905
|
alias: "a",
|
|
@@ -2441,6 +2964,14 @@ var main = defineCommand({
|
|
|
2441
2964
|
const trace = args.trace;
|
|
2442
2965
|
const useCache = args.cache;
|
|
2443
2966
|
const clearCache = args["clear-cache"];
|
|
2967
|
+
const hostname = args.host;
|
|
2968
|
+
const remoteAccess = args["remote-access"];
|
|
2969
|
+
if (!isLoopbackHostname(hostname) && !remoteAccess) {
|
|
2970
|
+
console.error(
|
|
2971
|
+
`Refusing to expose CodeSesh on ${hostname} without authentication. Add --remote-access to continue.`
|
|
2972
|
+
);
|
|
2973
|
+
process.exit(1);
|
|
2974
|
+
}
|
|
2444
2975
|
if (trace) {
|
|
2445
2976
|
perf.enable();
|
|
2446
2977
|
}
|
|
@@ -2454,7 +2985,7 @@ var main = defineCommand({
|
|
|
2454
2985
|
log_path: appLogger.getLogPath()
|
|
2455
2986
|
});
|
|
2456
2987
|
if (clearCache) {
|
|
2457
|
-
const { clearCache: clear } = await import("./dist-
|
|
2988
|
+
const { clearCache: clear } = await import("./dist-C3KC3XKJ.js");
|
|
2458
2989
|
clear();
|
|
2459
2990
|
appLogger.info("cache.clear");
|
|
2460
2991
|
console.log("Cache cleared.");
|
|
@@ -2472,27 +3003,26 @@ var main = defineCommand({
|
|
|
2472
3003
|
if (cwdFilter === ".") {
|
|
2473
3004
|
cwdFilter = process.cwd();
|
|
2474
3005
|
}
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
listDefaultDays = 0;
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
const listDefaultTo = args.to ? parseDateToTimestamp(args.to) : void 0;
|
|
3006
|
+
const {
|
|
3007
|
+
from: listDefaultFrom,
|
|
3008
|
+
to: listDefaultTo,
|
|
3009
|
+
days: listDefaultDays
|
|
3010
|
+
} = resolveTimeWindow({
|
|
3011
|
+
mode: "cli",
|
|
3012
|
+
from: args.from,
|
|
3013
|
+
to: args.to,
|
|
3014
|
+
days: args.days
|
|
3015
|
+
});
|
|
2489
3016
|
const scanOptions = {
|
|
2490
3017
|
agents: targetSession ? [targetSession.agent] : args.agent ? args.agent.split(",").map((a) => a.trim()) : void 0,
|
|
2491
3018
|
cwd: cwdFilter,
|
|
2492
3019
|
useCache
|
|
2493
3020
|
};
|
|
2494
3021
|
const startupScanOptions = targetSession || jsonOnly ? {} : { from: listDefaultFrom, to: listDefaultTo };
|
|
2495
|
-
const store = new LiveScanStore(
|
|
3022
|
+
const store = new LiveScanStore({
|
|
3023
|
+
watchEnabled: !jsonOnly,
|
|
3024
|
+
scanOptions,
|
|
3025
|
+
startupScanOptions,
|
|
2496
3026
|
deferInitialRefresh: !jsonOnly
|
|
2497
3027
|
});
|
|
2498
3028
|
await store.initialize();
|
|
@@ -2544,7 +3074,8 @@ var main = defineCommand({
|
|
|
2544
3074
|
defaultSessionTo: listDefaultTo,
|
|
2545
3075
|
defaultSessionDays: listDefaultDays,
|
|
2546
3076
|
portFallbackAttempts: explicitPort ? 1 : DEFAULT_PORT_FALLBACK_ATTEMPTS,
|
|
2547
|
-
hostname
|
|
3077
|
+
hostname,
|
|
3078
|
+
remoteAccess
|
|
2548
3079
|
});
|
|
2549
3080
|
} catch (error) {
|
|
2550
3081
|
console.error(getServerStartupErrorMessage(error, port));
|
|
@@ -2571,14 +3102,14 @@ var main = defineCommand({
|
|
|
2571
3102
|
console.log(` ${url}`);
|
|
2572
3103
|
console.log("");
|
|
2573
3104
|
appLogger.info("cli.ready", {
|
|
2574
|
-
url,
|
|
3105
|
+
url: redactStartupUrl(url),
|
|
2575
3106
|
duration_ms: Math.round(performance.now() - startedAt),
|
|
2576
3107
|
log_path: appLogger.getLogPath()
|
|
2577
3108
|
});
|
|
2578
3109
|
if (!noOpen) {
|
|
2579
3110
|
const open = (await import("open")).default;
|
|
2580
|
-
const targetUrl = targetSession ?
|
|
2581
|
-
appLogger.info("browser.open", { url: targetUrl });
|
|
3111
|
+
const targetUrl = targetSession ? appendStartupPath(url, `/${targetSession.agent.toLowerCase()}/${targetSession.sessionId}`) : url;
|
|
3112
|
+
appLogger.info("browser.open", { url: redactStartupUrl(targetUrl) });
|
|
2582
3113
|
await open(targetUrl);
|
|
2583
3114
|
}
|
|
2584
3115
|
}
|