codesesh 0.12.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-GCOAE7KI.js → chunk-BV65IEWZ.js} +368 -179
- package/dist/chunk-BV65IEWZ.js.map +1 -0
- package/dist/{dist-TZTL6VP4.js → dist-VD54GDPO.js} +10 -2
- package/dist/index.js +949 -510
- 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-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-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-VD54GDPO.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
createProjectScopeMatcher,
|
|
12
12
|
createRegisteredAgents,
|
|
13
13
|
deleteBookmark,
|
|
14
|
+
executeSessionSearch,
|
|
14
15
|
extractSessionFileActivity,
|
|
15
16
|
filterSessions,
|
|
16
17
|
getAgentInfoMap,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
getTotalTokens,
|
|
23
24
|
importBookmarks,
|
|
24
25
|
isAgentCacheInitialized,
|
|
26
|
+
isProjectIdentityKind,
|
|
25
27
|
listBookmarks,
|
|
26
28
|
listCachedProjectGroups,
|
|
27
29
|
listFileActivity,
|
|
@@ -29,26 +31,25 @@ import {
|
|
|
29
31
|
loadCachedSessionData,
|
|
30
32
|
loadCachedSessions,
|
|
31
33
|
markAgentFullSyncCompleted,
|
|
34
|
+
matchesProjectIdentity,
|
|
32
35
|
matchesProjectScope,
|
|
33
|
-
parseSearchQuery,
|
|
34
36
|
perf,
|
|
35
37
|
realFs,
|
|
36
38
|
refreshPricingCache,
|
|
37
39
|
resolveProviderRoots,
|
|
38
40
|
scanSessions,
|
|
39
|
-
searchFileActivitySessions,
|
|
40
|
-
searchSessions,
|
|
41
41
|
sessionSignature,
|
|
42
42
|
sortSessions,
|
|
43
43
|
startOfLocalDay,
|
|
44
44
|
upsertBookmark
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-BV65IEWZ.js";
|
|
46
46
|
|
|
47
47
|
// src/index.ts
|
|
48
48
|
import { defineCommand, runMain } from "citty";
|
|
49
49
|
|
|
50
50
|
// src/server.ts
|
|
51
51
|
import { Hono as Hono2 } from "hono";
|
|
52
|
+
import { bodyLimit } from "hono/body-limit";
|
|
52
53
|
import { serve } from "@hono/node-server";
|
|
53
54
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
54
55
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -227,7 +228,7 @@ function parseBookmarkPayload(value) {
|
|
|
227
228
|
title: value.title,
|
|
228
229
|
directory: value.directory,
|
|
229
230
|
time_created: value.time_created,
|
|
230
|
-
time_updated: value.time_updated,
|
|
231
|
+
time_updated: value.time_updated ?? void 0,
|
|
231
232
|
stats: value.stats
|
|
232
233
|
};
|
|
233
234
|
}
|
|
@@ -265,13 +266,14 @@ function parseSmartTags(values) {
|
|
|
265
266
|
);
|
|
266
267
|
return tags.length > 0 ? [...new Set(tags)] : void 0;
|
|
267
268
|
}
|
|
268
|
-
function parseSearchOptions(c, defaults) {
|
|
269
|
+
function parseSearchOptions(c, defaults, projectIdentity) {
|
|
269
270
|
const params = searchParams(c);
|
|
270
271
|
const limitValue = parseNumberParam(params.get("limit") ?? void 0);
|
|
271
272
|
return {
|
|
272
273
|
agent: optionalQueryValue(params.get("agent") ?? void 0),
|
|
273
274
|
project: optionalQueryValue(params.get("project") ?? void 0),
|
|
274
|
-
|
|
275
|
+
projectKind: projectIdentity?.kind,
|
|
276
|
+
projectKey: projectIdentity?.key,
|
|
275
277
|
cwd: optionalQueryValue(params.get("cwd") ?? void 0),
|
|
276
278
|
tags: parseSmartTags(queryValues(params, "tag", "tags", "signal")),
|
|
277
279
|
tools: queryValues(params, "tool", "tools").map((tool) => tool.toLowerCase()),
|
|
@@ -310,49 +312,6 @@ function sanitizeClientLogData(value) {
|
|
|
310
312
|
})
|
|
311
313
|
);
|
|
312
314
|
}
|
|
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
315
|
function getProjectGroupKey(identityKind, identityKey) {
|
|
357
316
|
return `${identityKind}:${identityKey}`;
|
|
358
317
|
}
|
|
@@ -408,46 +367,15 @@ function attachProjectMetrics(projects, sessions) {
|
|
|
408
367
|
};
|
|
409
368
|
});
|
|
410
369
|
}
|
|
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
370
|
function handleGetConfig(c, defaults) {
|
|
444
|
-
|
|
371
|
+
const payload = {
|
|
445
372
|
window: {
|
|
446
373
|
from: defaults.from,
|
|
447
374
|
to: defaults.to,
|
|
448
375
|
days: defaults.days
|
|
449
376
|
}
|
|
450
|
-
}
|
|
377
|
+
};
|
|
378
|
+
return c.json(payload);
|
|
451
379
|
}
|
|
452
380
|
function handleGetScanStatus(c, scanSource) {
|
|
453
381
|
return c.json(scanSource.getScanStatus());
|
|
@@ -477,7 +405,13 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
477
405
|
const agent = c.req.query("agent");
|
|
478
406
|
const q = c.req.query("q")?.toLowerCase();
|
|
479
407
|
const cwd = c.req.query("cwd");
|
|
480
|
-
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
|
+
}
|
|
481
415
|
const tag = c.req.query("tag")?.toLowerCase();
|
|
482
416
|
const from = parseDateParam(c.req.query("from"), defaults.from);
|
|
483
417
|
const to = parseDateParam(c.req.query("to"), defaults.to);
|
|
@@ -487,8 +421,10 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
487
421
|
} else {
|
|
488
422
|
sessions = [...scanResult.sessions];
|
|
489
423
|
}
|
|
490
|
-
if (
|
|
491
|
-
sessions = sessions.filter(
|
|
424
|
+
if (projectIdentity) {
|
|
425
|
+
sessions = sessions.filter(
|
|
426
|
+
(session) => matchesProjectIdentity(session.project_identity, projectIdentity)
|
|
427
|
+
);
|
|
492
428
|
} else if (cwd) {
|
|
493
429
|
const projectScope = createProjectScopeMatcher(cwd);
|
|
494
430
|
sessions = sessions.filter((s) => matchesProjectScope(s, projectScope));
|
|
@@ -505,29 +441,15 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
505
441
|
function handleSearchSessions(c, scanSource, defaults = {}) {
|
|
506
442
|
const query = c.req.query("q")?.trim() ?? "";
|
|
507
443
|
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
|
|
444
|
+
const projectIdentity = parseProjectIdentityFilter(
|
|
445
|
+
c.req.query("projectKind"),
|
|
446
|
+
c.req.query("projectKey")
|
|
514
447
|
);
|
|
515
|
-
if (
|
|
516
|
-
return c.json({
|
|
517
|
-
results: recentSearchSessions(
|
|
518
|
-
scanResult,
|
|
519
|
-
mergedSearchOptions
|
|
520
|
-
)
|
|
521
|
-
});
|
|
448
|
+
if (projectIdentity === null) {
|
|
449
|
+
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
522
450
|
}
|
|
523
|
-
const
|
|
524
|
-
const results =
|
|
525
|
-
[
|
|
526
|
-
...fileQuery ? searchFileActivitySessions(fileQuery, mergedSearchOptions) : [],
|
|
527
|
-
...searchSessions(query, mergedSearchOptions)
|
|
528
|
-
],
|
|
529
|
-
mergedSearchOptions.limit ?? 50
|
|
530
|
-
);
|
|
451
|
+
const searchOptions = parseSearchOptions(c, defaults, projectIdentity);
|
|
452
|
+
const results = executeSessionSearch(query, searchOptions, scanResult);
|
|
531
453
|
return c.json({ results });
|
|
532
454
|
}
|
|
533
455
|
function parseFileActivityKind(value) {
|
|
@@ -540,14 +462,29 @@ function optionalQueryValue(value) {
|
|
|
540
462
|
const normalized = value?.trim();
|
|
541
463
|
return normalized ? normalized : void 0;
|
|
542
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
|
+
}
|
|
543
472
|
function handleGetFileActivity(c, defaults = {}) {
|
|
544
473
|
const limitValue = Number(c.req.query("limit"));
|
|
545
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
|
+
}
|
|
546
482
|
return c.json({
|
|
547
483
|
activity: listFileActivity({
|
|
548
484
|
agent: optionalQueryValue(c.req.query("agent")),
|
|
549
485
|
sessionId: optionalQueryValue(c.req.query("sessionId")),
|
|
550
|
-
|
|
486
|
+
projectKind: projectIdentity?.kind,
|
|
487
|
+
projectKey: projectIdentity?.key,
|
|
551
488
|
project: optionalQueryValue(c.req.query("project")),
|
|
552
489
|
cwd: optionalQueryValue(c.req.query("cwd")),
|
|
553
490
|
path: optionalQueryValue(c.req.query("path")),
|
|
@@ -563,6 +500,9 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
563
500
|
const scanResult = scanSource.getSnapshot();
|
|
564
501
|
const agentName = c.req.param("agent");
|
|
565
502
|
const sessionId = c.req.param("id");
|
|
503
|
+
if (!agentName) {
|
|
504
|
+
return c.json({ error: "Missing agent name" }, 400);
|
|
505
|
+
}
|
|
566
506
|
if (!sessionId) {
|
|
567
507
|
return c.json({ error: "Missing session ID" }, 400);
|
|
568
508
|
}
|
|
@@ -712,6 +652,13 @@ function resolveDashboardWindow(defaults, queryDays, queryFrom, queryTo) {
|
|
|
712
652
|
}
|
|
713
653
|
function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
714
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
|
+
}
|
|
715
662
|
const { from, to, days } = resolveDashboardWindow(
|
|
716
663
|
defaults,
|
|
717
664
|
c.req.query("days"),
|
|
@@ -720,8 +667,8 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
720
667
|
);
|
|
721
668
|
const scope = {
|
|
722
669
|
agent: optionalQueryValue(c.req.query("agent"))?.toLowerCase(),
|
|
723
|
-
projectKind:
|
|
724
|
-
projectKey:
|
|
670
|
+
projectKind: projectIdentity?.kind,
|
|
671
|
+
projectKey: projectIdentity?.key
|
|
725
672
|
};
|
|
726
673
|
const agentInfo = getAgentInfoMap({});
|
|
727
674
|
const agentInfoMap = new Map(agentInfo.map((a) => [a.name, a]));
|
|
@@ -736,6 +683,7 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
736
683
|
...aggregate,
|
|
737
684
|
recentFileActivities: listFileActivity({
|
|
738
685
|
agent: scope.agent,
|
|
686
|
+
projectKind: scope.projectKind,
|
|
739
687
|
projectKey: scope.projectKey,
|
|
740
688
|
from,
|
|
741
689
|
to,
|
|
@@ -749,10 +697,14 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
749
697
|
// src/api/routes.ts
|
|
750
698
|
function createSseResponse(store, signal) {
|
|
751
699
|
const encoder = new TextEncoder();
|
|
700
|
+
let cancelStream = () => {
|
|
701
|
+
};
|
|
752
702
|
return new Response(
|
|
753
703
|
new ReadableStream({
|
|
754
704
|
start(controller) {
|
|
705
|
+
let isClosed = false;
|
|
755
706
|
const write = (event, data) => {
|
|
707
|
+
if (isClosed) return;
|
|
756
708
|
controller.enqueue(encoder.encode(`event: ${event}
|
|
757
709
|
`));
|
|
758
710
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
@@ -768,18 +720,28 @@ function createSseResponse(store, signal) {
|
|
|
768
720
|
write(event.type, event);
|
|
769
721
|
});
|
|
770
722
|
const heartbeat = setInterval(() => {
|
|
771
|
-
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
|
723
|
+
if (!isClosed) controller.enqueue(encoder.encode(": keepalive\n\n"));
|
|
772
724
|
}, 15e3);
|
|
773
|
-
const
|
|
725
|
+
const cleanup = () => {
|
|
726
|
+
if (isClosed) return false;
|
|
727
|
+
isClosed = true;
|
|
774
728
|
clearInterval(heartbeat);
|
|
775
729
|
unsubscribeSessions();
|
|
776
730
|
unsubscribeScanStatus();
|
|
777
|
-
|
|
731
|
+
signal.removeEventListener("abort", abortStream);
|
|
732
|
+
return true;
|
|
778
733
|
};
|
|
779
|
-
|
|
734
|
+
const abortStream = () => {
|
|
735
|
+
if (cleanup()) controller.close();
|
|
736
|
+
};
|
|
737
|
+
cancelStream = () => {
|
|
738
|
+
cleanup();
|
|
739
|
+
};
|
|
740
|
+
if (signal.aborted) abortStream();
|
|
741
|
+
else signal.addEventListener("abort", abortStream, { once: true });
|
|
780
742
|
},
|
|
781
743
|
cancel() {
|
|
782
|
-
|
|
744
|
+
cancelStream();
|
|
783
745
|
}
|
|
784
746
|
}),
|
|
785
747
|
{
|
|
@@ -820,7 +782,46 @@ function createApiRoutes(scanSource, store, options = {}) {
|
|
|
820
782
|
return api;
|
|
821
783
|
}
|
|
822
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
|
+
|
|
823
823
|
// src/server.ts
|
|
824
|
+
var MAX_API_REQUEST_BYTES = 1024 * 1024;
|
|
824
825
|
function findWebDistPath() {
|
|
825
826
|
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
826
827
|
const packagedPath = resolve(__dirname2, "web");
|
|
@@ -862,6 +863,14 @@ function getListeningPort(server, fallback) {
|
|
|
862
863
|
}
|
|
863
864
|
async function createServer(port, store, options = {}) {
|
|
864
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
|
+
}
|
|
865
874
|
app.use("*", async (c, next) => {
|
|
866
875
|
const startedAt = performance.now();
|
|
867
876
|
let thrown;
|
|
@@ -882,6 +891,16 @@ async function createServer(port, store, options = {}) {
|
|
|
882
891
|
});
|
|
883
892
|
}
|
|
884
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
|
+
);
|
|
885
904
|
const routeOptions = {
|
|
886
905
|
defaultSessionFrom: options.defaultSessionFrom,
|
|
887
906
|
defaultSessionTo: options.defaultSessionTo,
|
|
@@ -901,7 +920,6 @@ async function createServer(port, store, options = {}) {
|
|
|
901
920
|
app.get("/*", serveStatic({ root: webDistPath, path: "index.html" }));
|
|
902
921
|
}
|
|
903
922
|
const attempts = Math.max(1, options.portFallbackAttempts ?? 1);
|
|
904
|
-
const hostname = options.hostname ?? "127.0.0.1";
|
|
905
923
|
let server = null;
|
|
906
924
|
let actualPort = port;
|
|
907
925
|
for (let offset = 0; offset < attempts; offset += 1) {
|
|
@@ -928,16 +946,19 @@ async function createServer(port, store, options = {}) {
|
|
|
928
946
|
throw new Error(getServerStartupErrorMessage(error, candidatePort));
|
|
929
947
|
}
|
|
930
948
|
}
|
|
931
|
-
const
|
|
932
|
-
const url =
|
|
933
|
-
appLogger.info("server.listen", {
|
|
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
|
+
});
|
|
934
957
|
if (!isLoopback) {
|
|
935
|
-
appLogger.warn("server.listen.
|
|
936
|
-
console.warn(
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
`
|
|
940
|
-
);
|
|
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
|
+
`);
|
|
941
962
|
}
|
|
942
963
|
return {
|
|
943
964
|
url,
|
|
@@ -958,12 +979,661 @@ async function createServer(port, store, options = {}) {
|
|
|
958
979
|
}
|
|
959
980
|
|
|
960
981
|
// src/live-scan.ts
|
|
961
|
-
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";
|
|
962
988
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
963
989
|
import { Worker } from "worker_threads";
|
|
964
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
|
+
|
|
965
1635
|
// src/session-watcher.ts
|
|
966
|
-
import { existsSync as
|
|
1636
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, watch } from "fs";
|
|
967
1637
|
import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
|
|
968
1638
|
var WRITE_STABILITY_THRESHOLD_MS = 250;
|
|
969
1639
|
var WRITE_STABILITY_POLL_MS = 100;
|
|
@@ -971,11 +1641,11 @@ function toAbsolutePath(path) {
|
|
|
971
1641
|
return isAbsolute(path) ? path : resolve2(path);
|
|
972
1642
|
}
|
|
973
1643
|
function closestWatchablePath(targetPath) {
|
|
974
|
-
if (!isAbsolute(targetPath) && !
|
|
1644
|
+
if (!isAbsolute(targetPath) && !existsSync4(targetPath)) {
|
|
975
1645
|
return null;
|
|
976
1646
|
}
|
|
977
1647
|
let current = toAbsolutePath(targetPath);
|
|
978
|
-
while (!
|
|
1648
|
+
while (!existsSync4(current)) {
|
|
979
1649
|
const parent = dirname2(current);
|
|
980
1650
|
if (parent === current) {
|
|
981
1651
|
return null;
|
|
@@ -1283,9 +1953,6 @@ var SessionWatcher = class {
|
|
|
1283
1953
|
// src/live-scan.ts
|
|
1284
1954
|
var REFRESH_DEBOUNCE_MS = 200;
|
|
1285
1955
|
var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
|
|
1286
|
-
var PENDING_REFRESH_DELAY_MS = 100;
|
|
1287
|
-
var MAX_ADAPTIVE_REFRESH_DELAY_MS = 3e4;
|
|
1288
|
-
var ADAPTIVE_REFRESH_DELAY_MULTIPLIER = 4;
|
|
1289
1956
|
var NEW_SESSION_EVENT_WINDOW_MS = 250;
|
|
1290
1957
|
var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
|
|
1291
1958
|
var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -1364,45 +2031,17 @@ var LiveScanStore = class {
|
|
|
1364
2031
|
sessions = [];
|
|
1365
2032
|
listeners = /* @__PURE__ */ new Set();
|
|
1366
2033
|
scanStatusListeners = /* @__PURE__ */ new Set();
|
|
1367
|
-
scanStatus =
|
|
1368
|
-
|
|
1369
|
-
|
|
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();
|
|
2034
|
+
scanStatus = new ScanStatusModel();
|
|
2035
|
+
backfills = new BackfillCoordinator();
|
|
2036
|
+
refreshes = new RefreshCoordinator();
|
|
1381
2037
|
watcher = null;
|
|
1382
2038
|
pendingEvent = null;
|
|
1383
2039
|
pendingEventTimer = null;
|
|
1384
2040
|
backgroundRefreshTimer = null;
|
|
1385
|
-
|
|
1386
|
-
|
|
2041
|
+
scanRefreshWorkers = /* @__PURE__ */ new Set();
|
|
2042
|
+
searchIndexJobs = new SearchIndexJobRunner();
|
|
2043
|
+
shutdownPromise = null;
|
|
1387
2044
|
shuttingDown = false;
|
|
1388
|
-
/** Set once a search-index worker in this process has completed the FTS integrity check. */
|
|
1389
|
-
ftsIntegrityChecked = false;
|
|
1390
|
-
getRefreshState(agentName) {
|
|
1391
|
-
let state = this.refreshStates.get(agentName);
|
|
1392
|
-
if (!state) {
|
|
1393
|
-
state = {
|
|
1394
|
-
timer: null,
|
|
1395
|
-
timerDeadline: 0,
|
|
1396
|
-
inFlight: false,
|
|
1397
|
-
pendingRerun: false,
|
|
1398
|
-
lastRefreshAt: 0,
|
|
1399
|
-
lastRefreshDurationMs: 0,
|
|
1400
|
-
pendingPathCount: 0
|
|
1401
|
-
};
|
|
1402
|
-
this.refreshStates.set(agentName, state);
|
|
1403
|
-
}
|
|
1404
|
-
return state;
|
|
1405
|
-
}
|
|
1406
2045
|
async initialize() {
|
|
1407
2046
|
const startedAt = performance.now();
|
|
1408
2047
|
const deferInitialRefresh = this.storeOptions.deferInitialRefresh === true;
|
|
@@ -1427,7 +2066,7 @@ var LiveScanStore = class {
|
|
|
1427
2066
|
this.applyScanResult(initialResult);
|
|
1428
2067
|
const indexStartedAt = performance.now();
|
|
1429
2068
|
if (!deferInitialRefresh) {
|
|
1430
|
-
await this.
|
|
2069
|
+
await this.searchIndexJobs.enqueue(
|
|
1431
2070
|
"scan.initial",
|
|
1432
2071
|
this.buildFullSearchIndexJobs("scan.initial")
|
|
1433
2072
|
);
|
|
@@ -1459,7 +2098,7 @@ var LiveScanStore = class {
|
|
|
1459
2098
|
this.watcher = new SessionWatcher();
|
|
1460
2099
|
this.watcher.onAgentsChanged((agentNames) => {
|
|
1461
2100
|
for (const agentName of agentNames) {
|
|
1462
|
-
this.
|
|
2101
|
+
this.refreshes.recordChangedPaths(agentName);
|
|
1463
2102
|
const delayMs = (this.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
|
|
1464
2103
|
this.scheduleRefresh(agentName, delayMs);
|
|
1465
2104
|
}
|
|
@@ -1496,24 +2135,7 @@ var LiveScanStore = class {
|
|
|
1496
2135
|
};
|
|
1497
2136
|
}
|
|
1498
2137
|
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
|
-
};
|
|
2138
|
+
return this.scanStatus.snapshot();
|
|
1517
2139
|
}
|
|
1518
2140
|
subscribe(listener) {
|
|
1519
2141
|
this.listeners.add(listener);
|
|
@@ -1527,15 +2149,26 @@ var LiveScanStore = class {
|
|
|
1527
2149
|
this.scanStatusListeners.delete(listener);
|
|
1528
2150
|
};
|
|
1529
2151
|
}
|
|
1530
|
-
|
|
2152
|
+
shutdown() {
|
|
2153
|
+
this.shutdownPromise ??= this.performShutdown();
|
|
2154
|
+
return this.shutdownPromise;
|
|
2155
|
+
}
|
|
2156
|
+
async performShutdown() {
|
|
1531
2157
|
this.shuttingDown = true;
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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);
|
|
1538
2166
|
}
|
|
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
|
+
});
|
|
1539
2172
|
if (this.pendingEventTimer) {
|
|
1540
2173
|
clearTimeout(this.pendingEventTimer);
|
|
1541
2174
|
this.pendingEventTimer = null;
|
|
@@ -1544,21 +2177,24 @@ var LiveScanStore = class {
|
|
|
1544
2177
|
clearTimeout(this.backgroundRefreshTimer);
|
|
1545
2178
|
this.backgroundRefreshTimer = null;
|
|
1546
2179
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
batch.reject(new Error("Live scan store shut down"));
|
|
1553
|
-
}
|
|
1554
|
-
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();
|
|
1555
2185
|
this.pendingEvent = null;
|
|
1556
2186
|
if (this.watcher) {
|
|
1557
2187
|
await this.watcher.dispose();
|
|
1558
2188
|
this.watcher = null;
|
|
1559
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
|
+
});
|
|
1560
2195
|
}
|
|
1561
2196
|
emit(event) {
|
|
2197
|
+
if (this.shuttingDown) return;
|
|
1562
2198
|
if (this.pendingEvent || event.newSessions > 0) {
|
|
1563
2199
|
this.queueEvent(event);
|
|
1564
2200
|
return;
|
|
@@ -1570,164 +2206,36 @@ var LiveScanStore = class {
|
|
|
1570
2206
|
listener(event);
|
|
1571
2207
|
}
|
|
1572
2208
|
}
|
|
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
2209
|
startScanBatch(agentNames, phase) {
|
|
1584
|
-
const
|
|
1585
|
-
|
|
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
|
-
])
|
|
2210
|
+
const sessionCounts = Object.fromEntries(
|
|
2211
|
+
agentNames.map((agentName) => [agentName, this.byAgent[agentName]?.length ?? 0])
|
|
1597
2212
|
);
|
|
1598
|
-
this.
|
|
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
|
-
});
|
|
2213
|
+
this.publishScanStatus(this.scanStatus.startBatch(agentNames, phase, sessionCounts));
|
|
1611
2214
|
}
|
|
1612
2215
|
setScanPhase(phase) {
|
|
1613
|
-
|
|
1614
|
-
this.updateScanStatus({
|
|
1615
|
-
...this.scanStatus,
|
|
1616
|
-
phase,
|
|
1617
|
-
updatedAt: Date.now()
|
|
1618
|
-
});
|
|
2216
|
+
this.publishScanStatus(this.scanStatus.setPhase(phase));
|
|
1619
2217
|
}
|
|
1620
2218
|
beginAgentScan(agentName) {
|
|
1621
|
-
if (!this.scanStatus.active)
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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
|
-
});
|
|
2219
|
+
if (!this.scanStatus.snapshot().active) this.startScanBatch([agentName], "scanning");
|
|
2220
|
+
this.publishScanStatus(
|
|
2221
|
+
this.scanStatus.beginAgent(agentName, this.byAgent[agentName]?.length ?? 0)
|
|
2222
|
+
);
|
|
1655
2223
|
}
|
|
1656
2224
|
updateAgentScanProgress(agentName, progress) {
|
|
1657
|
-
|
|
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
|
-
});
|
|
2225
|
+
this.publishScanStatus(this.scanStatus.updateAgent(agentName, progress));
|
|
1673
2226
|
}
|
|
1674
2227
|
finishAgentScan(agentName) {
|
|
1675
|
-
|
|
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
|
-
});
|
|
2228
|
+
this.publishScanStatus(this.scanStatus.finishAgent(agentName, this.byAgent[agentName]?.length));
|
|
1706
2229
|
}
|
|
1707
2230
|
finishScanBatch() {
|
|
1708
|
-
|
|
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
|
-
});
|
|
2231
|
+
this.publishScanStatus(this.scanStatus.finishBatch());
|
|
1724
2232
|
}
|
|
1725
2233
|
updateBackfillStatus(patch) {
|
|
1726
|
-
this.
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
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);
|
|
1731
2239
|
}
|
|
1732
2240
|
/**
|
|
1733
2241
|
* Only FileSystemSessionSource agents pay the O(history) enumeration cost this
|
|
@@ -1743,35 +2251,32 @@ var LiveScanStore = class {
|
|
|
1743
2251
|
return lastSyncAt == null || Date.now() - lastSyncAt > BACKFILL_INTERVAL_MS;
|
|
1744
2252
|
}
|
|
1745
2253
|
enqueueBackfill(agentName) {
|
|
1746
|
-
if (this.
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
this.
|
|
1750
|
-
this.updateBackfillStatus({ active: true, pendingAgents: [...this.backfillQueue] });
|
|
2254
|
+
if (this.shuttingDown) return;
|
|
2255
|
+
const status = this.backfills.enqueue(agentName);
|
|
2256
|
+
if (!status) return;
|
|
2257
|
+
this.updateBackfillStatus(status);
|
|
1751
2258
|
this.pumpBackfillQueue();
|
|
1752
2259
|
}
|
|
1753
2260
|
pumpBackfillQueue() {
|
|
1754
|
-
if (this.
|
|
1755
|
-
const
|
|
1756
|
-
if (!
|
|
1757
|
-
this.
|
|
1758
|
-
this.
|
|
1759
|
-
|
|
1760
|
-
this.
|
|
1761
|
-
this.updateBackfillStatus({
|
|
1762
|
-
currentAgent: void 0,
|
|
1763
|
-
completedAgents: [.../* @__PURE__ */ new Set([...this.scanStatus.backfill.completedAgents, agentName])],
|
|
1764
|
-
active: this.backfillQueue.length > 0
|
|
1765
|
-
});
|
|
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));
|
|
1766
2268
|
this.pumpBackfillQueue();
|
|
1767
2269
|
});
|
|
1768
2270
|
}
|
|
1769
2271
|
/** Unbounded per-source sync to reconcile the full session history, not just the display window. */
|
|
1770
2272
|
async runBackfill(agentName) {
|
|
2273
|
+
await this.refreshes.serialize(agentName, "backfill", () => this.performBackfill(agentName));
|
|
2274
|
+
}
|
|
2275
|
+
async performBackfill(agentName) {
|
|
1771
2276
|
const startedAt = performance.now();
|
|
1772
2277
|
const agent = this.agents.find((item) => item.name === agentName);
|
|
1773
2278
|
if (!agent || !(agent instanceof FileSystemSessionSource) || !agent.isAvailable()) {
|
|
1774
|
-
return;
|
|
2279
|
+
return "skipped";
|
|
1775
2280
|
}
|
|
1776
2281
|
const cached = loadCachedSessions(agentName);
|
|
1777
2282
|
const baseline = cached?.sessions ?? this.byAgent[agentName] ?? [];
|
|
@@ -1798,7 +2303,7 @@ var LiveScanStore = class {
|
|
|
1798
2303
|
);
|
|
1799
2304
|
this.byAgent[agentName] = sortSessions(filtered);
|
|
1800
2305
|
this.rebuildSessions();
|
|
1801
|
-
await this.
|
|
2306
|
+
await this.searchIndexJobs.enqueue("scan.backfill", [
|
|
1802
2307
|
{
|
|
1803
2308
|
kind: "full",
|
|
1804
2309
|
context: "scan.backfill",
|
|
@@ -1819,9 +2324,11 @@ var LiveScanStore = class {
|
|
|
1819
2324
|
sessions: fullSessions.length,
|
|
1820
2325
|
changed: result.changedIds?.length ?? 0
|
|
1821
2326
|
});
|
|
2327
|
+
return "committed";
|
|
1822
2328
|
} catch (error) {
|
|
1823
2329
|
appLogger.error("scan.backfill.error", { agent: agentName, error });
|
|
1824
2330
|
console.error(`[${agentName}] Backfill failed:`, error);
|
|
2331
|
+
return "failed";
|
|
1825
2332
|
}
|
|
1826
2333
|
}
|
|
1827
2334
|
queueEvent(event) {
|
|
@@ -1841,16 +2348,9 @@ var LiveScanStore = class {
|
|
|
1841
2348
|
rebuildSessions() {
|
|
1842
2349
|
this.sessions = sortSessions(Object.values(this.byAgent).flat());
|
|
1843
2350
|
}
|
|
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
2351
|
getSmartTagWorkerUrl() {
|
|
1852
2352
|
const workerUrl = new URL("./smart-tag-worker.js", import.meta.url);
|
|
1853
|
-
if (workerUrl.protocol === "file:" && !
|
|
2353
|
+
if (workerUrl.protocol === "file:" && !existsSync5(fileURLToPath3(workerUrl))) {
|
|
1854
2354
|
return null;
|
|
1855
2355
|
}
|
|
1856
2356
|
return workerUrl;
|
|
@@ -1861,7 +2361,7 @@ var LiveScanStore = class {
|
|
|
1861
2361
|
scanAgentInWorker(agent, previousSessions, changedIds, scanOptions, workerOptions = {}) {
|
|
1862
2362
|
const workerUrl = this.getScanRefreshWorkerUrl();
|
|
1863
2363
|
return new Promise((resolve4, reject) => {
|
|
1864
|
-
const worker = new
|
|
2364
|
+
const worker = new Worker2(workerUrl, {
|
|
1865
2365
|
workerData: {
|
|
1866
2366
|
agentName: agent.name,
|
|
1867
2367
|
previousSessions,
|
|
@@ -1872,11 +2372,13 @@ var LiveScanStore = class {
|
|
|
1872
2372
|
}
|
|
1873
2373
|
});
|
|
1874
2374
|
worker.unref();
|
|
2375
|
+
this.scanRefreshWorkers.add(worker);
|
|
1875
2376
|
let settled = false;
|
|
1876
|
-
const finish = (callback) => {
|
|
2377
|
+
const finish = (callback, terminate = true) => {
|
|
1877
2378
|
if (settled) return;
|
|
1878
2379
|
settled = true;
|
|
1879
|
-
|
|
2380
|
+
this.scanRefreshWorkers.delete(worker);
|
|
2381
|
+
if (terminate) void worker.terminate();
|
|
1880
2382
|
callback();
|
|
1881
2383
|
};
|
|
1882
2384
|
worker.on("message", (message) => {
|
|
@@ -1900,8 +2402,15 @@ var LiveScanStore = class {
|
|
|
1900
2402
|
finish(() => reject(error));
|
|
1901
2403
|
});
|
|
1902
2404
|
worker.once("exit", (code) => {
|
|
1903
|
-
if (!settled
|
|
1904
|
-
|
|
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
|
+
);
|
|
1905
2414
|
}
|
|
1906
2415
|
});
|
|
1907
2416
|
});
|
|
@@ -1927,77 +2436,6 @@ var LiveScanStore = class {
|
|
|
1927
2436
|
};
|
|
1928
2437
|
});
|
|
1929
2438
|
}
|
|
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
2439
|
applyScanResult(result) {
|
|
2002
2440
|
const knownAgents = createRegisteredAgents();
|
|
2003
2441
|
const agentMap = /* @__PURE__ */ new Map();
|
|
@@ -2019,7 +2457,10 @@ var LiveScanStore = class {
|
|
|
2019
2457
|
this.byAgent = {};
|
|
2020
2458
|
for (const agent of this.agents) {
|
|
2021
2459
|
this.byAgent[agent.name] = sortSessions(result.byAgent[agent.name] ?? []);
|
|
2022
|
-
this.
|
|
2460
|
+
this.refreshes.setLastRefreshAt(
|
|
2461
|
+
agent.name,
|
|
2462
|
+
result.cacheTimestamps?.[agent.name] ?? Date.now()
|
|
2463
|
+
);
|
|
2023
2464
|
}
|
|
2024
2465
|
this.rebuildSessions();
|
|
2025
2466
|
}
|
|
@@ -2036,7 +2477,7 @@ var LiveScanStore = class {
|
|
|
2036
2477
|
const startedAt = performance.now();
|
|
2037
2478
|
const context = "scan.initial.background";
|
|
2038
2479
|
try {
|
|
2039
|
-
await this.
|
|
2480
|
+
await this.searchIndexJobs.enqueue(context, this.buildFullSearchIndexJobs(context));
|
|
2040
2481
|
appLogger.info(`${context}.complete`, {
|
|
2041
2482
|
duration_ms: Math.round(performance.now() - startedAt),
|
|
2042
2483
|
sessions: this.sessions.length
|
|
@@ -2056,63 +2497,34 @@ var LiveScanStore = class {
|
|
|
2056
2497
|
* backoff elapses — push the deadline out forever and starve the refresh.
|
|
2057
2498
|
*/
|
|
2058
2499
|
scheduleRefresh(agentName, delayMs = REFRESH_DEBOUNCE_MS) {
|
|
2059
|
-
|
|
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);
|
|
2500
|
+
this.refreshes.schedule(agentName, delayMs, () => this.refreshAgent(agentName));
|
|
2078
2501
|
}
|
|
2079
2502
|
async refreshAgent(agentName) {
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
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);
|
|
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);
|
|
2099
2513
|
}
|
|
2100
|
-
}
|
|
2514
|
+
});
|
|
2101
2515
|
}
|
|
2102
2516
|
async runRefresh(agentName) {
|
|
2103
2517
|
const startedAt = performance.now();
|
|
2104
|
-
const
|
|
2105
|
-
const pendingPathCount = state.pendingPathCount;
|
|
2106
|
-
state.pendingPathCount = 0;
|
|
2518
|
+
const pendingPathCount = this.refreshes.takePendingPathCount(agentName);
|
|
2107
2519
|
const agent = this.agents.find((item) => item.name === agentName);
|
|
2108
2520
|
if (!agent) {
|
|
2109
2521
|
appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
|
|
2110
|
-
return;
|
|
2522
|
+
return "skipped";
|
|
2111
2523
|
}
|
|
2112
2524
|
const previousSessions = this.byAgent[agentName] ?? [];
|
|
2113
2525
|
const cached = loadCachedSessions(agentName);
|
|
2114
2526
|
const refreshBaseline = cached?.sessions ?? previousSessions;
|
|
2115
|
-
const cacheTimestamp = cached?.timestamp ??
|
|
2527
|
+
const cacheTimestamp = cached?.timestamp ?? this.refreshes.lastRefreshAt(agentName);
|
|
2116
2528
|
if (cached) {
|
|
2117
2529
|
restoreAgentCacheMeta(agent, cached.meta);
|
|
2118
2530
|
}
|
|
@@ -2135,7 +2547,7 @@ var LiveScanStore = class {
|
|
|
2135
2547
|
availabilityDuration = performance.now() - availabilityStartedAt;
|
|
2136
2548
|
if (!isAvailable) {
|
|
2137
2549
|
nextSessions = [];
|
|
2138
|
-
|
|
2550
|
+
this.refreshes.setLastRefreshAt(agentName, Date.now());
|
|
2139
2551
|
} else if (!isInitialized) {
|
|
2140
2552
|
this.setScanPhase("initializing");
|
|
2141
2553
|
const scanStartedAt = performance.now();
|
|
@@ -2150,7 +2562,7 @@ var LiveScanStore = class {
|
|
|
2150
2562
|
fullScanSessions = attachMissingProjectIdentities(nextSessions);
|
|
2151
2563
|
nextSessions = fullScanSessions;
|
|
2152
2564
|
scanDuration = performance.now() - scanStartedAt;
|
|
2153
|
-
|
|
2565
|
+
this.refreshes.setLastRefreshAt(agentName, Date.now());
|
|
2154
2566
|
} else if (cached && agent instanceof FileSystemSessionSource) {
|
|
2155
2567
|
const scanStartedAt = performance.now();
|
|
2156
2568
|
const result = await this.scanAgentInWorker(
|
|
@@ -2174,7 +2586,7 @@ var LiveScanStore = class {
|
|
|
2174
2586
|
preciseChangedIds
|
|
2175
2587
|
);
|
|
2176
2588
|
scanDuration = performance.now() - scanStartedAt;
|
|
2177
|
-
|
|
2589
|
+
this.refreshes.setLastRefreshAt(agentName, Date.now());
|
|
2178
2590
|
if (preciseChangedIds.length === 0) {
|
|
2179
2591
|
appLogger.debug("scan.refresh.unchanged", {
|
|
2180
2592
|
agent: agentName,
|
|
@@ -2187,13 +2599,13 @@ var LiveScanStore = class {
|
|
|
2187
2599
|
agent.checkForChanges(cacheTimestamp, refreshBaseline)
|
|
2188
2600
|
);
|
|
2189
2601
|
checkDuration = performance.now() - checkStartedAt;
|
|
2190
|
-
|
|
2602
|
+
this.refreshes.setLastRefreshAt(agentName, checkResult.timestamp);
|
|
2191
2603
|
if (!checkResult.hasChanges) {
|
|
2192
2604
|
appLogger.debug("scan.refresh.unchanged", {
|
|
2193
2605
|
agent: agentName,
|
|
2194
2606
|
duration_ms: Math.round(performance.now() - startedAt)
|
|
2195
2607
|
});
|
|
2196
|
-
return;
|
|
2608
|
+
return "unchanged";
|
|
2197
2609
|
}
|
|
2198
2610
|
preciseChangedIds = checkResult.changedIds ?? null;
|
|
2199
2611
|
usedIncrementalScan = Array.isArray(checkResult.changedIds);
|
|
@@ -2218,7 +2630,7 @@ var LiveScanStore = class {
|
|
|
2218
2630
|
fullScanSessions = attachMissingProjectIdentities(nextSessions);
|
|
2219
2631
|
nextSessions = fullScanSessions;
|
|
2220
2632
|
scanDuration = performance.now() - scanStartedAt;
|
|
2221
|
-
|
|
2633
|
+
this.refreshes.setLastRefreshAt(agentName, Date.now());
|
|
2222
2634
|
}
|
|
2223
2635
|
nextSessions = attachMissingProjectIdentities(nextSessions);
|
|
2224
2636
|
const filterStartedAt = performance.now();
|
|
@@ -2258,7 +2670,7 @@ var LiveScanStore = class {
|
|
|
2258
2670
|
} : null;
|
|
2259
2671
|
if (persistentJob) {
|
|
2260
2672
|
persistentJobKind = persistentJob.kind;
|
|
2261
|
-
const persist = this.
|
|
2673
|
+
const persist = this.searchIndexJobs.enqueue("scan.refresh", [persistentJob]);
|
|
2262
2674
|
if (!isInitialized && persistentJob.kind === "full") {
|
|
2263
2675
|
await persist;
|
|
2264
2676
|
} else {
|
|
@@ -2280,7 +2692,7 @@ var LiveScanStore = class {
|
|
|
2280
2692
|
this.emit(event);
|
|
2281
2693
|
}
|
|
2282
2694
|
const totalDurationMs = performance.now() - startedAt;
|
|
2283
|
-
|
|
2695
|
+
this.refreshes.setLastRefreshDuration(agentName, totalDurationMs);
|
|
2284
2696
|
appLogger.info("scan.refresh.done", {
|
|
2285
2697
|
agent: agentName,
|
|
2286
2698
|
duration_ms: Math.round(totalDurationMs),
|
|
@@ -2299,6 +2711,7 @@ var LiveScanStore = class {
|
|
|
2299
2711
|
persistent_index_worker_job: persistentJobKind,
|
|
2300
2712
|
persistent_index_skipped: !persistentJob || void 0
|
|
2301
2713
|
});
|
|
2714
|
+
return "committed";
|
|
2302
2715
|
}
|
|
2303
2716
|
};
|
|
2304
2717
|
|
|
@@ -2308,8 +2721,8 @@ import { consola } from "consola";
|
|
|
2308
2721
|
// src/version.ts
|
|
2309
2722
|
import { readFileSync } from "fs";
|
|
2310
2723
|
import { resolve as resolve3, dirname as dirname3 } from "path";
|
|
2311
|
-
import { fileURLToPath as
|
|
2312
|
-
var __dirname = dirname3(
|
|
2724
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2725
|
+
var __dirname = dirname3(fileURLToPath4(import.meta.url));
|
|
2313
2726
|
var pkg = JSON.parse(readFileSync(resolve3(__dirname, "../package.json"), "utf-8"));
|
|
2314
2727
|
var VERSION = pkg.version;
|
|
2315
2728
|
|
|
@@ -2359,6 +2772,18 @@ function parseSessionUri(uri) {
|
|
|
2359
2772
|
if (!match) return null;
|
|
2360
2773
|
return { agent: match[1], sessionId: match[2] };
|
|
2361
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
|
+
}
|
|
2362
2787
|
var main = defineCommand({
|
|
2363
2788
|
meta: {
|
|
2364
2789
|
name: "codesesh",
|
|
@@ -2377,6 +2802,11 @@ var main = defineCommand({
|
|
|
2377
2802
|
description: "HTTP server bind address (default 127.0.0.1, local access only)",
|
|
2378
2803
|
default: "127.0.0.1"
|
|
2379
2804
|
},
|
|
2805
|
+
"remote-access": {
|
|
2806
|
+
type: "boolean",
|
|
2807
|
+
description: "Allow authenticated access when binding to a non-loopback address",
|
|
2808
|
+
default: false
|
|
2809
|
+
},
|
|
2380
2810
|
agent: {
|
|
2381
2811
|
type: "string",
|
|
2382
2812
|
alias: "a",
|
|
@@ -2441,6 +2871,14 @@ var main = defineCommand({
|
|
|
2441
2871
|
const trace = args.trace;
|
|
2442
2872
|
const useCache = args.cache;
|
|
2443
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
|
+
}
|
|
2444
2882
|
if (trace) {
|
|
2445
2883
|
perf.enable();
|
|
2446
2884
|
}
|
|
@@ -2454,7 +2892,7 @@ var main = defineCommand({
|
|
|
2454
2892
|
log_path: appLogger.getLogPath()
|
|
2455
2893
|
});
|
|
2456
2894
|
if (clearCache) {
|
|
2457
|
-
const { clearCache: clear } = await import("./dist-
|
|
2895
|
+
const { clearCache: clear } = await import("./dist-VD54GDPO.js");
|
|
2458
2896
|
clear();
|
|
2459
2897
|
appLogger.info("cache.clear");
|
|
2460
2898
|
console.log("Cache cleared.");
|
|
@@ -2544,7 +2982,8 @@ var main = defineCommand({
|
|
|
2544
2982
|
defaultSessionTo: listDefaultTo,
|
|
2545
2983
|
defaultSessionDays: listDefaultDays,
|
|
2546
2984
|
portFallbackAttempts: explicitPort ? 1 : DEFAULT_PORT_FALLBACK_ATTEMPTS,
|
|
2547
|
-
hostname
|
|
2985
|
+
hostname,
|
|
2986
|
+
remoteAccess
|
|
2548
2987
|
});
|
|
2549
2988
|
} catch (error) {
|
|
2550
2989
|
console.error(getServerStartupErrorMessage(error, port));
|
|
@@ -2571,14 +3010,14 @@ var main = defineCommand({
|
|
|
2571
3010
|
console.log(` ${url}`);
|
|
2572
3011
|
console.log("");
|
|
2573
3012
|
appLogger.info("cli.ready", {
|
|
2574
|
-
url,
|
|
3013
|
+
url: redactStartupUrl(url),
|
|
2575
3014
|
duration_ms: Math.round(performance.now() - startedAt),
|
|
2576
3015
|
log_path: appLogger.getLogPath()
|
|
2577
3016
|
});
|
|
2578
3017
|
if (!noOpen) {
|
|
2579
3018
|
const open = (await import("open")).default;
|
|
2580
|
-
const targetUrl = targetSession ?
|
|
2581
|
-
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) });
|
|
2582
3021
|
await open(targetUrl);
|
|
2583
3022
|
}
|
|
2584
3023
|
}
|