codesesh 0.12.0 → 0.14.0

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