codesesh 0.11.0 → 0.13.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 +7 -1
- package/dist/{chunk-BIXOP5QX.js → chunk-BV65IEWZ.js} +429 -192
- package/dist/chunk-BV65IEWZ.js.map +1 -0
- package/dist/{dist-T737I76S.js → dist-VD54GDPO.js} +22 -2
- package/dist/index.js +1058 -442
- package/dist/index.js.map +1 -1
- package/dist/scan-refresh-worker.js +35 -9
- package/dist/scan-refresh-worker.js.map +1 -1
- package/dist/search-index-worker.js +6 -1
- package/dist/search-index-worker.js.map +1 -1
- package/dist/smart-tag-worker.js +1 -1
- package/dist/web/assets/index-ZikfLnUo.js +113 -0
- package/dist/web/assets/index-y6Zx3LT1.css +2 -0
- package/dist/web/assets/markdown-dR4kAewZ.js +14 -0
- package/dist/web/assets/react-DBHwgW7V.js +1821 -0
- package/dist/web/assets/syntax-C_RLNYQn.js +6 -0
- package/dist/web/assets/vendor-bXsb4Kz4.js +40 -0
- package/dist/web/index.html +6 -6
- package/package.json +9 -8
- package/dist/chunk-BIXOP5QX.js.map +0 -1
- package/dist/web/assets/index-DlZEB-Op.css +0 -2
- package/dist/web/assets/index-O6YSX0s1.js +0 -113
- package/dist/web/assets/markdown-Bm__FT1K.js +0 -14
- package/dist/web/assets/react-CGUTVnhO.js +0 -1821
- package/dist/web/assets/syntax-Cjw-OADI.js +0 -6
- package/dist/web/assets/vendor-CR3FdUHs.js +0 -40
- /package/dist/{dist-T737I76S.js.map → dist-VD54GDPO.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
createProjectScopeMatcher,
|
|
12
12
|
createRegisteredAgents,
|
|
13
13
|
deleteBookmark,
|
|
14
|
+
executeSessionSearch,
|
|
14
15
|
extractSessionFileActivity,
|
|
15
16
|
filterSessions,
|
|
16
17
|
getAgentInfoMap,
|
|
18
|
+
getAgentLastFullSyncAt,
|
|
17
19
|
getCursorDataPath,
|
|
18
20
|
getSessionActivityTime,
|
|
19
21
|
getSessionAgentName,
|
|
@@ -21,32 +23,33 @@ import {
|
|
|
21
23
|
getTotalTokens,
|
|
22
24
|
importBookmarks,
|
|
23
25
|
isAgentCacheInitialized,
|
|
26
|
+
isProjectIdentityKind,
|
|
24
27
|
listBookmarks,
|
|
25
28
|
listCachedProjectGroups,
|
|
26
29
|
listFileActivity,
|
|
27
30
|
listSessionFileActivity,
|
|
28
31
|
loadCachedSessionData,
|
|
29
32
|
loadCachedSessions,
|
|
33
|
+
markAgentFullSyncCompleted,
|
|
34
|
+
matchesProjectIdentity,
|
|
30
35
|
matchesProjectScope,
|
|
31
|
-
parseSearchQuery,
|
|
32
36
|
perf,
|
|
33
37
|
realFs,
|
|
34
38
|
refreshPricingCache,
|
|
35
39
|
resolveProviderRoots,
|
|
36
40
|
scanSessions,
|
|
37
|
-
searchFileActivitySessions,
|
|
38
|
-
searchSessions,
|
|
39
41
|
sessionSignature,
|
|
40
42
|
sortSessions,
|
|
41
43
|
startOfLocalDay,
|
|
42
44
|
upsertBookmark
|
|
43
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-BV65IEWZ.js";
|
|
44
46
|
|
|
45
47
|
// src/index.ts
|
|
46
48
|
import { defineCommand, runMain } from "citty";
|
|
47
49
|
|
|
48
50
|
// src/server.ts
|
|
49
51
|
import { Hono as Hono2 } from "hono";
|
|
52
|
+
import { bodyLimit } from "hono/body-limit";
|
|
50
53
|
import { serve } from "@hono/node-server";
|
|
51
54
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
52
55
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -225,7 +228,7 @@ function parseBookmarkPayload(value) {
|
|
|
225
228
|
title: value.title,
|
|
226
229
|
directory: value.directory,
|
|
227
230
|
time_created: value.time_created,
|
|
228
|
-
time_updated: value.time_updated,
|
|
231
|
+
time_updated: value.time_updated ?? void 0,
|
|
229
232
|
stats: value.stats
|
|
230
233
|
};
|
|
231
234
|
}
|
|
@@ -263,13 +266,14 @@ function parseSmartTags(values) {
|
|
|
263
266
|
);
|
|
264
267
|
return tags.length > 0 ? [...new Set(tags)] : void 0;
|
|
265
268
|
}
|
|
266
|
-
function parseSearchOptions(c, defaults) {
|
|
269
|
+
function parseSearchOptions(c, defaults, projectIdentity) {
|
|
267
270
|
const params = searchParams(c);
|
|
268
271
|
const limitValue = parseNumberParam(params.get("limit") ?? void 0);
|
|
269
272
|
return {
|
|
270
273
|
agent: optionalQueryValue(params.get("agent") ?? void 0),
|
|
271
274
|
project: optionalQueryValue(params.get("project") ?? void 0),
|
|
272
|
-
|
|
275
|
+
projectKind: projectIdentity?.kind,
|
|
276
|
+
projectKey: projectIdentity?.key,
|
|
273
277
|
cwd: optionalQueryValue(params.get("cwd") ?? void 0),
|
|
274
278
|
tags: parseSmartTags(queryValues(params, "tag", "tags", "signal")),
|
|
275
279
|
tools: queryValues(params, "tool", "tools").map((tool) => tool.toLowerCase()),
|
|
@@ -308,49 +312,6 @@ function sanitizeClientLogData(value) {
|
|
|
308
312
|
})
|
|
309
313
|
);
|
|
310
314
|
}
|
|
311
|
-
function sessionMatchesCostFilter(session, options) {
|
|
312
|
-
const cost = session.stats.total_cost;
|
|
313
|
-
if (options.costMin != null) {
|
|
314
|
-
if (options.costMinExclusive ? cost <= options.costMin : cost < options.costMin) return false;
|
|
315
|
-
}
|
|
316
|
-
if (options.costMax != null) {
|
|
317
|
-
if (options.costMaxExclusive ? cost >= options.costMax : cost > options.costMax) return false;
|
|
318
|
-
}
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
function mergeSearchLists(left, right) {
|
|
322
|
-
const values = [...left ?? [], ...right ?? []];
|
|
323
|
-
return values.length > 0 ? [...new Set(values)] : void 0;
|
|
324
|
-
}
|
|
325
|
-
function mergeSearchOptions(options, filters) {
|
|
326
|
-
return {
|
|
327
|
-
...options,
|
|
328
|
-
agent: options.agent ?? filters.agent,
|
|
329
|
-
project: options.project ?? filters.project,
|
|
330
|
-
projectKey: options.projectKey ?? filters.projectKey,
|
|
331
|
-
cwd: options.cwd ?? filters.cwd,
|
|
332
|
-
tags: mergeSearchLists(options.tags, filters.tags),
|
|
333
|
-
tools: mergeSearchLists(options.tools, filters.tools),
|
|
334
|
-
file: options.file ?? filters.file,
|
|
335
|
-
fileKind: options.fileKind ?? filters.fileKind,
|
|
336
|
-
costMin: options.costMin ?? filters.costMin,
|
|
337
|
-
costMax: options.costMax ?? filters.costMax,
|
|
338
|
-
costMinExclusive: options.costMinExclusive ?? filters.costMinExclusive,
|
|
339
|
-
costMaxExclusive: options.costMaxExclusive ?? filters.costMaxExclusive
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
function mergeSearchResults(results, limit) {
|
|
343
|
-
const seen = /* @__PURE__ */ new Set();
|
|
344
|
-
const merged = [];
|
|
345
|
-
for (const result of results) {
|
|
346
|
-
const key = `${result.agentName}/${result.session.id}`;
|
|
347
|
-
if (seen.has(key)) continue;
|
|
348
|
-
seen.add(key);
|
|
349
|
-
merged.push(result);
|
|
350
|
-
if (merged.length >= limit) break;
|
|
351
|
-
}
|
|
352
|
-
return merged;
|
|
353
|
-
}
|
|
354
315
|
function getProjectGroupKey(identityKind, identityKey) {
|
|
355
316
|
return `${identityKind}:${identityKey}`;
|
|
356
317
|
}
|
|
@@ -406,46 +367,15 @@ function attachProjectMetrics(projects, sessions) {
|
|
|
406
367
|
};
|
|
407
368
|
});
|
|
408
369
|
}
|
|
409
|
-
function matchesRecentSearchFilters(session, options, projectScope) {
|
|
410
|
-
if (options.projectKey && session.project_identity?.key !== options.projectKey) return false;
|
|
411
|
-
if (projectScope && !matchesProjectScope(session, projectScope)) return false;
|
|
412
|
-
if (options.project) {
|
|
413
|
-
const projectNeedle = options.project.toLowerCase();
|
|
414
|
-
const projectText = [
|
|
415
|
-
session.project_identity?.key,
|
|
416
|
-
session.project_identity?.displayName,
|
|
417
|
-
session.directory
|
|
418
|
-
].filter(Boolean).join("\n").toLowerCase();
|
|
419
|
-
if (!projectText.includes(projectNeedle)) return false;
|
|
420
|
-
}
|
|
421
|
-
if (options.tags?.length && !options.tags.every((tag) => session.smart_tags?.includes(tag))) {
|
|
422
|
-
return false;
|
|
423
|
-
}
|
|
424
|
-
if (!sessionMatchesCostFilter(session, options)) return false;
|
|
425
|
-
return true;
|
|
426
|
-
}
|
|
427
|
-
function recentSearchSessions(scanResult, options) {
|
|
428
|
-
const projectScope = options.cwd ? createProjectScopeMatcher(options.cwd) : null;
|
|
429
|
-
const entries = options.agent ? [[options.agent, scanResult.byAgent[options.agent] ?? []]] : Object.entries(scanResult.byAgent);
|
|
430
|
-
return entries.flatMap(
|
|
431
|
-
([agentName, sessions]) => filterSessionsByActivityWindow(sessions, options.from, options.to).filter((session) => matchesRecentSearchFilters(session, options, projectScope)).map((session) => ({ agentName, session }))
|
|
432
|
-
).toSorted(
|
|
433
|
-
(a, b) => (b.session.time_updated ?? b.session.time_created) - (a.session.time_updated ?? a.session.time_created)
|
|
434
|
-
).slice(0, options.limit).map(({ agentName, session }) => ({
|
|
435
|
-
agentName,
|
|
436
|
-
session,
|
|
437
|
-
snippet: `Recent session \xB7 ${session.directory}`,
|
|
438
|
-
matchType: "recent"
|
|
439
|
-
}));
|
|
440
|
-
}
|
|
441
370
|
function handleGetConfig(c, defaults) {
|
|
442
|
-
|
|
371
|
+
const payload = {
|
|
443
372
|
window: {
|
|
444
373
|
from: defaults.from,
|
|
445
374
|
to: defaults.to,
|
|
446
375
|
days: defaults.days
|
|
447
376
|
}
|
|
448
|
-
}
|
|
377
|
+
};
|
|
378
|
+
return c.json(payload);
|
|
449
379
|
}
|
|
450
380
|
function handleGetScanStatus(c, scanSource) {
|
|
451
381
|
return c.json(scanSource.getScanStatus());
|
|
@@ -475,7 +405,13 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
475
405
|
const agent = c.req.query("agent");
|
|
476
406
|
const q = c.req.query("q")?.toLowerCase();
|
|
477
407
|
const cwd = c.req.query("cwd");
|
|
478
|
-
const
|
|
408
|
+
const projectIdentity = parseProjectIdentityFilter(
|
|
409
|
+
c.req.query("projectKind"),
|
|
410
|
+
c.req.query("projectKey")
|
|
411
|
+
);
|
|
412
|
+
if (projectIdentity === null) {
|
|
413
|
+
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
414
|
+
}
|
|
479
415
|
const tag = c.req.query("tag")?.toLowerCase();
|
|
480
416
|
const from = parseDateParam(c.req.query("from"), defaults.from);
|
|
481
417
|
const to = parseDateParam(c.req.query("to"), defaults.to);
|
|
@@ -485,8 +421,10 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
485
421
|
} else {
|
|
486
422
|
sessions = [...scanResult.sessions];
|
|
487
423
|
}
|
|
488
|
-
if (
|
|
489
|
-
sessions = sessions.filter(
|
|
424
|
+
if (projectIdentity) {
|
|
425
|
+
sessions = sessions.filter(
|
|
426
|
+
(session) => matchesProjectIdentity(session.project_identity, projectIdentity)
|
|
427
|
+
);
|
|
490
428
|
} else if (cwd) {
|
|
491
429
|
const projectScope = createProjectScopeMatcher(cwd);
|
|
492
430
|
sessions = sessions.filter((s) => matchesProjectScope(s, projectScope));
|
|
@@ -503,29 +441,15 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
503
441
|
function handleSearchSessions(c, scanSource, defaults = {}) {
|
|
504
442
|
const query = c.req.query("q")?.trim() ?? "";
|
|
505
443
|
const scanResult = scanSource.getSnapshot();
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const textQuery = parsedQuery.text || (parsedQuery.hasQualifiers ? "" : query);
|
|
510
|
-
const needsIndexedSearch = Boolean(
|
|
511
|
-
textQuery || mergedSearchOptions.file || mergedSearchOptions.fileKind || mergedSearchOptions.tools?.length
|
|
444
|
+
const projectIdentity = parseProjectIdentityFilter(
|
|
445
|
+
c.req.query("projectKind"),
|
|
446
|
+
c.req.query("projectKey")
|
|
512
447
|
);
|
|
513
|
-
if (
|
|
514
|
-
return c.json({
|
|
515
|
-
results: recentSearchSessions(
|
|
516
|
-
scanResult,
|
|
517
|
-
mergedSearchOptions
|
|
518
|
-
)
|
|
519
|
-
});
|
|
448
|
+
if (projectIdentity === null) {
|
|
449
|
+
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
520
450
|
}
|
|
521
|
-
const
|
|
522
|
-
const results =
|
|
523
|
-
[
|
|
524
|
-
...fileQuery ? searchFileActivitySessions(fileQuery, mergedSearchOptions) : [],
|
|
525
|
-
...searchSessions(query, mergedSearchOptions)
|
|
526
|
-
],
|
|
527
|
-
mergedSearchOptions.limit ?? 50
|
|
528
|
-
);
|
|
451
|
+
const searchOptions = parseSearchOptions(c, defaults, projectIdentity);
|
|
452
|
+
const results = executeSessionSearch(query, searchOptions, scanResult);
|
|
529
453
|
return c.json({ results });
|
|
530
454
|
}
|
|
531
455
|
function parseFileActivityKind(value) {
|
|
@@ -538,14 +462,29 @@ function optionalQueryValue(value) {
|
|
|
538
462
|
const normalized = value?.trim();
|
|
539
463
|
return normalized ? normalized : void 0;
|
|
540
464
|
}
|
|
465
|
+
function parseProjectIdentityFilter(kindValue, keyValue) {
|
|
466
|
+
const kind = optionalQueryValue(kindValue);
|
|
467
|
+
const key = optionalQueryValue(keyValue);
|
|
468
|
+
if (!kind && !key) return void 0;
|
|
469
|
+
if (!kind || !key || !isProjectIdentityKind(kind)) return null;
|
|
470
|
+
return { kind, key };
|
|
471
|
+
}
|
|
541
472
|
function handleGetFileActivity(c, defaults = {}) {
|
|
542
473
|
const limitValue = Number(c.req.query("limit"));
|
|
543
474
|
const limit = Number.isFinite(limitValue) && limitValue > 0 ? Math.min(limitValue, 200) : 50;
|
|
475
|
+
const projectIdentity = parseProjectIdentityFilter(
|
|
476
|
+
c.req.query("projectKind"),
|
|
477
|
+
c.req.query("projectKey")
|
|
478
|
+
);
|
|
479
|
+
if (projectIdentity === null) {
|
|
480
|
+
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
481
|
+
}
|
|
544
482
|
return c.json({
|
|
545
483
|
activity: listFileActivity({
|
|
546
484
|
agent: optionalQueryValue(c.req.query("agent")),
|
|
547
485
|
sessionId: optionalQueryValue(c.req.query("sessionId")),
|
|
548
|
-
|
|
486
|
+
projectKind: projectIdentity?.kind,
|
|
487
|
+
projectKey: projectIdentity?.key,
|
|
549
488
|
project: optionalQueryValue(c.req.query("project")),
|
|
550
489
|
cwd: optionalQueryValue(c.req.query("cwd")),
|
|
551
490
|
path: optionalQueryValue(c.req.query("path")),
|
|
@@ -561,6 +500,9 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
561
500
|
const scanResult = scanSource.getSnapshot();
|
|
562
501
|
const agentName = c.req.param("agent");
|
|
563
502
|
const sessionId = c.req.param("id");
|
|
503
|
+
if (!agentName) {
|
|
504
|
+
return c.json({ error: "Missing agent name" }, 400);
|
|
505
|
+
}
|
|
564
506
|
if (!sessionId) {
|
|
565
507
|
return c.json({ error: "Missing session ID" }, 400);
|
|
566
508
|
}
|
|
@@ -710,6 +652,13 @@ function resolveDashboardWindow(defaults, queryDays, queryFrom, queryTo) {
|
|
|
710
652
|
}
|
|
711
653
|
function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
712
654
|
const scanResult = scanSource.getSnapshot();
|
|
655
|
+
const projectIdentity = parseProjectIdentityFilter(
|
|
656
|
+
c.req.query("projectKind"),
|
|
657
|
+
c.req.query("projectKey")
|
|
658
|
+
);
|
|
659
|
+
if (projectIdentity === null) {
|
|
660
|
+
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
661
|
+
}
|
|
713
662
|
const { from, to, days } = resolveDashboardWindow(
|
|
714
663
|
defaults,
|
|
715
664
|
c.req.query("days"),
|
|
@@ -718,8 +667,8 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
718
667
|
);
|
|
719
668
|
const scope = {
|
|
720
669
|
agent: optionalQueryValue(c.req.query("agent"))?.toLowerCase(),
|
|
721
|
-
projectKind:
|
|
722
|
-
projectKey:
|
|
670
|
+
projectKind: projectIdentity?.kind,
|
|
671
|
+
projectKey: projectIdentity?.key
|
|
723
672
|
};
|
|
724
673
|
const agentInfo = getAgentInfoMap({});
|
|
725
674
|
const agentInfoMap = new Map(agentInfo.map((a) => [a.name, a]));
|
|
@@ -734,6 +683,7 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
734
683
|
...aggregate,
|
|
735
684
|
recentFileActivities: listFileActivity({
|
|
736
685
|
agent: scope.agent,
|
|
686
|
+
projectKind: scope.projectKind,
|
|
737
687
|
projectKey: scope.projectKey,
|
|
738
688
|
from,
|
|
739
689
|
to,
|
|
@@ -747,10 +697,14 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
747
697
|
// src/api/routes.ts
|
|
748
698
|
function createSseResponse(store, signal) {
|
|
749
699
|
const encoder = new TextEncoder();
|
|
700
|
+
let cancelStream = () => {
|
|
701
|
+
};
|
|
750
702
|
return new Response(
|
|
751
703
|
new ReadableStream({
|
|
752
704
|
start(controller) {
|
|
705
|
+
let isClosed = false;
|
|
753
706
|
const write = (event, data) => {
|
|
707
|
+
if (isClosed) return;
|
|
754
708
|
controller.enqueue(encoder.encode(`event: ${event}
|
|
755
709
|
`));
|
|
756
710
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
@@ -766,18 +720,28 @@ function createSseResponse(store, signal) {
|
|
|
766
720
|
write(event.type, event);
|
|
767
721
|
});
|
|
768
722
|
const heartbeat = setInterval(() => {
|
|
769
|
-
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
|
723
|
+
if (!isClosed) controller.enqueue(encoder.encode(": keepalive\n\n"));
|
|
770
724
|
}, 15e3);
|
|
771
|
-
const
|
|
725
|
+
const cleanup = () => {
|
|
726
|
+
if (isClosed) return false;
|
|
727
|
+
isClosed = true;
|
|
772
728
|
clearInterval(heartbeat);
|
|
773
729
|
unsubscribeSessions();
|
|
774
730
|
unsubscribeScanStatus();
|
|
775
|
-
|
|
731
|
+
signal.removeEventListener("abort", abortStream);
|
|
732
|
+
return true;
|
|
733
|
+
};
|
|
734
|
+
const abortStream = () => {
|
|
735
|
+
if (cleanup()) controller.close();
|
|
736
|
+
};
|
|
737
|
+
cancelStream = () => {
|
|
738
|
+
cleanup();
|
|
776
739
|
};
|
|
777
|
-
signal.
|
|
740
|
+
if (signal.aborted) abortStream();
|
|
741
|
+
else signal.addEventListener("abort", abortStream, { once: true });
|
|
778
742
|
},
|
|
779
743
|
cancel() {
|
|
780
|
-
|
|
744
|
+
cancelStream();
|
|
781
745
|
}
|
|
782
746
|
}),
|
|
783
747
|
{
|
|
@@ -818,7 +782,46 @@ function createApiRoutes(scanSource, store, options = {}) {
|
|
|
818
782
|
return api;
|
|
819
783
|
}
|
|
820
784
|
|
|
785
|
+
// src/remote-access.ts
|
|
786
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
787
|
+
import { isIP } from "net";
|
|
788
|
+
var REMOTE_ACCESS_QUERY_PARAM = "access_token";
|
|
789
|
+
function createRemoteAccessToken() {
|
|
790
|
+
return randomBytes(32).toString("base64url");
|
|
791
|
+
}
|
|
792
|
+
function isLoopbackHostname(hostname) {
|
|
793
|
+
const normalized = hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
794
|
+
if (normalized === "localhost" || normalized === "::1") return true;
|
|
795
|
+
return isIP(normalized) === 4 && normalized.startsWith("127.");
|
|
796
|
+
}
|
|
797
|
+
function tokenMatches(actual, expected) {
|
|
798
|
+
if (!actual) return false;
|
|
799
|
+
const actualBuffer = Buffer.from(actual);
|
|
800
|
+
const expectedBuffer = Buffer.from(expected);
|
|
801
|
+
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
|
|
802
|
+
}
|
|
803
|
+
function bearerToken(c) {
|
|
804
|
+
const authorization = c.req.header("Authorization");
|
|
805
|
+
if (!authorization?.startsWith("Bearer ")) return void 0;
|
|
806
|
+
return authorization.slice("Bearer ".length);
|
|
807
|
+
}
|
|
808
|
+
function requestToken(c) {
|
|
809
|
+
const bearer = bearerToken(c);
|
|
810
|
+
if (bearer) return bearer;
|
|
811
|
+
if (c.req.method !== "GET") return void 0;
|
|
812
|
+
return c.req.query(REMOTE_ACCESS_QUERY_PARAM);
|
|
813
|
+
}
|
|
814
|
+
function remoteAccessAuth(expectedToken) {
|
|
815
|
+
return async (c, next) => {
|
|
816
|
+
if (!tokenMatches(requestToken(c), expectedToken)) {
|
|
817
|
+
return c.json({ error: "Remote access authentication required" }, 401);
|
|
818
|
+
}
|
|
819
|
+
await next();
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
821
823
|
// src/server.ts
|
|
824
|
+
var MAX_API_REQUEST_BYTES = 1024 * 1024;
|
|
822
825
|
function findWebDistPath() {
|
|
823
826
|
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
824
827
|
const packagedPath = resolve(__dirname2, "web");
|
|
@@ -860,6 +863,14 @@ function getListeningPort(server, fallback) {
|
|
|
860
863
|
}
|
|
861
864
|
async function createServer(port, store, options = {}) {
|
|
862
865
|
const app = new Hono2();
|
|
866
|
+
const hostname = options.hostname ?? "127.0.0.1";
|
|
867
|
+
const isLoopback = isLoopbackHostname(hostname);
|
|
868
|
+
const remoteAccessToken = !isLoopback ? options.remoteAccessToken ?? (options.remoteAccess ? createRemoteAccessToken() : null) : null;
|
|
869
|
+
if (!isLoopback && !remoteAccessToken) {
|
|
870
|
+
throw new Error(
|
|
871
|
+
`Refusing to expose CodeSesh on ${hostname} without authentication. Add --remote-access to continue.`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
863
874
|
app.use("*", async (c, next) => {
|
|
864
875
|
const startedAt = performance.now();
|
|
865
876
|
let thrown;
|
|
@@ -880,6 +891,16 @@ async function createServer(port, store, options = {}) {
|
|
|
880
891
|
});
|
|
881
892
|
}
|
|
882
893
|
});
|
|
894
|
+
if (remoteAccessToken) {
|
|
895
|
+
app.use("/api/*", remoteAccessAuth(remoteAccessToken));
|
|
896
|
+
}
|
|
897
|
+
app.use(
|
|
898
|
+
"/api/*",
|
|
899
|
+
bodyLimit({
|
|
900
|
+
maxSize: MAX_API_REQUEST_BYTES,
|
|
901
|
+
onError: (c) => c.json({ error: "Request body too large" }, 413)
|
|
902
|
+
})
|
|
903
|
+
);
|
|
883
904
|
const routeOptions = {
|
|
884
905
|
defaultSessionFrom: options.defaultSessionFrom,
|
|
885
906
|
defaultSessionTo: options.defaultSessionTo,
|
|
@@ -903,7 +924,7 @@ async function createServer(port, store, options = {}) {
|
|
|
903
924
|
let actualPort = port;
|
|
904
925
|
for (let offset = 0; offset < attempts; offset += 1) {
|
|
905
926
|
const candidatePort = port + offset;
|
|
906
|
-
server = serve({ fetch: app.fetch, port: candidatePort });
|
|
927
|
+
server = serve({ fetch: app.fetch, port: candidatePort, hostname });
|
|
907
928
|
try {
|
|
908
929
|
await waitForListening(server);
|
|
909
930
|
actualPort = getListeningPort(server, candidatePort);
|
|
@@ -925,8 +946,20 @@ async function createServer(port, store, options = {}) {
|
|
|
925
946
|
throw new Error(getServerStartupErrorMessage(error, candidatePort));
|
|
926
947
|
}
|
|
927
948
|
}
|
|
928
|
-
const
|
|
929
|
-
|
|
949
|
+
const baseUrl = isLoopback ? `http://localhost:${actualPort}` : `http://${hostname}:${actualPort}`;
|
|
950
|
+
const url = remoteAccessToken ? `${baseUrl}/?${REMOTE_ACCESS_QUERY_PARAM}=${encodeURIComponent(remoteAccessToken)}` : baseUrl;
|
|
951
|
+
appLogger.info("server.listen", {
|
|
952
|
+
port: actualPort,
|
|
953
|
+
requested_port: port,
|
|
954
|
+
hostname,
|
|
955
|
+
remote_access: Boolean(remoteAccessToken)
|
|
956
|
+
});
|
|
957
|
+
if (!isLoopback) {
|
|
958
|
+
appLogger.warn("server.listen.remote_access", { hostname, port: actualPort });
|
|
959
|
+
console.warn(`
|
|
960
|
+
\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
|
|
961
|
+
`);
|
|
962
|
+
}
|
|
930
963
|
return {
|
|
931
964
|
url,
|
|
932
965
|
shutdown: async () => {
|
|
@@ -946,12 +979,661 @@ async function createServer(port, store, options = {}) {
|
|
|
946
979
|
}
|
|
947
980
|
|
|
948
981
|
// src/live-scan.ts
|
|
949
|
-
import { existsSync as
|
|
982
|
+
import { existsSync as existsSync5 } from "fs";
|
|
983
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
984
|
+
import { Worker as Worker2 } from "worker_threads";
|
|
985
|
+
|
|
986
|
+
// src/search-index-job-runner.ts
|
|
987
|
+
import { existsSync as existsSync3 } from "fs";
|
|
950
988
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
951
989
|
import { Worker } from "worker_threads";
|
|
952
990
|
|
|
991
|
+
// src/pending-search-index-jobs.ts
|
|
992
|
+
var PendingSearchIndexJobBatch = class {
|
|
993
|
+
constructor(id, context, jobs, waiter) {
|
|
994
|
+
this.id = id;
|
|
995
|
+
this.context = context;
|
|
996
|
+
this.merge(context, jobs, waiter);
|
|
997
|
+
}
|
|
998
|
+
id;
|
|
999
|
+
context;
|
|
1000
|
+
jobsByAgent = /* @__PURE__ */ new Map();
|
|
1001
|
+
waiters = [];
|
|
1002
|
+
settled = false;
|
|
1003
|
+
get jobs() {
|
|
1004
|
+
const jobs = [];
|
|
1005
|
+
for (const pending of this.jobsByAgent.values()) {
|
|
1006
|
+
if (pending.full) jobs.push(pending.full);
|
|
1007
|
+
if (pending.changes) jobs.push(changesJobFromPending(pending.changes));
|
|
1008
|
+
}
|
|
1009
|
+
return jobs;
|
|
1010
|
+
}
|
|
1011
|
+
get changeCount() {
|
|
1012
|
+
let count = 0;
|
|
1013
|
+
for (const pending of this.jobsByAgent.values()) {
|
|
1014
|
+
if (!pending.changes) continue;
|
|
1015
|
+
count += pending.changes.changesBySessionId.size + pending.changes.removedSessionIds.size;
|
|
1016
|
+
}
|
|
1017
|
+
return count;
|
|
1018
|
+
}
|
|
1019
|
+
merge(context, jobs, waiter) {
|
|
1020
|
+
this.context = context;
|
|
1021
|
+
this.waiters.push(waiter);
|
|
1022
|
+
for (const job of jobs) this.mergeJob(job);
|
|
1023
|
+
}
|
|
1024
|
+
settle(error) {
|
|
1025
|
+
if (this.settled) return false;
|
|
1026
|
+
this.settled = true;
|
|
1027
|
+
for (const waiter of this.waiters) {
|
|
1028
|
+
if (error) waiter.reject(error);
|
|
1029
|
+
else waiter.resolve();
|
|
1030
|
+
}
|
|
1031
|
+
this.waiters.length = 0;
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
mergeJob(job) {
|
|
1035
|
+
const pending = this.jobsByAgent.get(job.agentName) ?? {};
|
|
1036
|
+
this.jobsByAgent.set(job.agentName, pending);
|
|
1037
|
+
if (job.kind === "full") {
|
|
1038
|
+
pending.full = job;
|
|
1039
|
+
pending.changes = void 0;
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
pending.changes ??= createPendingChanges(job);
|
|
1043
|
+
mergeChanges(pending.changes, job);
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
var PendingSearchIndexJobs = class {
|
|
1047
|
+
pendingBatch = null;
|
|
1048
|
+
get batchCount() {
|
|
1049
|
+
return this.pendingBatch ? 1 : 0;
|
|
1050
|
+
}
|
|
1051
|
+
get jobCount() {
|
|
1052
|
+
return this.pendingBatch?.jobs.length ?? 0;
|
|
1053
|
+
}
|
|
1054
|
+
get changeCount() {
|
|
1055
|
+
return this.pendingBatch?.changeCount ?? 0;
|
|
1056
|
+
}
|
|
1057
|
+
enqueue(id, context, jobs) {
|
|
1058
|
+
if (jobs.length === 0) return Promise.resolve();
|
|
1059
|
+
return new Promise((resolve4, reject) => {
|
|
1060
|
+
const waiter = { resolve: resolve4, reject };
|
|
1061
|
+
if (this.pendingBatch) {
|
|
1062
|
+
this.pendingBatch.merge(context, jobs, waiter);
|
|
1063
|
+
} else {
|
|
1064
|
+
this.pendingBatch = new PendingSearchIndexJobBatch(id, context, jobs, waiter);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
take() {
|
|
1069
|
+
const batch = this.pendingBatch;
|
|
1070
|
+
this.pendingBatch = null;
|
|
1071
|
+
return batch;
|
|
1072
|
+
}
|
|
1073
|
+
settle(batch, error) {
|
|
1074
|
+
return batch instanceof PendingSearchIndexJobBatch && batch.settle(error);
|
|
1075
|
+
}
|
|
1076
|
+
rejectAll(error) {
|
|
1077
|
+
const batch = this.take();
|
|
1078
|
+
if (batch) this.settle(batch, error);
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
function createPendingChanges(job) {
|
|
1082
|
+
return {
|
|
1083
|
+
context: job.context,
|
|
1084
|
+
agentName: job.agentName,
|
|
1085
|
+
changesBySessionId: /* @__PURE__ */ new Map(),
|
|
1086
|
+
removedSessionIds: /* @__PURE__ */ new Set(),
|
|
1087
|
+
meta: {},
|
|
1088
|
+
searchIndexOptions: job.searchIndexOptions
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function mergeChanges(pending, job) {
|
|
1092
|
+
pending.context = job.context;
|
|
1093
|
+
pending.searchIndexOptions = mergeSearchIndexOptions(
|
|
1094
|
+
pending.searchIndexOptions,
|
|
1095
|
+
job.searchIndexOptions
|
|
1096
|
+
);
|
|
1097
|
+
for (const sessionId of job.removedSessionIds) {
|
|
1098
|
+
pending.changesBySessionId.delete(sessionId);
|
|
1099
|
+
pending.removedSessionIds.add(sessionId);
|
|
1100
|
+
delete pending.meta[sessionId];
|
|
1101
|
+
}
|
|
1102
|
+
for (const change of job.changes) {
|
|
1103
|
+
const sessionId = change.session.id;
|
|
1104
|
+
pending.removedSessionIds.delete(sessionId);
|
|
1105
|
+
pending.changesBySessionId.set(sessionId, change);
|
|
1106
|
+
if (Object.hasOwn(job.meta, sessionId)) pending.meta[sessionId] = job.meta[sessionId];
|
|
1107
|
+
else delete pending.meta[sessionId];
|
|
1108
|
+
}
|
|
1109
|
+
for (const [sessionId, meta] of Object.entries(job.meta)) {
|
|
1110
|
+
if (!pending.removedSessionIds.has(sessionId)) pending.meta[sessionId] = meta;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function mergeSearchIndexOptions(current, incoming) {
|
|
1114
|
+
if (!current) return incoming;
|
|
1115
|
+
if (!incoming) return current;
|
|
1116
|
+
return { ...current, ...incoming };
|
|
1117
|
+
}
|
|
1118
|
+
function changesJobFromPending(pending) {
|
|
1119
|
+
return {
|
|
1120
|
+
kind: "changes",
|
|
1121
|
+
context: pending.context,
|
|
1122
|
+
agentName: pending.agentName,
|
|
1123
|
+
changes: [...pending.changesBySessionId.values()],
|
|
1124
|
+
removedSessionIds: [...pending.removedSessionIds],
|
|
1125
|
+
meta: pending.meta,
|
|
1126
|
+
...pending.searchIndexOptions ? { searchIndexOptions: pending.searchIndexOptions } : {}
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/search-index-job-runner.ts
|
|
1131
|
+
var SHUTDOWN_ERROR_MESSAGE = "Live scan store shut down";
|
|
1132
|
+
var SearchIndexJobRunner = class {
|
|
1133
|
+
worker = null;
|
|
1134
|
+
activeBatch = null;
|
|
1135
|
+
nextBatchId = 1;
|
|
1136
|
+
pendingJobs = new PendingSearchIndexJobs();
|
|
1137
|
+
isShuttingDown = false;
|
|
1138
|
+
hasCheckedFtsIntegrity = false;
|
|
1139
|
+
enqueue(context, jobs) {
|
|
1140
|
+
if (jobs.length === 0) return Promise.resolve();
|
|
1141
|
+
if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE));
|
|
1142
|
+
const batchId = this.nextBatchId++;
|
|
1143
|
+
const completion = this.pendingJobs.enqueue(batchId, context, jobs);
|
|
1144
|
+
if (this.worker) {
|
|
1145
|
+
const snapshot = this.snapshot();
|
|
1146
|
+
appLogger.debug("search_index.worker_queued", {
|
|
1147
|
+
batch_id: batchId,
|
|
1148
|
+
context,
|
|
1149
|
+
jobs: jobs.length,
|
|
1150
|
+
pending_batches: snapshot.pendingBatches,
|
|
1151
|
+
pending_jobs: snapshot.pendingJobs,
|
|
1152
|
+
pending_changes: snapshot.pendingChanges
|
|
1153
|
+
});
|
|
1154
|
+
} else {
|
|
1155
|
+
this.startNextBatch();
|
|
1156
|
+
}
|
|
1157
|
+
return completion;
|
|
1158
|
+
}
|
|
1159
|
+
snapshot() {
|
|
1160
|
+
return {
|
|
1161
|
+
activeBatchId: this.activeBatch?.id,
|
|
1162
|
+
pendingBatches: this.pendingJobs.batchCount,
|
|
1163
|
+
pendingJobs: this.pendingJobs.jobCount,
|
|
1164
|
+
pendingChanges: this.pendingJobs.changeCount
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
async shutdown() {
|
|
1168
|
+
this.isShuttingDown = true;
|
|
1169
|
+
const activeBatch = this.activeBatch;
|
|
1170
|
+
const worker = this.worker;
|
|
1171
|
+
this.activeBatch = null;
|
|
1172
|
+
this.worker = null;
|
|
1173
|
+
const shutdownError = new Error(SHUTDOWN_ERROR_MESSAGE);
|
|
1174
|
+
if (activeBatch) this.settle(activeBatch, shutdownError);
|
|
1175
|
+
this.pendingJobs.rejectAll(shutdownError);
|
|
1176
|
+
if (worker) await worker.terminate();
|
|
1177
|
+
}
|
|
1178
|
+
startNextBatch() {
|
|
1179
|
+
if (this.isShuttingDown || this.worker) return;
|
|
1180
|
+
const batch = this.pendingJobs.take();
|
|
1181
|
+
if (!batch) return;
|
|
1182
|
+
appLogger.info("search_index.worker_dequeued", {
|
|
1183
|
+
batch_id: batch.id,
|
|
1184
|
+
context: batch.context,
|
|
1185
|
+
pending_batches: this.pendingJobs.batchCount
|
|
1186
|
+
});
|
|
1187
|
+
this.startBatch(batch);
|
|
1188
|
+
}
|
|
1189
|
+
startBatch(batch) {
|
|
1190
|
+
if (this.isShuttingDown) {
|
|
1191
|
+
this.settle(batch, new Error(SHUTDOWN_ERROR_MESSAGE));
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const workerUrl = this.workerUrl();
|
|
1195
|
+
if (!workerUrl) {
|
|
1196
|
+
appLogger.warn("search_index.worker_missing", { context: batch.context });
|
|
1197
|
+
this.settle(batch);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
appLogger.info("search_index.worker_started", {
|
|
1201
|
+
batch_id: batch.id,
|
|
1202
|
+
context: batch.context,
|
|
1203
|
+
jobs: batch.jobs.length
|
|
1204
|
+
});
|
|
1205
|
+
const worker = new Worker(workerUrl, {
|
|
1206
|
+
workerData: {
|
|
1207
|
+
context: batch.context,
|
|
1208
|
+
jobs: batch.jobs,
|
|
1209
|
+
agentNames: [],
|
|
1210
|
+
sessionsByAgent: {},
|
|
1211
|
+
metaByAgent: {},
|
|
1212
|
+
skipFtsIntegrityCheck: this.hasCheckedFtsIntegrity
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
worker.unref();
|
|
1216
|
+
this.worker = worker;
|
|
1217
|
+
this.activeBatch = batch;
|
|
1218
|
+
worker.on("message", (message) => {
|
|
1219
|
+
if (message.type === "sync-result") {
|
|
1220
|
+
logSearchIndexSync(message.context, message.result);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (message.type !== "done") return;
|
|
1224
|
+
appLogger.info(`${message.context}.done`, {
|
|
1225
|
+
duration_ms: Math.round(message.durationMs),
|
|
1226
|
+
sessions: message.sessions
|
|
1227
|
+
});
|
|
1228
|
+
this.hasCheckedFtsIntegrity = true;
|
|
1229
|
+
this.settle(batch);
|
|
1230
|
+
});
|
|
1231
|
+
worker.on("error", (error) => {
|
|
1232
|
+
appLogger.error("search_index.worker_error", { context: batch.context, error });
|
|
1233
|
+
this.settle(batch, error);
|
|
1234
|
+
});
|
|
1235
|
+
worker.on("exit", (code) => this.finishWorker(worker, batch, code));
|
|
1236
|
+
}
|
|
1237
|
+
finishWorker(worker, batch, code) {
|
|
1238
|
+
appLogger.info("search_index.worker_exited", {
|
|
1239
|
+
batch_id: batch.id,
|
|
1240
|
+
context: batch.context,
|
|
1241
|
+
code,
|
|
1242
|
+
shutting_down: this.isShuttingDown || void 0
|
|
1243
|
+
});
|
|
1244
|
+
if (this.worker === worker) this.worker = null;
|
|
1245
|
+
if (this.activeBatch === batch) this.activeBatch = null;
|
|
1246
|
+
const error = code === 0 ? new Error("Search index worker exited before completing its batch") : new Error(`Search index worker exited with code ${code}`);
|
|
1247
|
+
if (code !== 0) appLogger.warn("search_index.worker_exit", { context: batch.context, code });
|
|
1248
|
+
this.settle(batch, error);
|
|
1249
|
+
this.startNextBatch();
|
|
1250
|
+
}
|
|
1251
|
+
settle(batch, error) {
|
|
1252
|
+
if (!this.pendingJobs.settle(batch, error)) return;
|
|
1253
|
+
appLogger.info("search_index.worker_settled", {
|
|
1254
|
+
batch_id: batch.id,
|
|
1255
|
+
context: batch.context,
|
|
1256
|
+
result: error ? "rejected" : "resolved"
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
workerUrl() {
|
|
1260
|
+
const workerUrl = new URL("./search-index-worker.js", import.meta.url);
|
|
1261
|
+
if (workerUrl.protocol === "file:" && !existsSync3(fileURLToPath2(workerUrl))) return null;
|
|
1262
|
+
return workerUrl;
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
// src/scan-status-model.ts
|
|
1267
|
+
var ScanStatusModel = class {
|
|
1268
|
+
status = {
|
|
1269
|
+
active: false,
|
|
1270
|
+
phase: "idle",
|
|
1271
|
+
pendingAgents: [],
|
|
1272
|
+
scanningAgents: [],
|
|
1273
|
+
completedAgents: [],
|
|
1274
|
+
agentStatuses: {},
|
|
1275
|
+
totalAgents: 0,
|
|
1276
|
+
updatedAt: Date.now(),
|
|
1277
|
+
backfill: { active: false, pendingAgents: [], completedAgents: [] }
|
|
1278
|
+
};
|
|
1279
|
+
snapshot() {
|
|
1280
|
+
return {
|
|
1281
|
+
type: "scan-status",
|
|
1282
|
+
...this.status,
|
|
1283
|
+
pendingAgents: [...this.status.pendingAgents],
|
|
1284
|
+
scanningAgents: [...this.status.scanningAgents],
|
|
1285
|
+
completedAgents: [...this.status.completedAgents],
|
|
1286
|
+
agentStatuses: Object.fromEntries(
|
|
1287
|
+
Object.entries(this.status.agentStatuses).map(([agentName, status]) => [
|
|
1288
|
+
agentName,
|
|
1289
|
+
{ ...status }
|
|
1290
|
+
])
|
|
1291
|
+
),
|
|
1292
|
+
backfill: {
|
|
1293
|
+
...this.status.backfill,
|
|
1294
|
+
pendingAgents: [...this.status.backfill.pendingAgents],
|
|
1295
|
+
completedAgents: [...this.status.backfill.completedAgents]
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
startBatch(agentNames, phase, sessionCounts) {
|
|
1300
|
+
const uniqueAgentNames = [...new Set(agentNames)];
|
|
1301
|
+
const now = Date.now();
|
|
1302
|
+
const agentStatuses = Object.fromEntries(
|
|
1303
|
+
uniqueAgentNames.map((agentName) => [
|
|
1304
|
+
agentName,
|
|
1305
|
+
{
|
|
1306
|
+
agentName,
|
|
1307
|
+
status: "pending",
|
|
1308
|
+
processed: 0,
|
|
1309
|
+
sessions: sessionCounts[agentName] ?? 0,
|
|
1310
|
+
updatedAt: now
|
|
1311
|
+
}
|
|
1312
|
+
])
|
|
1313
|
+
);
|
|
1314
|
+
return this.set({
|
|
1315
|
+
...this.status,
|
|
1316
|
+
active: uniqueAgentNames.length > 0,
|
|
1317
|
+
phase: uniqueAgentNames.length > 0 ? phase : "idle",
|
|
1318
|
+
pendingAgents: uniqueAgentNames,
|
|
1319
|
+
scanningAgents: [],
|
|
1320
|
+
completedAgents: [],
|
|
1321
|
+
agentStatuses,
|
|
1322
|
+
totalAgents: uniqueAgentNames.length,
|
|
1323
|
+
startedAt: uniqueAgentNames.length > 0 ? now : void 0,
|
|
1324
|
+
updatedAt: now,
|
|
1325
|
+
completedAt: uniqueAgentNames.length > 0 ? void 0 : now
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
setPhase(phase) {
|
|
1329
|
+
if (!this.status.active) return null;
|
|
1330
|
+
return this.set({ ...this.status, phase, updatedAt: Date.now() });
|
|
1331
|
+
}
|
|
1332
|
+
beginAgent(agentName, sessionCount) {
|
|
1333
|
+
if (!this.status.active)
|
|
1334
|
+
this.startBatch([agentName], "scanning", { [agentName]: sessionCount });
|
|
1335
|
+
const pendingAgents = this.status.pendingAgents.filter((agent) => agent !== agentName);
|
|
1336
|
+
const scanningAgents = [.../* @__PURE__ */ new Set([...this.status.scanningAgents, agentName])];
|
|
1337
|
+
const completedAgents = this.status.completedAgents.filter((agent) => agent !== agentName);
|
|
1338
|
+
const existingStatus = this.status.agentStatuses[agentName];
|
|
1339
|
+
const now = Date.now();
|
|
1340
|
+
return this.set({
|
|
1341
|
+
...this.status,
|
|
1342
|
+
active: true,
|
|
1343
|
+
phase: this.status.phase === "initializing" ? "initializing" : "scanning",
|
|
1344
|
+
pendingAgents,
|
|
1345
|
+
scanningAgents,
|
|
1346
|
+
completedAgents,
|
|
1347
|
+
agentStatuses: {
|
|
1348
|
+
...this.status.agentStatuses,
|
|
1349
|
+
[agentName]: {
|
|
1350
|
+
agentName,
|
|
1351
|
+
status: "scanning",
|
|
1352
|
+
total: existingStatus?.total,
|
|
1353
|
+
processed: existingStatus?.processed ?? 0,
|
|
1354
|
+
sessions: existingStatus?.sessions ?? sessionCount,
|
|
1355
|
+
startedAt: existingStatus?.startedAt ?? now,
|
|
1356
|
+
updatedAt: now
|
|
1357
|
+
}
|
|
1358
|
+
},
|
|
1359
|
+
totalAgents: Math.max(this.status.totalAgents, pendingAgents.length + scanningAgents.length),
|
|
1360
|
+
updatedAt: now,
|
|
1361
|
+
completedAt: void 0
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
updateAgent(agentName, progress) {
|
|
1365
|
+
const status = this.status.agentStatuses[agentName];
|
|
1366
|
+
if (!status || status.status !== "scanning") return null;
|
|
1367
|
+
const now = Date.now();
|
|
1368
|
+
return this.set({
|
|
1369
|
+
...this.status,
|
|
1370
|
+
agentStatuses: {
|
|
1371
|
+
...this.status.agentStatuses,
|
|
1372
|
+
[agentName]: {
|
|
1373
|
+
...status,
|
|
1374
|
+
total: progress.total ?? status.total,
|
|
1375
|
+
processed: progress.processed ?? status.processed,
|
|
1376
|
+
sessions: progress.sessions ?? status.sessions,
|
|
1377
|
+
updatedAt: now
|
|
1378
|
+
}
|
|
1379
|
+
},
|
|
1380
|
+
updatedAt: now
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
finishAgent(agentName, sessionCount) {
|
|
1384
|
+
const pendingAgents = this.status.pendingAgents.filter((agent) => agent !== agentName);
|
|
1385
|
+
const scanningAgents = this.status.scanningAgents.filter((agent) => agent !== agentName);
|
|
1386
|
+
const completedAgents = [.../* @__PURE__ */ new Set([...this.status.completedAgents, agentName])];
|
|
1387
|
+
const isActive = pendingAgents.length > 0 || scanningAgents.length > 0;
|
|
1388
|
+
const now = Date.now();
|
|
1389
|
+
const previousStatus = this.status.agentStatuses[agentName];
|
|
1390
|
+
const total = previousStatus?.total ?? previousStatus?.processed;
|
|
1391
|
+
return this.set({
|
|
1392
|
+
...this.status,
|
|
1393
|
+
active: isActive,
|
|
1394
|
+
phase: isActive ? "scanning" : "idle",
|
|
1395
|
+
pendingAgents,
|
|
1396
|
+
scanningAgents,
|
|
1397
|
+
completedAgents,
|
|
1398
|
+
agentStatuses: {
|
|
1399
|
+
...this.status.agentStatuses,
|
|
1400
|
+
[agentName]: {
|
|
1401
|
+
agentName,
|
|
1402
|
+
status: "complete",
|
|
1403
|
+
total,
|
|
1404
|
+
processed: total,
|
|
1405
|
+
sessions: sessionCount ?? previousStatus?.sessions ?? 0,
|
|
1406
|
+
startedAt: previousStatus?.startedAt,
|
|
1407
|
+
updatedAt: now,
|
|
1408
|
+
completedAt: now
|
|
1409
|
+
}
|
|
1410
|
+
},
|
|
1411
|
+
updatedAt: now,
|
|
1412
|
+
completedAt: isActive ? void 0 : now
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
finishBatch() {
|
|
1416
|
+
const now = Date.now();
|
|
1417
|
+
return this.set({
|
|
1418
|
+
...this.status,
|
|
1419
|
+
active: false,
|
|
1420
|
+
phase: "idle",
|
|
1421
|
+
pendingAgents: [],
|
|
1422
|
+
scanningAgents: [],
|
|
1423
|
+
agentStatuses: Object.fromEntries(
|
|
1424
|
+
Object.entries(this.status.agentStatuses).map(([agentName, status]) => [
|
|
1425
|
+
agentName,
|
|
1426
|
+
{ ...status, status: "complete", completedAt: status.completedAt ?? now, updatedAt: now }
|
|
1427
|
+
])
|
|
1428
|
+
),
|
|
1429
|
+
updatedAt: now,
|
|
1430
|
+
completedAt: now
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
updateBackfill(patch) {
|
|
1434
|
+
return this.set({
|
|
1435
|
+
...this.status,
|
|
1436
|
+
backfill: { ...this.status.backfill, ...patch },
|
|
1437
|
+
updatedAt: Date.now()
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
set(status) {
|
|
1441
|
+
this.status = status;
|
|
1442
|
+
return this.snapshot();
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
|
|
1446
|
+
// src/backfill-coordinator.ts
|
|
1447
|
+
var BackfillCoordinator = class {
|
|
1448
|
+
queue = [];
|
|
1449
|
+
currentAgent;
|
|
1450
|
+
completedAgents = [];
|
|
1451
|
+
get isRunning() {
|
|
1452
|
+
return this.currentAgent != null;
|
|
1453
|
+
}
|
|
1454
|
+
enqueue(agentName) {
|
|
1455
|
+
if (this.currentAgent === agentName || this.queue.includes(agentName)) return null;
|
|
1456
|
+
this.queue.push(agentName);
|
|
1457
|
+
return this.snapshot();
|
|
1458
|
+
}
|
|
1459
|
+
take() {
|
|
1460
|
+
if (this.currentAgent) return null;
|
|
1461
|
+
const agentName = this.queue.shift();
|
|
1462
|
+
if (!agentName) return null;
|
|
1463
|
+
this.currentAgent = agentName;
|
|
1464
|
+
return { agentName, status: this.snapshot() };
|
|
1465
|
+
}
|
|
1466
|
+
complete(agentName) {
|
|
1467
|
+
if (this.currentAgent === agentName) this.currentAgent = void 0;
|
|
1468
|
+
if (!this.completedAgents.includes(agentName)) this.completedAgents.push(agentName);
|
|
1469
|
+
return this.snapshot();
|
|
1470
|
+
}
|
|
1471
|
+
clear() {
|
|
1472
|
+
this.queue.length = 0;
|
|
1473
|
+
this.currentAgent = void 0;
|
|
1474
|
+
}
|
|
1475
|
+
snapshot() {
|
|
1476
|
+
return {
|
|
1477
|
+
active: this.currentAgent != null || this.queue.length > 0,
|
|
1478
|
+
pendingAgents: [...this.queue],
|
|
1479
|
+
currentAgent: this.currentAgent,
|
|
1480
|
+
completedAgents: [...this.completedAgents]
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
// src/refresh-coordinator.ts
|
|
1486
|
+
var PENDING_REFRESH_DELAY_MS = 100;
|
|
1487
|
+
var MAX_ADAPTIVE_REFRESH_DELAY_MS = 3e4;
|
|
1488
|
+
var ADAPTIVE_REFRESH_DELAY_MULTIPLIER = 4;
|
|
1489
|
+
var RefreshCoordinator = class {
|
|
1490
|
+
states = /* @__PURE__ */ new Map();
|
|
1491
|
+
operationGenerations = /* @__PURE__ */ new Map();
|
|
1492
|
+
operationTails = /* @__PURE__ */ new Map();
|
|
1493
|
+
isShuttingDown = false;
|
|
1494
|
+
get activeOperationCount() {
|
|
1495
|
+
return this.operationTails.size;
|
|
1496
|
+
}
|
|
1497
|
+
get activeRefreshCount() {
|
|
1498
|
+
return [...this.states.values()].filter((state) => state.isRunning).length;
|
|
1499
|
+
}
|
|
1500
|
+
recordChangedPaths(agentName, count = 1) {
|
|
1501
|
+
this.state(agentName).pendingPathCount += count;
|
|
1502
|
+
}
|
|
1503
|
+
takePendingPathCount(agentName) {
|
|
1504
|
+
const state = this.state(agentName);
|
|
1505
|
+
const count = state.pendingPathCount;
|
|
1506
|
+
state.pendingPathCount = 0;
|
|
1507
|
+
return count;
|
|
1508
|
+
}
|
|
1509
|
+
lastRefreshAt(agentName) {
|
|
1510
|
+
return this.state(agentName).lastRefreshAt;
|
|
1511
|
+
}
|
|
1512
|
+
setLastRefreshAt(agentName, timestamp) {
|
|
1513
|
+
this.state(agentName).lastRefreshAt = timestamp;
|
|
1514
|
+
}
|
|
1515
|
+
setLastRefreshDuration(agentName, durationMs) {
|
|
1516
|
+
this.state(agentName).lastRefreshDurationMs = durationMs;
|
|
1517
|
+
}
|
|
1518
|
+
schedule(agentName, delayMs, refresh) {
|
|
1519
|
+
if (this.isShuttingDown) return;
|
|
1520
|
+
const state = this.state(agentName);
|
|
1521
|
+
const adaptiveDelayMs = Math.min(
|
|
1522
|
+
state.lastRefreshDurationMs * ADAPTIVE_REFRESH_DELAY_MULTIPLIER,
|
|
1523
|
+
MAX_ADAPTIVE_REFRESH_DELAY_MS
|
|
1524
|
+
);
|
|
1525
|
+
const effectiveDelayMs = Math.max(delayMs, adaptiveDelayMs);
|
|
1526
|
+
const deadline = Date.now() + effectiveDelayMs;
|
|
1527
|
+
if (state.timer) {
|
|
1528
|
+
if (deadline >= state.timerDeadline) return;
|
|
1529
|
+
clearTimeout(state.timer);
|
|
1530
|
+
}
|
|
1531
|
+
appLogger.debug("scan.refresh.schedule", { agent: agentName, delay_ms: effectiveDelayMs });
|
|
1532
|
+
state.timerDeadline = deadline;
|
|
1533
|
+
state.timer = setTimeout(() => {
|
|
1534
|
+
state.timer = null;
|
|
1535
|
+
void refresh();
|
|
1536
|
+
}, effectiveDelayMs);
|
|
1537
|
+
}
|
|
1538
|
+
async runRefresh(agentName, operation) {
|
|
1539
|
+
const state = this.state(agentName);
|
|
1540
|
+
if (state.isRunning) {
|
|
1541
|
+
appLogger.debug("scan.refresh.pending", { agent: agentName });
|
|
1542
|
+
state.hasPendingRerun = true;
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
state.isRunning = true;
|
|
1546
|
+
try {
|
|
1547
|
+
await this.serialize(agentName, "refresh", operation);
|
|
1548
|
+
} finally {
|
|
1549
|
+
state.isRunning = false;
|
|
1550
|
+
if (state.hasPendingRerun && !this.isShuttingDown) {
|
|
1551
|
+
state.hasPendingRerun = false;
|
|
1552
|
+
this.schedule(
|
|
1553
|
+
agentName,
|
|
1554
|
+
PENDING_REFRESH_DELAY_MS,
|
|
1555
|
+
() => this.runRefresh(agentName, operation)
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
serialize(agentName, kind, operation) {
|
|
1561
|
+
const previous = this.operationTails.get(agentName) ?? Promise.resolve();
|
|
1562
|
+
const run = previous.then(async () => {
|
|
1563
|
+
if (this.isShuttingDown) return "skipped";
|
|
1564
|
+
const lifecycle = this.beginOperation(agentName, kind);
|
|
1565
|
+
try {
|
|
1566
|
+
const result = await operation();
|
|
1567
|
+
this.completeOperation(lifecycle, result);
|
|
1568
|
+
return result;
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
this.completeOperation(lifecycle, "failed");
|
|
1571
|
+
throw error;
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
const tail = run.then(
|
|
1575
|
+
() => void 0,
|
|
1576
|
+
() => void 0
|
|
1577
|
+
);
|
|
1578
|
+
this.operationTails.set(agentName, tail);
|
|
1579
|
+
void tail.finally(() => {
|
|
1580
|
+
if (this.operationTails.get(agentName) === tail) this.operationTails.delete(agentName);
|
|
1581
|
+
});
|
|
1582
|
+
return run;
|
|
1583
|
+
}
|
|
1584
|
+
async shutdown() {
|
|
1585
|
+
this.isShuttingDown = true;
|
|
1586
|
+
for (const state of this.states.values()) {
|
|
1587
|
+
if (!state.timer) continue;
|
|
1588
|
+
clearTimeout(state.timer);
|
|
1589
|
+
state.timer = null;
|
|
1590
|
+
state.timerDeadline = 0;
|
|
1591
|
+
}
|
|
1592
|
+
await Promise.allSettled(this.operationTails.values());
|
|
1593
|
+
}
|
|
1594
|
+
state(agentName) {
|
|
1595
|
+
const existing = this.states.get(agentName);
|
|
1596
|
+
if (existing) return existing;
|
|
1597
|
+
const state = {
|
|
1598
|
+
timer: null,
|
|
1599
|
+
timerDeadline: 0,
|
|
1600
|
+
isRunning: false,
|
|
1601
|
+
hasPendingRerun: false,
|
|
1602
|
+
lastRefreshAt: 0,
|
|
1603
|
+
lastRefreshDurationMs: 0,
|
|
1604
|
+
pendingPathCount: 0
|
|
1605
|
+
};
|
|
1606
|
+
this.states.set(agentName, state);
|
|
1607
|
+
return state;
|
|
1608
|
+
}
|
|
1609
|
+
beginOperation(agentName, kind) {
|
|
1610
|
+
const generation = (this.operationGenerations.get(agentName) ?? 0) + 1;
|
|
1611
|
+
const startedAt = Date.now();
|
|
1612
|
+
this.operationGenerations.set(agentName, generation);
|
|
1613
|
+
appLogger.info("scan.agent_operation.started", {
|
|
1614
|
+
agent: agentName,
|
|
1615
|
+
operation: kind,
|
|
1616
|
+
generation,
|
|
1617
|
+
started_at: startedAt
|
|
1618
|
+
});
|
|
1619
|
+
return { agentName, kind, generation, startedAt };
|
|
1620
|
+
}
|
|
1621
|
+
completeOperation(lifecycle, result) {
|
|
1622
|
+
const completedAt = Date.now();
|
|
1623
|
+
appLogger.info("scan.agent_operation.completed", {
|
|
1624
|
+
agent: lifecycle.agentName,
|
|
1625
|
+
operation: lifecycle.kind,
|
|
1626
|
+
generation: lifecycle.generation,
|
|
1627
|
+
started_at: lifecycle.startedAt,
|
|
1628
|
+
completed_at: completedAt,
|
|
1629
|
+
duration_ms: completedAt - lifecycle.startedAt,
|
|
1630
|
+
result
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
|
|
953
1635
|
// src/session-watcher.ts
|
|
954
|
-
import { existsSync as
|
|
1636
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, watch } from "fs";
|
|
955
1637
|
import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
|
|
956
1638
|
var WRITE_STABILITY_THRESHOLD_MS = 250;
|
|
957
1639
|
var WRITE_STABILITY_POLL_MS = 100;
|
|
@@ -959,11 +1641,11 @@ function toAbsolutePath(path) {
|
|
|
959
1641
|
return isAbsolute(path) ? path : resolve2(path);
|
|
960
1642
|
}
|
|
961
1643
|
function closestWatchablePath(targetPath) {
|
|
962
|
-
if (!isAbsolute(targetPath) && !
|
|
1644
|
+
if (!isAbsolute(targetPath) && !existsSync4(targetPath)) {
|
|
963
1645
|
return null;
|
|
964
1646
|
}
|
|
965
1647
|
let current = toAbsolutePath(targetPath);
|
|
966
|
-
while (!
|
|
1648
|
+
while (!existsSync4(current)) {
|
|
967
1649
|
const parent = dirname2(current);
|
|
968
1650
|
if (parent === current) {
|
|
969
1651
|
return null;
|
|
@@ -1271,9 +1953,9 @@ var SessionWatcher = class {
|
|
|
1271
1953
|
// src/live-scan.ts
|
|
1272
1954
|
var REFRESH_DEBOUNCE_MS = 200;
|
|
1273
1955
|
var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
|
|
1274
|
-
var PENDING_REFRESH_DELAY_MS = 100;
|
|
1275
1956
|
var NEW_SESSION_EVENT_WINDOW_MS = 250;
|
|
1276
1957
|
var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
|
|
1958
|
+
var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1277
1959
|
function buildRefreshDiff(agentName, previousSessions, nextSessions, candidateChangedIds = []) {
|
|
1278
1960
|
const { changes, removedSessionIds, counts } = computeSessionDiff(
|
|
1279
1961
|
previousSessions,
|
|
@@ -1349,27 +2031,16 @@ var LiveScanStore = class {
|
|
|
1349
2031
|
sessions = [];
|
|
1350
2032
|
listeners = /* @__PURE__ */ new Set();
|
|
1351
2033
|
scanStatusListeners = /* @__PURE__ */ new Set();
|
|
1352
|
-
scanStatus =
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
pendingAgents: [],
|
|
1356
|
-
scanningAgents: [],
|
|
1357
|
-
completedAgents: [],
|
|
1358
|
-
agentStatuses: {},
|
|
1359
|
-
totalAgents: 0,
|
|
1360
|
-
updatedAt: Date.now()
|
|
1361
|
-
};
|
|
1362
|
-
refreshTimers = /* @__PURE__ */ new Map();
|
|
1363
|
-
refreshTimestamps = /* @__PURE__ */ new Map();
|
|
1364
|
-
refreshInFlight = /* @__PURE__ */ new Set();
|
|
1365
|
-
pendingRefreshes = /* @__PURE__ */ new Set();
|
|
1366
|
-
pendingRefreshPathCounts = /* @__PURE__ */ new Map();
|
|
2034
|
+
scanStatus = new ScanStatusModel();
|
|
2035
|
+
backfills = new BackfillCoordinator();
|
|
2036
|
+
refreshes = new RefreshCoordinator();
|
|
1367
2037
|
watcher = null;
|
|
1368
2038
|
pendingEvent = null;
|
|
1369
2039
|
pendingEventTimer = null;
|
|
1370
2040
|
backgroundRefreshTimer = null;
|
|
1371
|
-
|
|
1372
|
-
|
|
2041
|
+
scanRefreshWorkers = /* @__PURE__ */ new Set();
|
|
2042
|
+
searchIndexJobs = new SearchIndexJobRunner();
|
|
2043
|
+
shutdownPromise = null;
|
|
1373
2044
|
shuttingDown = false;
|
|
1374
2045
|
async initialize() {
|
|
1375
2046
|
const startedAt = performance.now();
|
|
@@ -1395,7 +2066,7 @@ var LiveScanStore = class {
|
|
|
1395
2066
|
this.applyScanResult(initialResult);
|
|
1396
2067
|
const indexStartedAt = performance.now();
|
|
1397
2068
|
if (!deferInitialRefresh) {
|
|
1398
|
-
await this.
|
|
2069
|
+
await this.searchIndexJobs.enqueue(
|
|
1399
2070
|
"scan.initial",
|
|
1400
2071
|
this.buildFullSearchIndexJobs("scan.initial")
|
|
1401
2072
|
);
|
|
@@ -1427,10 +2098,7 @@ var LiveScanStore = class {
|
|
|
1427
2098
|
this.watcher = new SessionWatcher();
|
|
1428
2099
|
this.watcher.onAgentsChanged((agentNames) => {
|
|
1429
2100
|
for (const agentName of agentNames) {
|
|
1430
|
-
this.
|
|
1431
|
-
agentName,
|
|
1432
|
-
(this.pendingRefreshPathCounts.get(agentName) ?? 0) + 1
|
|
1433
|
-
);
|
|
2101
|
+
this.refreshes.recordChangedPaths(agentName);
|
|
1434
2102
|
const delayMs = (this.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
|
|
1435
2103
|
this.scheduleRefresh(agentName, delayMs);
|
|
1436
2104
|
}
|
|
@@ -1452,6 +2120,11 @@ var LiveScanStore = class {
|
|
|
1452
2120
|
if (agentNames.length === 0) {
|
|
1453
2121
|
this.finishScanBatch();
|
|
1454
2122
|
}
|
|
2123
|
+
for (const agent of this.agents) {
|
|
2124
|
+
if (this.needsBackfill(agent)) {
|
|
2125
|
+
this.enqueueBackfill(agent.name);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
1455
2128
|
}, 0);
|
|
1456
2129
|
}
|
|
1457
2130
|
getSnapshot() {
|
|
@@ -1462,19 +2135,7 @@ var LiveScanStore = class {
|
|
|
1462
2135
|
};
|
|
1463
2136
|
}
|
|
1464
2137
|
getScanStatus() {
|
|
1465
|
-
return
|
|
1466
|
-
type: "scan-status",
|
|
1467
|
-
...this.scanStatus,
|
|
1468
|
-
pendingAgents: [...this.scanStatus.pendingAgents],
|
|
1469
|
-
scanningAgents: [...this.scanStatus.scanningAgents],
|
|
1470
|
-
completedAgents: [...this.scanStatus.completedAgents],
|
|
1471
|
-
agentStatuses: Object.fromEntries(
|
|
1472
|
-
Object.entries(this.scanStatus.agentStatuses).map(([agentName, status]) => [
|
|
1473
|
-
agentName,
|
|
1474
|
-
{ ...status }
|
|
1475
|
-
])
|
|
1476
|
-
)
|
|
1477
|
-
};
|
|
2138
|
+
return this.scanStatus.snapshot();
|
|
1478
2139
|
}
|
|
1479
2140
|
subscribe(listener) {
|
|
1480
2141
|
this.listeners.add(listener);
|
|
@@ -1488,13 +2149,26 @@ var LiveScanStore = class {
|
|
|
1488
2149
|
this.scanStatusListeners.delete(listener);
|
|
1489
2150
|
};
|
|
1490
2151
|
}
|
|
1491
|
-
|
|
2152
|
+
shutdown() {
|
|
2153
|
+
this.shutdownPromise ??= this.performShutdown();
|
|
2154
|
+
return this.shutdownPromise;
|
|
2155
|
+
}
|
|
2156
|
+
async performShutdown() {
|
|
1492
2157
|
this.shuttingDown = true;
|
|
1493
|
-
|
|
1494
|
-
|
|
2158
|
+
const activeOperations = {
|
|
2159
|
+
agent_operations: this.refreshes.activeOperationCount,
|
|
2160
|
+
refreshes: this.refreshes.activeRefreshCount,
|
|
2161
|
+
backfill_running: this.backfills.isRunning || void 0,
|
|
2162
|
+
scan_workers: this.scanRefreshWorkers.size
|
|
2163
|
+
};
|
|
2164
|
+
if (activeOperations.agent_operations > 0 || activeOperations.scan_workers > 0) {
|
|
2165
|
+
appLogger.warn("scan.shutdown.active_operations", activeOperations);
|
|
1495
2166
|
}
|
|
1496
|
-
this.
|
|
1497
|
-
|
|
2167
|
+
const searchIndexSnapshot = this.searchIndexJobs.snapshot();
|
|
2168
|
+
appLogger.info("search_index.shutdown.started", {
|
|
2169
|
+
active_batch_id: searchIndexSnapshot.activeBatchId,
|
|
2170
|
+
pending_batches: searchIndexSnapshot.pendingBatches
|
|
2171
|
+
});
|
|
1498
2172
|
if (this.pendingEventTimer) {
|
|
1499
2173
|
clearTimeout(this.pendingEventTimer);
|
|
1500
2174
|
this.pendingEventTimer = null;
|
|
@@ -1503,21 +2177,24 @@ var LiveScanStore = class {
|
|
|
1503
2177
|
clearTimeout(this.backgroundRefreshTimer);
|
|
1504
2178
|
this.backgroundRefreshTimer = null;
|
|
1505
2179
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
batch.reject(new Error("Live scan store shut down"));
|
|
1512
|
-
}
|
|
1513
|
-
this.pendingSearchIndexJobs = [];
|
|
2180
|
+
this.backfills.clear();
|
|
2181
|
+
await this.searchIndexJobs.shutdown();
|
|
2182
|
+
const scanWorkers = [...this.scanRefreshWorkers];
|
|
2183
|
+
await Promise.allSettled(scanWorkers.map((scanWorker) => scanWorker.terminate()));
|
|
2184
|
+
await this.refreshes.shutdown();
|
|
1514
2185
|
this.pendingEvent = null;
|
|
1515
2186
|
if (this.watcher) {
|
|
1516
2187
|
await this.watcher.dispose();
|
|
1517
2188
|
this.watcher = null;
|
|
1518
2189
|
}
|
|
2190
|
+
const stoppedSearchIndexSnapshot = this.searchIndexJobs.snapshot();
|
|
2191
|
+
appLogger.info("search_index.shutdown.completed", {
|
|
2192
|
+
active_batch_id: searchIndexSnapshot.activeBatchId,
|
|
2193
|
+
pending_batches: stoppedSearchIndexSnapshot.pendingBatches
|
|
2194
|
+
});
|
|
1519
2195
|
}
|
|
1520
2196
|
emit(event) {
|
|
2197
|
+
if (this.shuttingDown) return;
|
|
1521
2198
|
if (this.pendingEvent || event.newSessions > 0) {
|
|
1522
2199
|
this.queueEvent(event);
|
|
1523
2200
|
return;
|
|
@@ -1529,157 +2206,131 @@ var LiveScanStore = class {
|
|
|
1529
2206
|
listener(event);
|
|
1530
2207
|
}
|
|
1531
2208
|
}
|
|
1532
|
-
emitScanStatus() {
|
|
1533
|
-
const event = this.getScanStatus();
|
|
1534
|
-
for (const listener of this.scanStatusListeners) {
|
|
1535
|
-
listener(event);
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
updateScanStatus(next) {
|
|
1539
|
-
this.scanStatus = next;
|
|
1540
|
-
this.emitScanStatus();
|
|
1541
|
-
}
|
|
1542
2209
|
startScanBatch(agentNames, phase) {
|
|
1543
|
-
const
|
|
1544
|
-
|
|
1545
|
-
const agentStatuses = Object.fromEntries(
|
|
1546
|
-
uniqueAgentNames.map((agentName) => [
|
|
1547
|
-
agentName,
|
|
1548
|
-
{
|
|
1549
|
-
agentName,
|
|
1550
|
-
status: "pending",
|
|
1551
|
-
processed: 0,
|
|
1552
|
-
sessions: this.byAgent[agentName]?.length ?? 0,
|
|
1553
|
-
updatedAt: now
|
|
1554
|
-
}
|
|
1555
|
-
])
|
|
2210
|
+
const sessionCounts = Object.fromEntries(
|
|
2211
|
+
agentNames.map((agentName) => [agentName, this.byAgent[agentName]?.length ?? 0])
|
|
1556
2212
|
);
|
|
1557
|
-
this.
|
|
1558
|
-
active: uniqueAgentNames.length > 0,
|
|
1559
|
-
phase: uniqueAgentNames.length > 0 ? phase : "idle",
|
|
1560
|
-
pendingAgents: uniqueAgentNames,
|
|
1561
|
-
scanningAgents: [],
|
|
1562
|
-
completedAgents: [],
|
|
1563
|
-
agentStatuses,
|
|
1564
|
-
totalAgents: uniqueAgentNames.length,
|
|
1565
|
-
startedAt: uniqueAgentNames.length > 0 ? now : void 0,
|
|
1566
|
-
updatedAt: now,
|
|
1567
|
-
completedAt: uniqueAgentNames.length > 0 ? void 0 : now
|
|
1568
|
-
});
|
|
2213
|
+
this.publishScanStatus(this.scanStatus.startBatch(agentNames, phase, sessionCounts));
|
|
1569
2214
|
}
|
|
1570
2215
|
setScanPhase(phase) {
|
|
1571
|
-
|
|
1572
|
-
this.updateScanStatus({
|
|
1573
|
-
...this.scanStatus,
|
|
1574
|
-
phase,
|
|
1575
|
-
updatedAt: Date.now()
|
|
1576
|
-
});
|
|
2216
|
+
this.publishScanStatus(this.scanStatus.setPhase(phase));
|
|
1577
2217
|
}
|
|
1578
2218
|
beginAgentScan(agentName) {
|
|
1579
|
-
if (!this.scanStatus.active)
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
const scanningAgents = [.../* @__PURE__ */ new Set([...this.scanStatus.scanningAgents, agentName])];
|
|
1584
|
-
const completedAgents = this.scanStatus.completedAgents.filter((agent) => agent !== agentName);
|
|
1585
|
-
const existingStatus = this.scanStatus.agentStatuses[agentName];
|
|
1586
|
-
const agentStatuses = {
|
|
1587
|
-
...this.scanStatus.agentStatuses,
|
|
1588
|
-
[agentName]: {
|
|
1589
|
-
agentName,
|
|
1590
|
-
status: "scanning",
|
|
1591
|
-
total: existingStatus?.total,
|
|
1592
|
-
processed: existingStatus?.processed ?? 0,
|
|
1593
|
-
sessions: existingStatus?.sessions ?? this.byAgent[agentName]?.length ?? 0,
|
|
1594
|
-
startedAt: existingStatus?.startedAt ?? Date.now(),
|
|
1595
|
-
updatedAt: Date.now()
|
|
1596
|
-
}
|
|
1597
|
-
};
|
|
1598
|
-
this.updateScanStatus({
|
|
1599
|
-
...this.scanStatus,
|
|
1600
|
-
active: true,
|
|
1601
|
-
phase: this.scanStatus.phase === "initializing" ? "initializing" : "scanning",
|
|
1602
|
-
pendingAgents,
|
|
1603
|
-
scanningAgents,
|
|
1604
|
-
completedAgents,
|
|
1605
|
-
agentStatuses,
|
|
1606
|
-
totalAgents: Math.max(
|
|
1607
|
-
this.scanStatus.totalAgents,
|
|
1608
|
-
pendingAgents.length + scanningAgents.length
|
|
1609
|
-
),
|
|
1610
|
-
updatedAt: Date.now(),
|
|
1611
|
-
completedAt: void 0
|
|
1612
|
-
});
|
|
2219
|
+
if (!this.scanStatus.snapshot().active) this.startScanBatch([agentName], "scanning");
|
|
2220
|
+
this.publishScanStatus(
|
|
2221
|
+
this.scanStatus.beginAgent(agentName, this.byAgent[agentName]?.length ?? 0)
|
|
2222
|
+
);
|
|
1613
2223
|
}
|
|
1614
2224
|
updateAgentScanProgress(agentName, progress) {
|
|
1615
|
-
|
|
1616
|
-
if (!status || status.status !== "scanning") return;
|
|
1617
|
-
this.updateScanStatus({
|
|
1618
|
-
...this.scanStatus,
|
|
1619
|
-
agentStatuses: {
|
|
1620
|
-
...this.scanStatus.agentStatuses,
|
|
1621
|
-
[agentName]: {
|
|
1622
|
-
...status,
|
|
1623
|
-
total: progress.total ?? status.total,
|
|
1624
|
-
processed: progress.processed ?? status.processed,
|
|
1625
|
-
sessions: progress.sessions ?? status.sessions,
|
|
1626
|
-
updatedAt: Date.now()
|
|
1627
|
-
}
|
|
1628
|
-
},
|
|
1629
|
-
updatedAt: Date.now()
|
|
1630
|
-
});
|
|
2225
|
+
this.publishScanStatus(this.scanStatus.updateAgent(agentName, progress));
|
|
1631
2226
|
}
|
|
1632
2227
|
finishAgentScan(agentName) {
|
|
1633
|
-
|
|
1634
|
-
const scanningAgents = this.scanStatus.scanningAgents.filter((agent) => agent !== agentName);
|
|
1635
|
-
const completedAgents = [.../* @__PURE__ */ new Set([...this.scanStatus.completedAgents, agentName])];
|
|
1636
|
-
const active = pendingAgents.length > 0 || scanningAgents.length > 0;
|
|
1637
|
-
const now = Date.now();
|
|
1638
|
-
const previousStatus = this.scanStatus.agentStatuses[agentName];
|
|
1639
|
-
const sessions = this.byAgent[agentName]?.length ?? previousStatus?.sessions ?? 0;
|
|
1640
|
-
const total = previousStatus?.total ?? previousStatus?.processed;
|
|
1641
|
-
this.updateScanStatus({
|
|
1642
|
-
...this.scanStatus,
|
|
1643
|
-
active,
|
|
1644
|
-
phase: active ? "scanning" : "idle",
|
|
1645
|
-
pendingAgents,
|
|
1646
|
-
scanningAgents,
|
|
1647
|
-
completedAgents,
|
|
1648
|
-
agentStatuses: {
|
|
1649
|
-
...this.scanStatus.agentStatuses,
|
|
1650
|
-
[agentName]: {
|
|
1651
|
-
agentName,
|
|
1652
|
-
status: "complete",
|
|
1653
|
-
total,
|
|
1654
|
-
processed: total,
|
|
1655
|
-
sessions,
|
|
1656
|
-
startedAt: previousStatus?.startedAt,
|
|
1657
|
-
updatedAt: now,
|
|
1658
|
-
completedAt: now
|
|
1659
|
-
}
|
|
1660
|
-
},
|
|
1661
|
-
updatedAt: now,
|
|
1662
|
-
completedAt: active ? void 0 : now
|
|
1663
|
-
});
|
|
2228
|
+
this.publishScanStatus(this.scanStatus.finishAgent(agentName, this.byAgent[agentName]?.length));
|
|
1664
2229
|
}
|
|
1665
2230
|
finishScanBatch() {
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
2231
|
+
this.publishScanStatus(this.scanStatus.finishBatch());
|
|
2232
|
+
}
|
|
2233
|
+
updateBackfillStatus(patch) {
|
|
2234
|
+
this.publishScanStatus(this.scanStatus.updateBackfill(patch));
|
|
2235
|
+
}
|
|
2236
|
+
publishScanStatus(event) {
|
|
2237
|
+
if (!event || this.shuttingDown) return;
|
|
2238
|
+
for (const listener of this.scanStatusListeners) listener(event);
|
|
2239
|
+
}
|
|
2240
|
+
/**
|
|
2241
|
+
* Only FileSystemSessionSource agents pay the O(history) enumeration cost this
|
|
2242
|
+
* guards against: database agents already do a cheap single-file mtime check.
|
|
2243
|
+
* With no startup window configured, the regular refresh path already walks
|
|
2244
|
+
* full history, so backfill would be redundant.
|
|
2245
|
+
*/
|
|
2246
|
+
needsBackfill(agent) {
|
|
2247
|
+
if (this.startupScanOptions.from == null && this.startupScanOptions.to == null) return false;
|
|
2248
|
+
if (!(agent instanceof FileSystemSessionSource)) return false;
|
|
2249
|
+
if (!agent.isAvailable()) return false;
|
|
2250
|
+
const lastSyncAt = getAgentLastFullSyncAt(agent.name);
|
|
2251
|
+
return lastSyncAt == null || Date.now() - lastSyncAt > BACKFILL_INTERVAL_MS;
|
|
2252
|
+
}
|
|
2253
|
+
enqueueBackfill(agentName) {
|
|
2254
|
+
if (this.shuttingDown) return;
|
|
2255
|
+
const status = this.backfills.enqueue(agentName);
|
|
2256
|
+
if (!status) return;
|
|
2257
|
+
this.updateBackfillStatus(status);
|
|
2258
|
+
this.pumpBackfillQueue();
|
|
2259
|
+
}
|
|
2260
|
+
pumpBackfillQueue() {
|
|
2261
|
+
if (this.shuttingDown) return;
|
|
2262
|
+
const work = this.backfills.take();
|
|
2263
|
+
if (!work) return;
|
|
2264
|
+
this.updateBackfillStatus(work.status);
|
|
2265
|
+
void this.runBackfill(work.agentName).finally(() => {
|
|
2266
|
+
if (this.shuttingDown) return;
|
|
2267
|
+
this.updateBackfillStatus(this.backfills.complete(work.agentName));
|
|
2268
|
+
this.pumpBackfillQueue();
|
|
1681
2269
|
});
|
|
1682
2270
|
}
|
|
2271
|
+
/** Unbounded per-source sync to reconcile the full session history, not just the display window. */
|
|
2272
|
+
async runBackfill(agentName) {
|
|
2273
|
+
await this.refreshes.serialize(agentName, "backfill", () => this.performBackfill(agentName));
|
|
2274
|
+
}
|
|
2275
|
+
async performBackfill(agentName) {
|
|
2276
|
+
const startedAt = performance.now();
|
|
2277
|
+
const agent = this.agents.find((item) => item.name === agentName);
|
|
2278
|
+
if (!agent || !(agent instanceof FileSystemSessionSource) || !agent.isAvailable()) {
|
|
2279
|
+
return "skipped";
|
|
2280
|
+
}
|
|
2281
|
+
const cached = loadCachedSessions(agentName);
|
|
2282
|
+
const baseline = cached?.sessions ?? this.byAgent[agentName] ?? [];
|
|
2283
|
+
const meta = cached?.meta ?? buildAgentCacheMeta(agent);
|
|
2284
|
+
if (cached) {
|
|
2285
|
+
restoreAgentCacheMeta(agent, cached.meta);
|
|
2286
|
+
}
|
|
2287
|
+
try {
|
|
2288
|
+
const result = await this.scanAgentInWorker(
|
|
2289
|
+
agent,
|
|
2290
|
+
baseline,
|
|
2291
|
+
null,
|
|
2292
|
+
{},
|
|
2293
|
+
{ sourceSync: true, meta }
|
|
2294
|
+
);
|
|
2295
|
+
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2296
|
+
const fullSessions = attachMissingProjectIdentities(result.sessions);
|
|
2297
|
+
const filtered = this.applyFilters(fullSessions);
|
|
2298
|
+
const diff = buildRefreshDiff(
|
|
2299
|
+
agentName,
|
|
2300
|
+
this.byAgent[agentName] ?? [],
|
|
2301
|
+
filtered,
|
|
2302
|
+
result.changedIds ?? []
|
|
2303
|
+
);
|
|
2304
|
+
this.byAgent[agentName] = sortSessions(filtered);
|
|
2305
|
+
this.rebuildSessions();
|
|
2306
|
+
await this.searchIndexJobs.enqueue("scan.backfill", [
|
|
2307
|
+
{
|
|
2308
|
+
kind: "full",
|
|
2309
|
+
context: "scan.backfill",
|
|
2310
|
+
agentName,
|
|
2311
|
+
sessions: fullSessions,
|
|
2312
|
+
meta: buildAgentCacheMeta(agent),
|
|
2313
|
+
saveCache: true
|
|
2314
|
+
}
|
|
2315
|
+
]);
|
|
2316
|
+
markAgentFullSyncCompleted(agentName);
|
|
2317
|
+
if (diff.event) {
|
|
2318
|
+
diff.event.totalSessions = this.sessions.length;
|
|
2319
|
+
this.emit(diff.event);
|
|
2320
|
+
}
|
|
2321
|
+
appLogger.info("scan.backfill.done", {
|
|
2322
|
+
agent: agentName,
|
|
2323
|
+
duration_ms: Math.round(performance.now() - startedAt),
|
|
2324
|
+
sessions: fullSessions.length,
|
|
2325
|
+
changed: result.changedIds?.length ?? 0
|
|
2326
|
+
});
|
|
2327
|
+
return "committed";
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
appLogger.error("scan.backfill.error", { agent: agentName, error });
|
|
2330
|
+
console.error(`[${agentName}] Backfill failed:`, error);
|
|
2331
|
+
return "failed";
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
1683
2334
|
queueEvent(event) {
|
|
1684
2335
|
this.pendingEvent = this.pendingEvent ? mergeEvents(this.pendingEvent, event) : event;
|
|
1685
2336
|
if (this.pendingEventTimer) {
|
|
@@ -1697,16 +2348,9 @@ var LiveScanStore = class {
|
|
|
1697
2348
|
rebuildSessions() {
|
|
1698
2349
|
this.sessions = sortSessions(Object.values(this.byAgent).flat());
|
|
1699
2350
|
}
|
|
1700
|
-
getSearchIndexWorkerUrl() {
|
|
1701
|
-
const workerUrl = new URL("./search-index-worker.js", import.meta.url);
|
|
1702
|
-
if (workerUrl.protocol === "file:" && !existsSync4(fileURLToPath2(workerUrl))) {
|
|
1703
|
-
return null;
|
|
1704
|
-
}
|
|
1705
|
-
return workerUrl;
|
|
1706
|
-
}
|
|
1707
2351
|
getSmartTagWorkerUrl() {
|
|
1708
2352
|
const workerUrl = new URL("./smart-tag-worker.js", import.meta.url);
|
|
1709
|
-
if (workerUrl.protocol === "file:" && !
|
|
2353
|
+
if (workerUrl.protocol === "file:" && !existsSync5(fileURLToPath3(workerUrl))) {
|
|
1710
2354
|
return null;
|
|
1711
2355
|
}
|
|
1712
2356
|
return workerUrl;
|
|
@@ -1717,7 +2361,7 @@ var LiveScanStore = class {
|
|
|
1717
2361
|
scanAgentInWorker(agent, previousSessions, changedIds, scanOptions, workerOptions = {}) {
|
|
1718
2362
|
const workerUrl = this.getScanRefreshWorkerUrl();
|
|
1719
2363
|
return new Promise((resolve4, reject) => {
|
|
1720
|
-
const worker = new
|
|
2364
|
+
const worker = new Worker2(workerUrl, {
|
|
1721
2365
|
workerData: {
|
|
1722
2366
|
agentName: agent.name,
|
|
1723
2367
|
previousSessions,
|
|
@@ -1728,11 +2372,13 @@ var LiveScanStore = class {
|
|
|
1728
2372
|
}
|
|
1729
2373
|
});
|
|
1730
2374
|
worker.unref();
|
|
2375
|
+
this.scanRefreshWorkers.add(worker);
|
|
1731
2376
|
let settled = false;
|
|
1732
|
-
const finish = (callback) => {
|
|
2377
|
+
const finish = (callback, terminate = true) => {
|
|
1733
2378
|
if (settled) return;
|
|
1734
2379
|
settled = true;
|
|
1735
|
-
|
|
2380
|
+
this.scanRefreshWorkers.delete(worker);
|
|
2381
|
+
if (terminate) void worker.terminate();
|
|
1736
2382
|
callback();
|
|
1737
2383
|
};
|
|
1738
2384
|
worker.on("message", (message) => {
|
|
@@ -1756,8 +2402,15 @@ var LiveScanStore = class {
|
|
|
1756
2402
|
finish(() => reject(error));
|
|
1757
2403
|
});
|
|
1758
2404
|
worker.once("exit", (code) => {
|
|
1759
|
-
if (!settled
|
|
1760
|
-
|
|
2405
|
+
if (!settled) {
|
|
2406
|
+
appLogger.warn("scan.refresh_worker.exit_before_done", {
|
|
2407
|
+
agent: agent.name,
|
|
2408
|
+
code
|
|
2409
|
+
});
|
|
2410
|
+
finish(
|
|
2411
|
+
() => reject(new Error(`Scan refresh worker exited before completing (code ${code})`)),
|
|
2412
|
+
false
|
|
2413
|
+
);
|
|
1761
2414
|
}
|
|
1762
2415
|
});
|
|
1763
2416
|
});
|
|
@@ -1783,75 +2436,6 @@ var LiveScanStore = class {
|
|
|
1783
2436
|
};
|
|
1784
2437
|
});
|
|
1785
2438
|
}
|
|
1786
|
-
enqueueSearchIndexJobs(context, jobs) {
|
|
1787
|
-
if (jobs.length === 0) return Promise.resolve();
|
|
1788
|
-
return new Promise((resolve4, reject) => {
|
|
1789
|
-
const batch = { context, jobs, resolve: resolve4, reject };
|
|
1790
|
-
if (this.searchIndexWorker) {
|
|
1791
|
-
this.pendingSearchIndexJobs.push(batch);
|
|
1792
|
-
appLogger.debug("search_index.worker_queued", {
|
|
1793
|
-
context,
|
|
1794
|
-
jobs: jobs.length,
|
|
1795
|
-
pending_jobs: this.pendingSearchIndexJobs.length
|
|
1796
|
-
});
|
|
1797
|
-
return;
|
|
1798
|
-
}
|
|
1799
|
-
this.startSearchIndexJobBatch(batch);
|
|
1800
|
-
});
|
|
1801
|
-
}
|
|
1802
|
-
startSearchIndexJobBatch(batch) {
|
|
1803
|
-
const workerUrl = this.getSearchIndexWorkerUrl();
|
|
1804
|
-
if (!workerUrl) {
|
|
1805
|
-
appLogger.warn("search_index.worker_missing", { context: batch.context });
|
|
1806
|
-
batch.resolve();
|
|
1807
|
-
return;
|
|
1808
|
-
}
|
|
1809
|
-
let settled = false;
|
|
1810
|
-
const worker = new Worker(workerUrl, {
|
|
1811
|
-
workerData: {
|
|
1812
|
-
context: batch.context,
|
|
1813
|
-
jobs: batch.jobs,
|
|
1814
|
-
agentNames: [],
|
|
1815
|
-
sessionsByAgent: {},
|
|
1816
|
-
metaByAgent: {}
|
|
1817
|
-
}
|
|
1818
|
-
});
|
|
1819
|
-
worker.unref();
|
|
1820
|
-
this.searchIndexWorker = worker;
|
|
1821
|
-
worker.on("message", (message) => {
|
|
1822
|
-
if (message.type === "sync-result") {
|
|
1823
|
-
logSearchIndexSync(message.context, message.result);
|
|
1824
|
-
} else if (message.type === "done") {
|
|
1825
|
-
appLogger.info(`${message.context}.done`, {
|
|
1826
|
-
duration_ms: Math.round(message.durationMs),
|
|
1827
|
-
sessions: message.sessions
|
|
1828
|
-
});
|
|
1829
|
-
settled = true;
|
|
1830
|
-
batch.resolve();
|
|
1831
|
-
}
|
|
1832
|
-
});
|
|
1833
|
-
worker.on("error", (error) => {
|
|
1834
|
-
appLogger.error("search_index.worker_error", { context: batch.context, error });
|
|
1835
|
-
if (!settled) {
|
|
1836
|
-
settled = true;
|
|
1837
|
-
batch.reject(error);
|
|
1838
|
-
}
|
|
1839
|
-
});
|
|
1840
|
-
worker.on("exit", (code) => {
|
|
1841
|
-
this.searchIndexWorker = null;
|
|
1842
|
-
if (code !== 0) {
|
|
1843
|
-
appLogger.warn("search_index.worker_exit", { context: batch.context, code });
|
|
1844
|
-
if (!settled) {
|
|
1845
|
-
settled = true;
|
|
1846
|
-
batch.reject(new Error(`Search index worker exited with code ${code}`));
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
if (this.pendingSearchIndexJobs.length > 0) {
|
|
1850
|
-
const pendingBatch = this.pendingSearchIndexJobs.shift();
|
|
1851
|
-
this.startSearchIndexJobBatch(pendingBatch);
|
|
1852
|
-
}
|
|
1853
|
-
});
|
|
1854
|
-
}
|
|
1855
2439
|
applyScanResult(result) {
|
|
1856
2440
|
const knownAgents = createRegisteredAgents();
|
|
1857
2441
|
const agentMap = /* @__PURE__ */ new Map();
|
|
@@ -1873,7 +2457,10 @@ var LiveScanStore = class {
|
|
|
1873
2457
|
this.byAgent = {};
|
|
1874
2458
|
for (const agent of this.agents) {
|
|
1875
2459
|
this.byAgent[agent.name] = sortSessions(result.byAgent[agent.name] ?? []);
|
|
1876
|
-
this.
|
|
2460
|
+
this.refreshes.setLastRefreshAt(
|
|
2461
|
+
agent.name,
|
|
2462
|
+
result.cacheTimestamps?.[agent.name] ?? Date.now()
|
|
2463
|
+
);
|
|
1877
2464
|
}
|
|
1878
2465
|
this.rebuildSessions();
|
|
1879
2466
|
}
|
|
@@ -1890,7 +2477,7 @@ var LiveScanStore = class {
|
|
|
1890
2477
|
const startedAt = performance.now();
|
|
1891
2478
|
const context = "scan.initial.background";
|
|
1892
2479
|
try {
|
|
1893
|
-
await this.
|
|
2480
|
+
await this.searchIndexJobs.enqueue(context, this.buildFullSearchIndexJobs(context));
|
|
1894
2481
|
appLogger.info(`${context}.complete`, {
|
|
1895
2482
|
duration_ms: Math.round(performance.now() - startedAt),
|
|
1896
2483
|
sessions: this.sessions.length
|
|
@@ -1903,52 +2490,41 @@ var LiveScanStore = class {
|
|
|
1903
2490
|
console.error("[search] Background index sync failed:", error);
|
|
1904
2491
|
}
|
|
1905
2492
|
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Throttles rather than debounces: a pending timer only gets replaced by a
|
|
2495
|
+
* request with an earlier deadline. Plain debounce (reset on every call)
|
|
2496
|
+
* would let a steady stream of events — each arriving before the adaptive
|
|
2497
|
+
* backoff elapses — push the deadline out forever and starve the refresh.
|
|
2498
|
+
*/
|
|
1906
2499
|
scheduleRefresh(agentName, delayMs = REFRESH_DEBOUNCE_MS) {
|
|
1907
|
-
|
|
1908
|
-
const existing = this.refreshTimers.get(agentName);
|
|
1909
|
-
if (existing) {
|
|
1910
|
-
clearTimeout(existing);
|
|
1911
|
-
}
|
|
1912
|
-
const timer = setTimeout(() => {
|
|
1913
|
-
this.refreshTimers.delete(agentName);
|
|
1914
|
-
void this.refreshAgent(agentName);
|
|
1915
|
-
}, delayMs);
|
|
1916
|
-
this.refreshTimers.set(agentName, timer);
|
|
2500
|
+
this.refreshes.schedule(agentName, delayMs, () => this.refreshAgent(agentName));
|
|
1917
2501
|
}
|
|
1918
2502
|
async refreshAgent(agentName) {
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
appLogger.error("scan.refresh.error", { agent: agentName, error });
|
|
1930
|
-
console.error(`[${agentName}] Session refresh failed:`, error);
|
|
1931
|
-
} finally {
|
|
1932
|
-
this.refreshInFlight.delete(agentName);
|
|
1933
|
-
this.finishAgentScan(agentName);
|
|
1934
|
-
if (this.pendingRefreshes.delete(agentName)) {
|
|
1935
|
-
this.scheduleRefresh(agentName, PENDING_REFRESH_DELAY_MS);
|
|
2503
|
+
await this.refreshes.runRefresh(agentName, async () => {
|
|
2504
|
+
this.beginAgentScan(agentName);
|
|
2505
|
+
try {
|
|
2506
|
+
return await this.runRefresh(agentName);
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
appLogger.error("scan.refresh.error", { agent: agentName, error });
|
|
2509
|
+
console.error(`[${agentName}] Session refresh failed:`, error);
|
|
2510
|
+
return "failed";
|
|
2511
|
+
} finally {
|
|
2512
|
+
this.finishAgentScan(agentName);
|
|
1936
2513
|
}
|
|
1937
|
-
}
|
|
2514
|
+
});
|
|
1938
2515
|
}
|
|
1939
2516
|
async runRefresh(agentName) {
|
|
1940
2517
|
const startedAt = performance.now();
|
|
1941
|
-
const pendingPathCount = this.
|
|
1942
|
-
this.pendingRefreshPathCounts.delete(agentName);
|
|
2518
|
+
const pendingPathCount = this.refreshes.takePendingPathCount(agentName);
|
|
1943
2519
|
const agent = this.agents.find((item) => item.name === agentName);
|
|
1944
2520
|
if (!agent) {
|
|
1945
2521
|
appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
|
|
1946
|
-
return;
|
|
2522
|
+
return "skipped";
|
|
1947
2523
|
}
|
|
1948
2524
|
const previousSessions = this.byAgent[agentName] ?? [];
|
|
1949
2525
|
const cached = loadCachedSessions(agentName);
|
|
1950
2526
|
const refreshBaseline = cached?.sessions ?? previousSessions;
|
|
1951
|
-
const cacheTimestamp = cached?.timestamp ?? this.
|
|
2527
|
+
const cacheTimestamp = cached?.timestamp ?? this.refreshes.lastRefreshAt(agentName);
|
|
1952
2528
|
if (cached) {
|
|
1953
2529
|
restoreAgentCacheMeta(agent, cached.meta);
|
|
1954
2530
|
}
|
|
@@ -1971,24 +2547,29 @@ var LiveScanStore = class {
|
|
|
1971
2547
|
availabilityDuration = performance.now() - availabilityStartedAt;
|
|
1972
2548
|
if (!isAvailable) {
|
|
1973
2549
|
nextSessions = [];
|
|
1974
|
-
this.
|
|
2550
|
+
this.refreshes.setLastRefreshAt(agentName, Date.now());
|
|
1975
2551
|
} else if (!isInitialized) {
|
|
1976
2552
|
this.setScanPhase("initializing");
|
|
1977
2553
|
const scanStartedAt = performance.now();
|
|
1978
|
-
const result = await this.scanAgentInWorker(
|
|
2554
|
+
const result = await this.scanAgentInWorker(
|
|
2555
|
+
agent,
|
|
2556
|
+
previousSessions,
|
|
2557
|
+
null,
|
|
2558
|
+
this.startupScanOptions
|
|
2559
|
+
);
|
|
1979
2560
|
nextSessions = result.sessions;
|
|
1980
2561
|
agent.setSessionMetaMap?.(new Map(Object.entries(result.meta)));
|
|
1981
2562
|
fullScanSessions = attachMissingProjectIdentities(nextSessions);
|
|
1982
2563
|
nextSessions = fullScanSessions;
|
|
1983
2564
|
scanDuration = performance.now() - scanStartedAt;
|
|
1984
|
-
this.
|
|
2565
|
+
this.refreshes.setLastRefreshAt(agentName, Date.now());
|
|
1985
2566
|
} else if (cached && agent instanceof FileSystemSessionSource) {
|
|
1986
2567
|
const scanStartedAt = performance.now();
|
|
1987
2568
|
const result = await this.scanAgentInWorker(
|
|
1988
2569
|
agent,
|
|
1989
2570
|
cached.sessions,
|
|
1990
2571
|
null,
|
|
1991
|
-
|
|
2572
|
+
this.startupScanOptions,
|
|
1992
2573
|
{
|
|
1993
2574
|
sourceSync: true,
|
|
1994
2575
|
meta: cached.meta
|
|
@@ -2005,7 +2586,7 @@ var LiveScanStore = class {
|
|
|
2005
2586
|
preciseChangedIds
|
|
2006
2587
|
);
|
|
2007
2588
|
scanDuration = performance.now() - scanStartedAt;
|
|
2008
|
-
this.
|
|
2589
|
+
this.refreshes.setLastRefreshAt(agentName, Date.now());
|
|
2009
2590
|
if (preciseChangedIds.length === 0) {
|
|
2010
2591
|
appLogger.debug("scan.refresh.unchanged", {
|
|
2011
2592
|
agent: agentName,
|
|
@@ -2018,13 +2599,13 @@ var LiveScanStore = class {
|
|
|
2018
2599
|
agent.checkForChanges(cacheTimestamp, refreshBaseline)
|
|
2019
2600
|
);
|
|
2020
2601
|
checkDuration = performance.now() - checkStartedAt;
|
|
2021
|
-
this.
|
|
2602
|
+
this.refreshes.setLastRefreshAt(agentName, checkResult.timestamp);
|
|
2022
2603
|
if (!checkResult.hasChanges) {
|
|
2023
2604
|
appLogger.debug("scan.refresh.unchanged", {
|
|
2024
2605
|
agent: agentName,
|
|
2025
2606
|
duration_ms: Math.round(performance.now() - startedAt)
|
|
2026
2607
|
});
|
|
2027
|
-
return;
|
|
2608
|
+
return "unchanged";
|
|
2028
2609
|
}
|
|
2029
2610
|
preciseChangedIds = checkResult.changedIds ?? null;
|
|
2030
2611
|
usedIncrementalScan = Array.isArray(checkResult.changedIds);
|
|
@@ -2049,7 +2630,7 @@ var LiveScanStore = class {
|
|
|
2049
2630
|
fullScanSessions = attachMissingProjectIdentities(nextSessions);
|
|
2050
2631
|
nextSessions = fullScanSessions;
|
|
2051
2632
|
scanDuration = performance.now() - scanStartedAt;
|
|
2052
|
-
this.
|
|
2633
|
+
this.refreshes.setLastRefreshAt(agentName, Date.now());
|
|
2053
2634
|
}
|
|
2054
2635
|
nextSessions = attachMissingProjectIdentities(nextSessions);
|
|
2055
2636
|
const filterStartedAt = performance.now();
|
|
@@ -2089,7 +2670,7 @@ var LiveScanStore = class {
|
|
|
2089
2670
|
} : null;
|
|
2090
2671
|
if (persistentJob) {
|
|
2091
2672
|
persistentJobKind = persistentJob.kind;
|
|
2092
|
-
const persist = this.
|
|
2673
|
+
const persist = this.searchIndexJobs.enqueue("scan.refresh", [persistentJob]);
|
|
2093
2674
|
if (!isInitialized && persistentJob.kind === "full") {
|
|
2094
2675
|
await persist;
|
|
2095
2676
|
} else {
|
|
@@ -2110,9 +2691,11 @@ var LiveScanStore = class {
|
|
|
2110
2691
|
event.totalSessions = this.sessions.length;
|
|
2111
2692
|
this.emit(event);
|
|
2112
2693
|
}
|
|
2694
|
+
const totalDurationMs = performance.now() - startedAt;
|
|
2695
|
+
this.refreshes.setLastRefreshDuration(agentName, totalDurationMs);
|
|
2113
2696
|
appLogger.info("scan.refresh.done", {
|
|
2114
2697
|
agent: agentName,
|
|
2115
|
-
duration_ms: Math.round(
|
|
2698
|
+
duration_ms: Math.round(totalDurationMs),
|
|
2116
2699
|
sessions: nextSessions.length,
|
|
2117
2700
|
new_sessions: event?.newSessions ?? 0,
|
|
2118
2701
|
updated_sessions: event?.updatedSessions ?? 0,
|
|
@@ -2128,6 +2711,7 @@ var LiveScanStore = class {
|
|
|
2128
2711
|
persistent_index_worker_job: persistentJobKind,
|
|
2129
2712
|
persistent_index_skipped: !persistentJob || void 0
|
|
2130
2713
|
});
|
|
2714
|
+
return "committed";
|
|
2131
2715
|
}
|
|
2132
2716
|
};
|
|
2133
2717
|
|
|
@@ -2137,8 +2721,8 @@ import { consola } from "consola";
|
|
|
2137
2721
|
// src/version.ts
|
|
2138
2722
|
import { readFileSync } from "fs";
|
|
2139
2723
|
import { resolve as resolve3, dirname as dirname3 } from "path";
|
|
2140
|
-
import { fileURLToPath as
|
|
2141
|
-
var __dirname = dirname3(
|
|
2724
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2725
|
+
var __dirname = dirname3(fileURLToPath4(import.meta.url));
|
|
2142
2726
|
var pkg = JSON.parse(readFileSync(resolve3(__dirname, "../package.json"), "utf-8"));
|
|
2143
2727
|
var VERSION = pkg.version;
|
|
2144
2728
|
|
|
@@ -2188,6 +2772,18 @@ function parseSessionUri(uri) {
|
|
|
2188
2772
|
if (!match) return null;
|
|
2189
2773
|
return { agent: match[1], sessionId: match[2] };
|
|
2190
2774
|
}
|
|
2775
|
+
function appendStartupPath(startupUrl, path) {
|
|
2776
|
+
const url = new URL(startupUrl);
|
|
2777
|
+
url.pathname = path;
|
|
2778
|
+
return url.toString();
|
|
2779
|
+
}
|
|
2780
|
+
function redactStartupUrl(startupUrl) {
|
|
2781
|
+
const url = new URL(startupUrl);
|
|
2782
|
+
for (const key of url.searchParams.keys()) {
|
|
2783
|
+
url.searchParams.set(key, "[redacted]");
|
|
2784
|
+
}
|
|
2785
|
+
return url.toString();
|
|
2786
|
+
}
|
|
2191
2787
|
var main = defineCommand({
|
|
2192
2788
|
meta: {
|
|
2193
2789
|
name: "codesesh",
|
|
@@ -2201,6 +2797,16 @@ var main = defineCommand({
|
|
|
2201
2797
|
description: "HTTP server port",
|
|
2202
2798
|
default: String(DEFAULT_PORT)
|
|
2203
2799
|
},
|
|
2800
|
+
host: {
|
|
2801
|
+
type: "string",
|
|
2802
|
+
description: "HTTP server bind address (default 127.0.0.1, local access only)",
|
|
2803
|
+
default: "127.0.0.1"
|
|
2804
|
+
},
|
|
2805
|
+
"remote-access": {
|
|
2806
|
+
type: "boolean",
|
|
2807
|
+
description: "Allow authenticated access when binding to a non-loopback address",
|
|
2808
|
+
default: false
|
|
2809
|
+
},
|
|
2204
2810
|
agent: {
|
|
2205
2811
|
type: "string",
|
|
2206
2812
|
alias: "a",
|
|
@@ -2265,6 +2871,14 @@ var main = defineCommand({
|
|
|
2265
2871
|
const trace = args.trace;
|
|
2266
2872
|
const useCache = args.cache;
|
|
2267
2873
|
const clearCache = args["clear-cache"];
|
|
2874
|
+
const hostname = args.host;
|
|
2875
|
+
const remoteAccess = args["remote-access"];
|
|
2876
|
+
if (!isLoopbackHostname(hostname) && !remoteAccess) {
|
|
2877
|
+
console.error(
|
|
2878
|
+
`Refusing to expose CodeSesh on ${hostname} without authentication. Add --remote-access to continue.`
|
|
2879
|
+
);
|
|
2880
|
+
process.exit(1);
|
|
2881
|
+
}
|
|
2268
2882
|
if (trace) {
|
|
2269
2883
|
perf.enable();
|
|
2270
2884
|
}
|
|
@@ -2278,7 +2892,7 @@ var main = defineCommand({
|
|
|
2278
2892
|
log_path: appLogger.getLogPath()
|
|
2279
2893
|
});
|
|
2280
2894
|
if (clearCache) {
|
|
2281
|
-
const { clearCache: clear } = await import("./dist-
|
|
2895
|
+
const { clearCache: clear } = await import("./dist-VD54GDPO.js");
|
|
2282
2896
|
clear();
|
|
2283
2897
|
appLogger.info("cache.clear");
|
|
2284
2898
|
console.log("Cache cleared.");
|
|
@@ -2367,7 +2981,9 @@ var main = defineCommand({
|
|
|
2367
2981
|
defaultSessionFrom: listDefaultFrom,
|
|
2368
2982
|
defaultSessionTo: listDefaultTo,
|
|
2369
2983
|
defaultSessionDays: listDefaultDays,
|
|
2370
|
-
portFallbackAttempts: explicitPort ? 1 : DEFAULT_PORT_FALLBACK_ATTEMPTS
|
|
2984
|
+
portFallbackAttempts: explicitPort ? 1 : DEFAULT_PORT_FALLBACK_ATTEMPTS,
|
|
2985
|
+
hostname,
|
|
2986
|
+
remoteAccess
|
|
2371
2987
|
});
|
|
2372
2988
|
} catch (error) {
|
|
2373
2989
|
console.error(getServerStartupErrorMessage(error, port));
|
|
@@ -2394,14 +3010,14 @@ var main = defineCommand({
|
|
|
2394
3010
|
console.log(` ${url}`);
|
|
2395
3011
|
console.log("");
|
|
2396
3012
|
appLogger.info("cli.ready", {
|
|
2397
|
-
url,
|
|
3013
|
+
url: redactStartupUrl(url),
|
|
2398
3014
|
duration_ms: Math.round(performance.now() - startedAt),
|
|
2399
3015
|
log_path: appLogger.getLogPath()
|
|
2400
3016
|
});
|
|
2401
3017
|
if (!noOpen) {
|
|
2402
3018
|
const open = (await import("open")).default;
|
|
2403
|
-
const targetUrl = targetSession ?
|
|
2404
|
-
appLogger.info("browser.open", { url: targetUrl });
|
|
3019
|
+
const targetUrl = targetSession ? appendStartupPath(url, `/${targetSession.agent.toLowerCase()}/${targetSession.sessionId}`) : url;
|
|
3020
|
+
appLogger.info("browser.open", { url: redactStartupUrl(targetUrl) });
|
|
2405
3021
|
await open(targetUrl);
|
|
2406
3022
|
}
|
|
2407
3023
|
}
|