codesesh 1.0.2 → 1.0.3

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.
Files changed (39) hide show
  1. package/dist/{chunk-VAUC2W7I.js → chunk-PSHZZITB.js} +1919 -731
  2. package/dist/chunk-PSHZZITB.js.map +1 -0
  3. package/dist/chunk-RF3EEWPH.js +17 -0
  4. package/dist/chunk-RF3EEWPH.js.map +1 -0
  5. package/dist/{chunk-PROMURPM.js → chunk-WAKW5OPS.js} +2 -2
  6. package/dist/{dist-6QTGTYZT.js → dist-RHDY2EZX.js} +18 -20
  7. package/dist/index.js +1291 -672
  8. package/dist/index.js.map +1 -1
  9. package/dist/project-identity-worker.js +27 -0
  10. package/dist/project-identity-worker.js.map +1 -0
  11. package/dist/scan-refresh-worker.js +15 -6
  12. package/dist/scan-refresh-worker.js.map +1 -1
  13. package/dist/search-index-worker.js +16 -6
  14. package/dist/search-index-worker.js.map +1 -1
  15. package/dist/smart-tag-worker.js +2 -2
  16. package/dist/web/assets/{ErrorBoundary-D11tWzfn.js → ErrorBoundary-DIwwrdlw.js} +1 -1
  17. package/dist/web/assets/InteractiveReceipt-DmNBDVms.js +1 -0
  18. package/dist/web/assets/{OverviewScreen-DriPmCam.js → OverviewScreen-B5JekBLC.js} +1 -1
  19. package/dist/web/assets/Projects-Bj6ma2k2.js +1 -0
  20. package/dist/web/assets/{SearchFilterBar-Bx73B5e9.js → SearchFilterBar-B9VrUbym.js} +1 -1
  21. package/dist/web/assets/{SearchResultsPanel-A6_3TPhW.js → SearchResultsPanel-C1cuuOKq.js} +1 -1
  22. package/dist/web/assets/{SessionDetail-BnpvDwtH.js → SessionDetail-Dc2CQ3hH.js} +5 -5
  23. package/dist/web/assets/contract-DXvltqJG.js +1 -0
  24. package/dist/web/assets/index-3UfOiTBO.css +1 -0
  25. package/dist/web/assets/index-DBMiEd61.js +1410 -0
  26. package/dist/web/assets/panel-CLnL4kMN.js +1 -0
  27. package/dist/web/assets/{session-indexes-0oT5HSS5.js → session-indexes-BPhf1X2P.js} +2 -2
  28. package/dist/web/assets/{utils-DgUgu15E.js → utils-BKqufGdv.js} +1 -1
  29. package/dist/web/index.html +8 -8
  30. package/package.json +1 -1
  31. package/dist/chunk-VAUC2W7I.js.map +0 -1
  32. package/dist/web/assets/InteractiveReceipt-DYGyHeZd.js +0 -1
  33. package/dist/web/assets/Projects-BPDIcUBK.js +0 -1
  34. package/dist/web/assets/contract-V6QuD0UY.js +0 -1
  35. package/dist/web/assets/index-99ZxpV2y.js +0 -1410
  36. package/dist/web/assets/index-B2RqUgZ5.css +0 -1
  37. package/dist/web/assets/panel-C-gc79Gz.js +0 -1
  38. /package/dist/{chunk-PROMURPM.js.map → chunk-WAKW5OPS.js.map} +0 -0
  39. /package/dist/{dist-6QTGTYZT.js.map → dist-RHDY2EZX.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ AGENT_UNAVAILABLE_DURING_SCAN_ERROR_CODE,
4
+ AgentUnavailableDuringScanError
5
+ } from "./chunk-RF3EEWPH.js";
2
6
  import {
3
7
  appLogger,
4
8
  logSearchIndexSync
5
- } from "./chunk-PROMURPM.js";
9
+ } from "./chunk-WAKW5OPS.js";
6
10
  import {
7
11
  FileSystemSessionSource,
12
+ PROJECT_IDENTITY_RESOLVER_REVISION,
8
13
  SessionAliasValidationError,
9
14
  StateStorageUnavailableError,
10
15
  addCalendarDays,
@@ -16,7 +21,7 @@ import {
16
21
  closeCacheStorage,
17
22
  computeSessionDiff,
18
23
  countCalendarDays,
19
- createProjectScopeMatcher,
24
+ createProjectScopeMatcherFromIdentity,
20
25
  createRegisteredAgents,
21
26
  createSessionIndex,
22
27
  createSessionProjectionContext,
@@ -28,16 +33,17 @@ import {
28
33
  formatSessionReference,
29
34
  getAgentFullSyncCursor,
30
35
  getAgentInfoMap,
36
+ getAnalyticsRevision,
31
37
  getPricingGeneration,
38
+ getSearchProjectDirectory,
32
39
  getSessionActivityTime,
33
40
  getSessionAgentKey,
34
- getSessionRouteKey,
35
41
  importBookmarks,
36
42
  isProjectIdentityKind,
37
43
  listBookmarks,
38
44
  listCachedProjectGroups,
45
+ listDashboardCostFacts,
39
46
  listFileActivity,
40
- listModelCostDistribution,
41
47
  listSessionAliases,
42
48
  loadCachedSessionHeads,
43
49
  loadCachedSessions,
@@ -51,11 +57,13 @@ import {
51
57
  mergeSearchQueryOptions,
52
58
  mergeSessionsUpdatedEvents,
53
59
  mergeSortedSessions,
60
+ normalizeProjectDirectory,
54
61
  normalizeSessionReference,
55
62
  perf,
56
63
  publishPendingPricing,
57
64
  readAgentCacheInitialization,
58
65
  readAgentLastFullSyncAt,
66
+ readCachedSessions,
59
67
  readPendingSearchIndexMaintenance,
60
68
  refreshPricingCache,
61
69
  scanSessions,
@@ -65,7 +73,7 @@ import {
65
73
  startOfCalendarDay,
66
74
  upsertBookmark,
67
75
  upsertSessionAlias
68
- } from "./chunk-VAUC2W7I.js";
76
+ } from "./chunk-PSHZZITB.js";
69
77
 
70
78
  // src/index.ts
71
79
  import { defineCommand, runMain } from "citty";
@@ -317,136 +325,92 @@ function paginateSessionSnapshot(items, request) {
317
325
  };
318
326
  }
319
327
 
320
- // src/api/session-aliases-view.ts
321
- function aliasKey(reference) {
322
- return `${reference.agentName.toLowerCase()}\0${reference.sessionId}`;
323
- }
324
- function loadAliasMap() {
325
- try {
326
- return new Map(listSessionAliases().map((alias) => [aliasKey(alias.reference), alias]));
327
- } catch (error) {
328
- if (!(error instanceof StateStorageUnavailableError)) {
329
- appLogger.warn("api.session_aliases.load_failed", {
330
- error: error instanceof Error ? error.message : String(error)
331
- });
332
- }
333
- return /* @__PURE__ */ new Map();
334
- }
335
- }
336
- function buildAliasView() {
337
- const aliases = loadAliasMap();
338
- return {
339
- size: aliases.size,
340
- get: (reference) => aliases.get(aliasKey(reference))?.alias,
341
- decorate(record, reference) {
342
- const alias = aliases.get(aliasKey(reference))?.alias;
343
- return alias ? { ...record, display_title: alias } : record;
344
- },
345
- entries: () => aliases.values()
346
- };
347
- }
348
- var cachedView = null;
349
- function loadAliasView() {
350
- return cachedView ??= buildAliasView();
351
- }
352
- function invalidateAliasView() {
353
- cachedView = null;
354
- }
355
- function decorateBookmark(bookmark, aliases) {
356
- if (bookmark.availability === "available") {
357
- return {
358
- ...bookmark,
359
- session: aliases.decorate(bookmark.session, bookmark.reference)
360
- };
361
- }
362
- const displayTitle = aliases.get(bookmark.reference);
363
- return displayTitle ? { ...bookmark, display_title: displayTitle } : bookmark;
364
- }
365
- function decorateFileActivity(activity, aliases) {
366
- return {
367
- ...activity,
368
- session: aliases.decorate(activity.session, activity.reference)
369
- };
370
- }
371
- function findAliasSearchResults(query, options, scanResult, aliases) {
372
- const search = mergeSearchQueryOptions(query, options);
373
- const needle = search.text.trim().toLowerCase();
374
- if (!needle || aliases.size === 0) return [];
375
- const sessionsByAgent = /* @__PURE__ */ new Map();
376
- const lookupSession = (agentName, sessionId) => {
377
- let byId = sessionsByAgent.get(agentName);
378
- if (!byId) {
379
- byId = new Map((scanResult.byAgent[agentName] ?? []).map((item) => [item.id, item]));
380
- sessionsByAgent.set(agentName, byId);
381
- }
382
- return byId.get(sessionId);
383
- };
384
- const results = [];
385
- for (const alias of aliases.entries()) {
386
- if (!alias.alias.toLowerCase().includes(needle)) continue;
387
- const { agentName, sessionId } = alias.reference;
388
- const session = lookupSession(agentName, sessionId);
389
- if (!session) continue;
390
- results.push({
391
- reference: alias.reference,
392
- session: aliases.decorate(session, alias.reference),
393
- snippet: `Alias \xB7 ${session.directory}`,
394
- snippetHighlights: [],
395
- matchType: "title"
396
- });
397
- }
398
- return filterSessionSearchCandidates(results, search.options, scanResult.sessions).sort(
399
- (a, b) => getSessionActivityTime(b.session) - getSessionActivityTime(a.session)
400
- );
401
- }
402
-
403
- // src/api/handlers.ts
404
- var KNOWN_AGENT_NAMES = getAgentInfoMap({}).map((agent) => agent.name);
405
- var KNOWN_AGENT_NAME_SET = new Set(KNOWN_AGENT_NAMES);
406
- function reportInvalidQueryParameter(endpoint, parameter, validationOutcome) {
407
- appLogger.warn("api.query_parameter.invalid", {
408
- endpoint,
409
- parameter,
410
- validation_outcome: validationOutcome
411
- });
412
- }
413
- function parseDateWindowRequest(c, endpoint, defaults) {
414
- const outcome = parseDateWindow(searchParams(c), defaults);
415
- if (outcome.kind === "valid") return outcome;
416
- reportInvalidQueryParameter(endpoint, outcome.parameter, "rejected");
417
- return {
418
- kind: "rejected",
419
- response: c.json({ error: `${outcome.parameter} ${outcome.error}` }, 400)
420
- };
421
- }
328
+ // src/api/snapshot-aggregation.ts
422
329
  var SNAPSHOT_AGGREGATION_CACHE_LIMIT = 64;
423
330
  var snapshotAggregationCaches = /* @__PURE__ */ new WeakMap();
424
- function getSnapshotAggregation(source, sessions, key, build) {
331
+ function getSnapshotAggregationCache(source, sessions) {
425
332
  let cache = snapshotAggregationCaches.get(source);
426
333
  if (!cache || cache.sessions !== sessions) {
427
334
  cache = { sessions, values: /* @__PURE__ */ new Map() };
428
335
  snapshotAggregationCaches.set(source, cache);
429
336
  }
337
+ return cache;
338
+ }
339
+ function getSnapshotAggregation(source, sessions, key, build, onCacheState) {
340
+ const cache = getSnapshotAggregationCache(source, sessions);
430
341
  const cacheKey = JSON.stringify(key);
431
- if (cache.values.has(cacheKey)) return cache.values.get(cacheKey);
342
+ if (cache.values.has(cacheKey)) {
343
+ const cached = cache.values.get(cacheKey);
344
+ cache.values.delete(cacheKey);
345
+ cache.values.set(cacheKey, cached);
346
+ onCacheState?.("hit");
347
+ return cached;
348
+ }
432
349
  const value = build();
433
350
  if (cache.values.size >= SNAPSHOT_AGGREGATION_CACHE_LIMIT) {
434
351
  const oldestKey = cache.values.keys().next().value;
435
352
  if (oldestKey != null) cache.values.delete(oldestKey);
436
353
  }
437
354
  cache.values.set(cacheKey, value);
355
+ onCacheState?.("miss");
438
356
  return value;
439
357
  }
440
- function withStorageErrors(handler, onUnavailable) {
441
- try {
442
- return handler();
443
- } catch (error) {
444
- if (error instanceof StateStorageUnavailableError) {
445
- return onUnavailable();
446
- }
447
- throw error;
358
+ function getSnapshotSessionTree(source, sessions) {
359
+ const cache = getSnapshotAggregationCache(source, sessions);
360
+ return cache.sessionTree ??= buildSessionTree(sessions);
361
+ }
362
+ function getDashboardCostFacts(source, sessions, from, to, cacheTo, analyticsRevision) {
363
+ const build = () => listDashboardCostFacts({ from, to });
364
+ if (analyticsRevision === null) return build();
365
+ return getSnapshotAggregation(
366
+ source,
367
+ sessions,
368
+ ["dashboard-cost-facts", from, cacheTo, analyticsRevision],
369
+ build
370
+ );
371
+ }
372
+ function getDashboardStorageAggregation(source, sessions, scope, from, to, cacheTo, analyticsRevision) {
373
+ const build = () => ({
374
+ recentFileActivities: listFileActivity({
375
+ agent: scope.agent,
376
+ projectKind: scope.projectKind,
377
+ projectKey: scope.projectKey,
378
+ from,
379
+ to,
380
+ limit: 12
381
+ })
382
+ });
383
+ const startedAt = performance.now();
384
+ const log = (cache) => {
385
+ appLogger.info("api.dashboard.storage_aggregation", {
386
+ cache,
387
+ ...analyticsRevision === null ? {} : { analytics_revision: analyticsRevision },
388
+ duration_ms: Math.round(performance.now() - startedAt)
389
+ });
390
+ };
391
+ if (analyticsRevision === null) {
392
+ const value = build();
393
+ log("unavailable");
394
+ return value;
448
395
  }
396
+ return getSnapshotAggregation(
397
+ source,
398
+ sessions,
399
+ [
400
+ "dashboard-storage",
401
+ scope.agent,
402
+ scope.projectKind,
403
+ scope.projectKey,
404
+ from,
405
+ cacheTo,
406
+ analyticsRevision
407
+ ],
408
+ build,
409
+ log
410
+ );
449
411
  }
412
+
413
+ // src/api/request-payloads.ts
450
414
  function isRecord(value) {
451
415
  return typeof value === "object" && value !== null;
452
416
  }
@@ -496,23 +460,8 @@ function sanitizeClientLogData(value) {
496
460
  })
497
461
  );
498
462
  }
499
- function toSessionListItem(session) {
500
- const {
501
- model_usage: _modelUsage,
502
- project_identity_resolver_revision: _resolverRevision,
503
- project_identity_input_signature: _identityInputSignature,
504
- smart_tags_source_updated_at: _smartTagsSourceUpdatedAt,
505
- smart_tags_classifier_revision: _smartTagsClassifierRevision,
506
- ...item
507
- } = session;
508
- return item;
509
- }
510
- function getSessionHeadReference(session) {
511
- return {
512
- agentName: getSessionAgentKey(session),
513
- sessionId: session.id
514
- };
515
- }
463
+
464
+ // src/api/session-detail-stream.ts
516
465
  var SESSION_DETAIL_STREAM_BATCH_CHARS = 64 * 1024;
517
466
  function createSessionDetailJsonResponse(data, messages) {
518
467
  const encoder = new TextEncoder();
@@ -565,6 +514,193 @@ function createSessionDetailJsonResponse(data, messages) {
565
514
  { headers: { "Content-Type": "application/json; charset=UTF-8" } }
566
515
  );
567
516
  }
517
+
518
+ // src/api/search-result-merge.ts
519
+ function withParentContext(results, getSessionTree, aliases) {
520
+ if (!results.some((result) => result.session.parent_reference)) return results;
521
+ const byRouteKey = getSessionTree().byRouteKey;
522
+ return results.map((result) => {
523
+ const parentReference = result.session.parent_reference;
524
+ if (!parentReference) return result;
525
+ const parent = byRouteKey.get(formatSessionReference(parentReference))?.session;
526
+ if (!parent) return result;
527
+ const reference = normalizeSessionReference(parentReference);
528
+ return { ...result, parent: { reference, title: aliases.get(reference) ?? parent.title } };
529
+ });
530
+ }
531
+ var ALIAS_SEARCH_RESULT_SHARE = 0.25;
532
+ function searchResultKey(result) {
533
+ return `${result.reference.agentName}\0${result.reference.sessionId}`;
534
+ }
535
+ function uniqueSearchResults(results) {
536
+ const seen = /* @__PURE__ */ new Set();
537
+ return results.filter((result) => {
538
+ const key = searchResultKey(result);
539
+ if (seen.has(key)) return false;
540
+ seen.add(key);
541
+ return true;
542
+ });
543
+ }
544
+ function mergeAliasSearchResults(rankedResults, aliasResults, limit) {
545
+ if (limit <= 0) return [];
546
+ const ranked = uniqueSearchResults(rankedResults);
547
+ const rankedKeys = new Set(ranked.map(searchResultKey));
548
+ const aliases = uniqueSearchResults(aliasResults).filter(
549
+ (result) => !rankedKeys.has(searchResultKey(result))
550
+ );
551
+ if (ranked.length === 0) return aliases.slice(0, limit);
552
+ if (aliases.length === 0) return ranked.slice(0, limit);
553
+ const aliasQuota = Math.min(
554
+ limit - 1,
555
+ Math.max(1, Math.floor(limit * ALIAS_SEARCH_RESULT_SHARE))
556
+ );
557
+ const rankedQuota = limit - aliasQuota;
558
+ return [
559
+ ...ranked.slice(0, rankedQuota),
560
+ ...aliases.slice(0, aliasQuota),
561
+ ...ranked.slice(rankedQuota),
562
+ ...aliases.slice(aliasQuota)
563
+ ].slice(0, limit);
564
+ }
565
+
566
+ // src/api/session-aliases-view.ts
567
+ function aliasKey(reference) {
568
+ return `${reference.agentName.toLowerCase()}\0${reference.sessionId}`;
569
+ }
570
+ function loadAliasMap() {
571
+ try {
572
+ return new Map(listSessionAliases().map((alias) => [aliasKey(alias.reference), alias]));
573
+ } catch (error) {
574
+ if (!(error instanceof StateStorageUnavailableError)) {
575
+ appLogger.warn("api.session_aliases.load_failed", {
576
+ error: error instanceof Error ? error.message : String(error)
577
+ });
578
+ }
579
+ return /* @__PURE__ */ new Map();
580
+ }
581
+ }
582
+ function buildAliasView() {
583
+ const aliases = loadAliasMap();
584
+ return {
585
+ size: aliases.size,
586
+ get: (reference) => aliases.get(aliasKey(reference))?.alias,
587
+ decorate(record, reference) {
588
+ const alias = aliases.get(aliasKey(reference))?.alias;
589
+ return alias ? { ...record, display_title: alias } : record;
590
+ },
591
+ entries: () => aliases.values()
592
+ };
593
+ }
594
+ var cachedView = null;
595
+ function loadAliasView() {
596
+ return cachedView ??= buildAliasView();
597
+ }
598
+ function invalidateAliasView() {
599
+ cachedView = null;
600
+ }
601
+ function decorateBookmark(bookmark, aliases) {
602
+ if (bookmark.availability === "available") {
603
+ return {
604
+ ...bookmark,
605
+ session: aliases.decorate(bookmark.session, bookmark.reference)
606
+ };
607
+ }
608
+ const displayTitle = aliases.get(bookmark.reference);
609
+ return displayTitle ? { ...bookmark, display_title: displayTitle } : bookmark;
610
+ }
611
+ function decorateFileActivity(activity, aliases) {
612
+ return {
613
+ ...activity,
614
+ session: aliases.decorate(activity.session, activity.reference)
615
+ };
616
+ }
617
+ function findAliasSearchResults(query, options, scanResult, aliases, context = {}) {
618
+ const search = mergeSearchQueryOptions(query, options);
619
+ const needle = search.text.trim().toLowerCase();
620
+ if (!needle || aliases.size === 0) return [];
621
+ const sessionsByAgent = /* @__PURE__ */ new Map();
622
+ const lookupSession = (agentName, sessionId) => {
623
+ let byId = sessionsByAgent.get(agentName);
624
+ if (!byId) {
625
+ byId = new Map((scanResult.byAgent[agentName] ?? []).map((item) => [item.id, item]));
626
+ sessionsByAgent.set(agentName, byId);
627
+ }
628
+ return byId.get(sessionId);
629
+ };
630
+ const results = [];
631
+ for (const alias of aliases.entries()) {
632
+ if (!alias.alias.toLowerCase().includes(needle)) continue;
633
+ const { agentName, sessionId } = alias.reference;
634
+ const session = lookupSession(agentName, sessionId);
635
+ if (!session) continue;
636
+ results.push({
637
+ reference: alias.reference,
638
+ session: aliases.decorate(session, alias.reference),
639
+ snippet: `Alias \xB7 ${session.directory}`,
640
+ snippetHighlights: [],
641
+ matchType: "title"
642
+ });
643
+ }
644
+ return filterSessionSearchCandidates(results, search.options, {
645
+ sessionSnapshot: scanResult.sessions,
646
+ sessionTree: context.sessionTree
647
+ }).sort((a, b) => getSessionActivityTime(b.session) - getSessionActivityTime(a.session));
648
+ }
649
+
650
+ // src/api/handlers.ts
651
+ var KNOWN_AGENT_NAMES = getAgentInfoMap({}).map((agent) => agent.name);
652
+ var KNOWN_AGENT_NAME_SET = new Set(KNOWN_AGENT_NAMES);
653
+ async function resolveProjectScope(cwd, sessions, resolver) {
654
+ const normalizedCwd = normalizeProjectDirectory(cwd);
655
+ const matchingSession = sessions.find(
656
+ (session) => normalizeProjectDirectory(session.directory) === normalizedCwd && session.project_identity != null && session.project_identity_resolver_revision === PROJECT_IDENTITY_RESOLVER_REVISION && Boolean(session.project_identity_input_signature)
657
+ );
658
+ if (matchingSession?.project_identity) {
659
+ return createProjectScopeMatcherFromIdentity(cwd, matchingSession.project_identity);
660
+ }
661
+ if (!resolver) throw new Error("Project identity resolver is unavailable");
662
+ const projection = await resolver.resolve(cwd);
663
+ return createProjectScopeMatcherFromIdentity(cwd, projection.identity);
664
+ }
665
+ function reportProjectScopeResolutionFailure(endpoint, error) {
666
+ appLogger.warn("api.project_scope.unavailable", {
667
+ endpoint,
668
+ error: error instanceof Error ? error.message : String(error)
669
+ });
670
+ }
671
+ function reportInvalidQueryParameter(endpoint, parameter, validationOutcome) {
672
+ appLogger.warn("api.query_parameter.invalid", {
673
+ endpoint,
674
+ parameter,
675
+ validation_outcome: validationOutcome
676
+ });
677
+ }
678
+ function parseDateWindowRequest(c, endpoint, defaults) {
679
+ const outcome = parseDateWindow(searchParams(c), defaults);
680
+ if (outcome.kind === "valid") return outcome;
681
+ reportInvalidQueryParameter(endpoint, outcome.parameter, "rejected");
682
+ return {
683
+ kind: "rejected",
684
+ response: c.json({ error: `${outcome.parameter} ${outcome.error}` }, 400)
685
+ };
686
+ }
687
+ function toSessionListItem(session) {
688
+ const {
689
+ model_usage: _modelUsage,
690
+ project_identity_resolver_revision: _resolverRevision,
691
+ project_identity_input_signature: _identityInputSignature,
692
+ smart_tags_source_updated_at: _smartTagsSourceUpdatedAt,
693
+ smart_tags_classifier_revision: _smartTagsClassifierRevision,
694
+ ...item
695
+ } = session;
696
+ return item;
697
+ }
698
+ function getSessionHeadReference(session) {
699
+ return {
700
+ agentName: getSessionAgentKey(session),
701
+ sessionId: session.id
702
+ };
703
+ }
568
704
  function handleGetConfig(c, defaults) {
569
705
  const payload = {
570
706
  window: {
@@ -604,21 +740,30 @@ function handleGetProjects(c, scanSource, defaults = {}) {
604
740
  const window = parseDateWindowRequest(c, "projects", defaults);
605
741
  if (window.kind === "rejected") return window.response;
606
742
  const { from, to } = window;
743
+ const analyticsRevision = getAnalyticsRevision();
607
744
  const projects = getSnapshotAggregation(
608
745
  scanSource,
609
746
  scanResult.sessions,
610
- ["projects", from, to],
747
+ ["projects", from, to, analyticsRevision],
611
748
  () => {
612
- const tree = buildSessionTree(scanResult.sessions);
613
- const sessions = filterSessionTreeByActivityWindow(scanResult.sessions, from, to, tree);
749
+ const tree = getSnapshotSessionTree(scanSource, scanResult.sessions);
750
+ const costFacts = listDashboardCostFacts({ from, to, includeModelCosts: false });
614
751
  return {
615
- projects: attachProjectMetricsFromTree(listCachedProjectGroups(sessions), tree, from, to)
752
+ projects: attachProjectMetricsFromTree(
753
+ listCachedProjectGroups(scanResult.sessions),
754
+ tree,
755
+ from,
756
+ to,
757
+ costFacts
758
+ ).filter(
759
+ (project) => project.sessionCount > 0 || project.messages > 0 || project.tokens > 0 || project.cost > 0
760
+ )
616
761
  };
617
762
  }
618
763
  );
619
764
  return c.json(projects);
620
765
  }
621
- function handleGetSessions(c, scanSource, defaults = {}) {
766
+ async function handleGetSessions(c, scanSource, defaults = {}, resolver) {
622
767
  const scanResult = scanSource.getSnapshot();
623
768
  const params = searchParams(c);
624
769
  const paginationRequested = params.has("limit") || params.has("cursor");
@@ -628,7 +773,7 @@ function handleGetSessions(c, scanSource, defaults = {}) {
628
773
  return c.json({ error: sessionQuery.limit.error }, 400);
629
774
  }
630
775
  const q = c.req.query("q")?.toLowerCase();
631
- const cwd = c.req.query("cwd");
776
+ const cwd = optionalQueryValue(c.req.query("cwd"));
632
777
  const projectIdentity = parseProjectIdentityFilter(
633
778
  c.req.query("projectKind"),
634
779
  c.req.query("projectKey")
@@ -640,6 +785,15 @@ function handleGetSessions(c, scanSource, defaults = {}) {
640
785
  const window = parseDateWindowRequest(c, "sessions", defaults);
641
786
  if (window.kind === "rejected") return window.response;
642
787
  const { from, to } = window;
788
+ let projectScope;
789
+ if (cwd && !projectIdentity) {
790
+ try {
791
+ projectScope = await resolveProjectScope(cwd, scanResult.sessions, resolver);
792
+ } catch (error) {
793
+ reportProjectScopeResolutionFailure("sessions", error);
794
+ return c.json({ error: "Project scope unavailable" }, 503);
795
+ }
796
+ }
643
797
  if (sessionQuery.agent.kind === "unknown") {
644
798
  reportInvalidQueryParameter("sessions", "agent", "empty_result");
645
799
  }
@@ -647,15 +801,25 @@ function handleGetSessions(c, scanSource, defaults = {}) {
647
801
  let sessions = getSnapshotAggregation(
648
802
  scanSource,
649
803
  scanResult.sessions,
650
- ["sessions", agentFilter, projectIdentity?.kind, projectIdentity?.key, cwd, tag, from, to],
804
+ [
805
+ "sessions",
806
+ agentFilter,
807
+ projectIdentity?.kind,
808
+ projectIdentity?.key,
809
+ projectScope?.identity.kind,
810
+ projectScope?.identity.key,
811
+ projectScope?.path,
812
+ tag,
813
+ from,
814
+ to
815
+ ],
651
816
  () => {
652
817
  let filtered = sessionQuery.agent.kind === "all" ? scanResult.sessions : sessionQuery.agent.kind === "known" ? scanResult.byAgent[sessionQuery.agent.agentName] ?? [] : [];
653
818
  if (projectIdentity) {
654
819
  filtered = filtered.filter(
655
820
  (session) => matchesProjectIdentity(session.project_identity, projectIdentity)
656
821
  );
657
- } else if (cwd) {
658
- const projectScope = createProjectScopeMatcher(cwd);
822
+ } else if (projectScope) {
659
823
  filtered = filtered.filter((session) => matchesProjectScope(session, projectScope));
660
824
  }
661
825
  filtered = filterSessionsByActivityWindow(filtered, from, to);
@@ -697,58 +861,7 @@ function handleGetSessions(c, scanSource, defaults = {}) {
697
861
  )
698
862
  });
699
863
  }
700
- function withParentContext(results, sessions, aliases) {
701
- if (!results.some((result) => result.session.parent_reference)) return results;
702
- const byRouteKey = new Map(
703
- sessions.map((session) => [
704
- getSessionRouteKey(getSessionAgentKey(session), session.id),
705
- session
706
- ])
707
- );
708
- return results.map((result) => {
709
- const parentReference = result.session.parent_reference;
710
- if (!parentReference) return result;
711
- const parent = byRouteKey.get(formatSessionReference(parentReference));
712
- if (!parent) return result;
713
- const reference = normalizeSessionReference(parentReference);
714
- return { ...result, parent: { reference, title: aliases.get(reference) ?? parent.title } };
715
- });
716
- }
717
- var ALIAS_SEARCH_RESULT_SHARE = 0.25;
718
- function searchResultKey(result) {
719
- return `${result.reference.agentName}\0${result.reference.sessionId}`;
720
- }
721
- function uniqueSearchResults(results) {
722
- const seen = /* @__PURE__ */ new Set();
723
- return results.filter((result) => {
724
- const key = searchResultKey(result);
725
- if (seen.has(key)) return false;
726
- seen.add(key);
727
- return true;
728
- });
729
- }
730
- function mergeAliasSearchResults(rankedResults, aliasResults, limit) {
731
- if (limit <= 0) return [];
732
- const ranked = uniqueSearchResults(rankedResults);
733
- const rankedKeys = new Set(ranked.map(searchResultKey));
734
- const aliases = uniqueSearchResults(aliasResults).filter(
735
- (result) => !rankedKeys.has(searchResultKey(result))
736
- );
737
- if (ranked.length === 0) return aliases.slice(0, limit);
738
- if (aliases.length === 0) return ranked.slice(0, limit);
739
- const aliasQuota = Math.min(
740
- limit - 1,
741
- Math.max(1, Math.floor(limit * ALIAS_SEARCH_RESULT_SHARE))
742
- );
743
- const rankedQuota = limit - aliasQuota;
744
- return [
745
- ...ranked.slice(0, rankedQuota),
746
- ...aliases.slice(0, aliasQuota),
747
- ...ranked.slice(rankedQuota),
748
- ...aliases.slice(aliasQuota)
749
- ].slice(0, limit);
750
- }
751
- function handleSearchSessions(c, scanSource, defaults = {}) {
864
+ async function handleSearchSessions(c, scanSource, defaults = {}, resolver) {
752
865
  const query = c.req.query("q")?.trim() ?? "";
753
866
  const scanResult = scanSource.getSnapshot();
754
867
  const sessionQuery = parseSessionQuery(searchParams(c), KNOWN_AGENT_NAMES, SEARCH_LIMIT_POLICY);
@@ -769,7 +882,7 @@ function handleSearchSessions(c, scanSource, defaults = {}) {
769
882
  reportInvalidQueryParameter("search", "agent", "empty_result");
770
883
  return c.json({ results: [] });
771
884
  }
772
- const searchOptions = parseSearchOptions(
885
+ const searchRequestOptions = parseSearchOptions(
773
886
  c,
774
887
  window,
775
888
  {
@@ -778,18 +891,45 @@ function handleSearchSessions(c, scanSource, defaults = {}) {
778
891
  },
779
892
  projectIdentity
780
893
  );
894
+ const cwd = getSearchProjectDirectory(query, searchRequestOptions);
895
+ let projectScope;
896
+ if (cwd) {
897
+ try {
898
+ projectScope = await resolveProjectScope(cwd, scanResult.sessions, resolver);
899
+ } catch (error) {
900
+ reportProjectScopeResolutionFailure("search", error);
901
+ return c.json({ error: "Project scope unavailable" }, 503);
902
+ }
903
+ }
904
+ const { cwd: _cwd, ...searchOptions } = searchRequestOptions;
905
+ const resolvedSearchOptions = projectScope ? { ...searchOptions, projectScope } : searchOptions;
781
906
  const aliases = loadAliasView();
782
- const results = executeSessionSearch(query, searchOptions, scanResult).map((result) => ({
907
+ let sessionTree;
908
+ const getSessionTree = () => sessionTree ??= getSnapshotSessionTree(scanSource, scanResult.sessions);
909
+ const effectiveSearchOptions = mergeSearchQueryOptions(query, resolvedSearchOptions).options;
910
+ const searchContext = effectiveSearchOptions.costMin != null || effectiveSearchOptions.costMax != null ? { sessionTree: getSessionTree() } : void 0;
911
+ const searchResults = searchContext ? executeSessionSearch(query, resolvedSearchOptions, scanResult, searchContext) : executeSessionSearch(query, resolvedSearchOptions, scanResult);
912
+ const results = searchResults.map((result) => ({
783
913
  ...result,
784
914
  session: aliases.decorate(result.session, result.reference)
785
915
  }));
786
- const aliasResults = findAliasSearchResults(query, searchOptions, scanResult, aliases);
787
- const mergedResults = mergeAliasSearchResults(results, aliasResults, searchOptions.limit ?? 50);
916
+ const aliasResults = findAliasSearchResults(
917
+ query,
918
+ resolvedSearchOptions,
919
+ scanResult,
920
+ aliases,
921
+ searchContext
922
+ );
923
+ const mergedResults = mergeAliasSearchResults(
924
+ results,
925
+ aliasResults,
926
+ resolvedSearchOptions.limit ?? 50
927
+ );
788
928
  return c.json({
789
- results: withParentContext(mergedResults, scanResult.sessions, aliases)
929
+ results: withParentContext(mergedResults, getSessionTree, aliases)
790
930
  });
791
931
  }
792
- function handleGetFileActivity(c, defaults = {}) {
932
+ async function handleGetFileActivity(c, defaults = {}, resolver) {
793
933
  const sessionQuery = parseSessionQuery(
794
934
  searchParams(c),
795
935
  KNOWN_AGENT_NAMES,
@@ -812,6 +952,16 @@ function handleGetFileActivity(c, defaults = {}) {
812
952
  reportInvalidQueryParameter("file-activity", "agent", "empty_result");
813
953
  return c.json({ activity: [] });
814
954
  }
955
+ const cwd = optionalQueryValue(c.req.query("cwd"));
956
+ let projectScope;
957
+ if (cwd) {
958
+ try {
959
+ projectScope = await resolveProjectScope(cwd, [], resolver);
960
+ } catch (error) {
961
+ reportProjectScopeResolutionFailure("file-activity", error);
962
+ return c.json({ error: "Project scope unavailable" }, 503);
963
+ }
964
+ }
815
965
  const aliases = loadAliasView();
816
966
  return c.json({
817
967
  activity: listFileActivity({
@@ -820,7 +970,7 @@ function handleGetFileActivity(c, defaults = {}) {
820
970
  projectKind: projectIdentity?.kind,
821
971
  projectKey: projectIdentity?.key,
822
972
  project: optionalQueryValue(c.req.query("project")),
823
- cwd: optionalQueryValue(c.req.query("cwd")),
973
+ projectScope,
824
974
  path: optionalQueryValue(c.req.query("path")),
825
975
  kind: parseFileActivityKind(optionalQueryValue(c.req.query("kind"))),
826
976
  from: window.from,
@@ -829,7 +979,7 @@ function handleGetFileActivity(c, defaults = {}) {
829
979
  }).map((activity) => decorateFileActivity(activity, aliases))
830
980
  });
831
981
  }
832
- async function handleGetSessionData(c, scanSource) {
982
+ async function handleGetSessionData(c, scanSource, resolver) {
833
983
  const startedAt = performance.now();
834
984
  const agentName = c.req.param("agent");
835
985
  const sessionId = c.req.param("id");
@@ -845,7 +995,18 @@ async function handleGetSessionData(c, scanSource) {
845
995
  sessionId
846
996
  };
847
997
  const messageCursor = optionalQueryValue(c.req.query("messageCursor"));
848
- const result = messageCursor ? materializeSessionDetailResponse(scanSource.getSnapshot(), reference, { messageCursor }) : materializeSessionDetailResponse(scanSource.getSnapshot(), reference);
998
+ const materialize = (fallback) => materializeSessionDetailResponse(scanSource.getSnapshot(), reference, {
999
+ ...messageCursor ? { messageCursor } : {},
1000
+ ...fallback ? { projectIdentityFallback: fallback } : {}
1001
+ });
1002
+ let result = materialize();
1003
+ if (result.status === "needs-identity") {
1004
+ if (!resolver) throw new Error("Project identity resolver is unavailable");
1005
+ result = materialize((await resolver.resolve(result.directory)).identity);
1006
+ }
1007
+ if (result.status === "needs-identity") {
1008
+ throw new Error("Project identity fallback was not applied");
1009
+ }
849
1010
  if (result.status === "unknown-agent") {
850
1011
  return c.json({ error: `Unknown agent: ${agentName}` }, 404);
851
1012
  }
@@ -884,15 +1045,118 @@ async function handleGetSessionData(c, scanSource) {
884
1045
  return c.json({ error: "Failed to load session" }, 500);
885
1046
  }
886
1047
  }
887
- async function handlePostClientLog(c) {
888
- const payload = await c.req.json().catch(() => null);
889
- const rawEvent = payload?.event;
890
- if (typeof rawEvent !== "string" || !rawEvent.trim()) {
891
- return c.json({ ok: false }, 400);
1048
+ async function handlePostClientLog(c) {
1049
+ const payload = await c.req.json().catch(() => null);
1050
+ const rawEvent = payload?.event;
1051
+ if (typeof rawEvent !== "string" || !rawEvent.trim()) {
1052
+ return c.json({ ok: false }, 400);
1053
+ }
1054
+ const event = rawEvent.trim().replace(/[^a-zA-Z0-9_.:-]/g, "_").slice(0, 120);
1055
+ appLogger.info(`client.${event}`, sanitizeClientLogData(payload?.data));
1056
+ return c.json({ ok: true });
1057
+ }
1058
+ function handleGetDashboard(c, scanSource, defaults = {}) {
1059
+ const scanResult = scanSource.getSnapshot();
1060
+ const projectIdentity = parseProjectIdentityFilter(
1061
+ c.req.query("projectKind"),
1062
+ c.req.query("projectKey")
1063
+ );
1064
+ if (projectIdentity === null) {
1065
+ return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
1066
+ }
1067
+ const dateWindow = parseDateWindowRequest(c, "dashboard", defaults);
1068
+ if (dateWindow.kind === "rejected") return dateWindow.response;
1069
+ const { from, to, days } = resolveTimeWindow({
1070
+ mode: "dashboard",
1071
+ query: {
1072
+ days: c.req.query("days"),
1073
+ from: c.req.query("from"),
1074
+ to: c.req.query("to")
1075
+ },
1076
+ defaults
1077
+ });
1078
+ const scope = {
1079
+ agent: optionalQueryValue(c.req.query("agent"))?.toLowerCase(),
1080
+ projectKind: projectIdentity?.kind,
1081
+ projectKey: projectIdentity?.key
1082
+ };
1083
+ const compare = from == null ? void 0 : { from: addCalendarDays(from, -(days ?? countCalendarDays(from, to))), to: from - 1 };
1084
+ const fixedTo = dateWindow.to;
1085
+ const cacheTo = fixedTo ?? startOfCalendarDay(to);
1086
+ const analyticsRevision = getAnalyticsRevision();
1087
+ const costFacts = getDashboardCostFacts(
1088
+ scanSource,
1089
+ scanResult.sessions,
1090
+ compare?.from ?? from,
1091
+ to,
1092
+ cacheTo,
1093
+ analyticsRevision
1094
+ );
1095
+ const aggregate = getSnapshotAggregation(
1096
+ scanSource,
1097
+ scanResult.sessions,
1098
+ [
1099
+ "dashboard",
1100
+ scope.agent,
1101
+ scope.projectKind,
1102
+ scope.projectKey,
1103
+ from,
1104
+ cacheTo,
1105
+ compare?.from,
1106
+ compare?.to,
1107
+ analyticsRevision
1108
+ ],
1109
+ () => {
1110
+ const agentInfo = getAgentInfoMap({});
1111
+ const agentInfoMap = new Map(agentInfo.map((agent) => [agent.name, agent]));
1112
+ return buildDashboard(scanResult.sessions, {
1113
+ byAgentNames: Object.keys(scanResult.byAgent),
1114
+ scope,
1115
+ from,
1116
+ to,
1117
+ agentInfoMap,
1118
+ compare,
1119
+ costFacts
1120
+ });
1121
+ }
1122
+ );
1123
+ const storageAggregation = getDashboardStorageAggregation(
1124
+ scanSource,
1125
+ scanResult.sessions,
1126
+ scope,
1127
+ from,
1128
+ to,
1129
+ cacheTo,
1130
+ analyticsRevision
1131
+ );
1132
+ const data = {
1133
+ ...aggregate,
1134
+ ...storageAggregation,
1135
+ window: { from, to, days, compareFrom: compare?.from, compareTo: compare?.to }
1136
+ };
1137
+ const aliases = loadAliasView();
1138
+ return c.json({
1139
+ ...data,
1140
+ recentSessions: data.recentSessions.map((item) => ({
1141
+ ...item,
1142
+ session: aliases.decorate(item.session, item.reference)
1143
+ })),
1144
+ recentFileActivities: data.recentFileActivities.map(
1145
+ (activity) => decorateFileActivity(activity, aliases)
1146
+ )
1147
+ });
1148
+ }
1149
+
1150
+ // src/api/bookmark-handlers.ts
1151
+ function withStorageErrors(handler, onUnavailable) {
1152
+ try {
1153
+ return handler();
1154
+ } catch (error) {
1155
+ if (error instanceof StateStorageUnavailableError) {
1156
+ return onUnavailable();
1157
+ }
1158
+ throw error;
892
1159
  }
893
- const event = rawEvent.trim().replace(/[^a-zA-Z0-9_.:-]/g, "_").slice(0, 120);
894
- appLogger.info(`client.${event}`, sanitizeClientLogData(payload?.data));
895
- return c.json({ ok: true });
896
1160
  }
897
1161
  function materializeStoredBookmarks(scanSource, bookmarks, aliases) {
898
1162
  const snapshot = scanSource.getSnapshot();
@@ -921,11 +1185,17 @@ function handleGetBookmarks(c, scanSource) {
921
1185
  () => c.json({ bookmarks: [], storageAvailable: false })
922
1186
  );
923
1187
  }
1188
+ function isKnownAgentKey(agentKey) {
1189
+ return KNOWN_AGENT_NAME_SET.has(agentKey.trim().toLowerCase());
1190
+ }
924
1191
  async function handlePutBookmark(c) {
925
1192
  const payload = parseBookmarkReference(await c.req.json().catch(() => null));
926
1193
  if (!payload) {
927
1194
  return c.json({ error: "Invalid bookmark payload" }, 400);
928
1195
  }
1196
+ if (!isKnownAgentKey(payload.agentName)) {
1197
+ return c.json({ error: `Unknown agent: ${payload.agentName}` }, 400);
1198
+ }
929
1199
  return withStorageErrors(
930
1200
  () => c.json({ bookmark: upsertBookmark(payload), storageAvailable: true }),
931
1201
  () => c.json({ error: "Bookmark storage is unavailable" }, 503)
@@ -936,15 +1206,21 @@ async function handleImportBookmarks(c, scanSource) {
936
1206
  if (!Array.isArray(payload)) {
937
1207
  return c.json({ error: "Invalid bookmark payload" }, 400);
938
1208
  }
939
- const bookmarks = payload.map((entry) => parseBookmarkImport(entry)).filter((entry) => entry !== null);
940
- if (bookmarks.length !== payload.length) {
1209
+ const parsed = payload.map((entry) => parseBookmarkImport(entry)).filter((entry) => entry !== null);
1210
+ if (parsed.length !== payload.length) {
941
1211
  return c.json({ error: "Invalid bookmark payload" }, 400);
942
1212
  }
1213
+ const bookmarks = parsed.filter((entry) => isKnownAgentKey(entry.reference.agentName));
1214
+ const skippedUnknownAgents = parsed.length - bookmarks.length;
943
1215
  return withStorageErrors(
944
1216
  () => {
945
1217
  const imported = importBookmarks(bookmarks);
946
1218
  const views = materializeStoredBookmarks(scanSource, imported, loadAliasView());
947
- return c.json({ bookmarks: views, storageAvailable: true });
1219
+ return c.json({
1220
+ bookmarks: views,
1221
+ storageAvailable: true,
1222
+ ...skippedUnknownAgents > 0 ? { skippedUnknownAgents } : {}
1223
+ });
948
1224
  },
949
1225
  () => c.json({ error: "Bookmark storage is unavailable" }, 503)
950
1226
  );
@@ -955,6 +1231,9 @@ function handleDeleteBookmark(c) {
955
1231
  if (!agentKey || !sessionId) {
956
1232
  return c.json({ error: "Missing bookmark identifier" }, 400);
957
1233
  }
1234
+ if (!isKnownAgentKey(agentKey)) {
1235
+ return c.json({ error: `Unknown agent: ${agentKey}` }, 400);
1236
+ }
958
1237
  return withStorageErrors(
959
1238
  () => {
960
1239
  deleteBookmark({ agentName: agentKey, sessionId });
@@ -971,6 +1250,9 @@ async function handlePutSessionAlias(c) {
971
1250
  if (!agentKey || !sessionId || typeof aliasValue !== "string") {
972
1251
  return c.json({ error: "Invalid session alias payload" }, 400);
973
1252
  }
1253
+ if (!isKnownAgentKey(agentKey)) {
1254
+ return c.json({ error: `Unknown agent: ${agentKey}` }, 400);
1255
+ }
974
1256
  try {
975
1257
  return withStorageErrors(
976
1258
  () => {
@@ -993,6 +1275,9 @@ function handleDeleteSessionAlias(c) {
993
1275
  if (!agentKey || !sessionId) {
994
1276
  return c.json({ error: "Missing session alias identifier" }, 400);
995
1277
  }
1278
+ if (!isKnownAgentKey(agentKey)) {
1279
+ return c.json({ error: `Unknown agent: ${agentKey}` }, 400);
1280
+ }
996
1281
  return withStorageErrors(
997
1282
  () => {
998
1283
  deleteSessionAlias({ agentName: agentKey, sessionId });
@@ -1002,95 +1287,24 @@ function handleDeleteSessionAlias(c) {
1002
1287
  () => c.json({ error: "Session alias storage is unavailable" }, 503)
1003
1288
  );
1004
1289
  }
1005
- function handleGetDashboard(c, scanSource, defaults = {}) {
1006
- const scanResult = scanSource.getSnapshot();
1007
- const projectIdentity = parseProjectIdentityFilter(
1008
- c.req.query("projectKind"),
1009
- c.req.query("projectKey")
1010
- );
1011
- if (projectIdentity === null) {
1012
- return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
1013
- }
1014
- const dateWindow = parseDateWindowRequest(c, "dashboard", defaults);
1015
- if (dateWindow.kind === "rejected") return dateWindow.response;
1016
- const { from, to, days } = resolveTimeWindow({
1017
- mode: "dashboard",
1018
- query: {
1019
- days: c.req.query("days"),
1020
- from: c.req.query("from"),
1021
- to: c.req.query("to")
1022
- },
1023
- defaults
1024
- });
1025
- const scope = {
1026
- agent: optionalQueryValue(c.req.query("agent"))?.toLowerCase(),
1027
- projectKind: projectIdentity?.kind,
1028
- projectKey: projectIdentity?.key
1029
- };
1030
- const compare = from == null ? void 0 : { from: addCalendarDays(from, -(days ?? countCalendarDays(from, to))), to: from - 1 };
1031
- const fixedTo = dateWindow.to;
1032
- const aggregate = getSnapshotAggregation(
1033
- scanSource,
1034
- scanResult.sessions,
1035
- [
1036
- "dashboard",
1037
- scope.agent,
1038
- scope.projectKind,
1039
- scope.projectKey,
1040
- from,
1041
- fixedTo ?? startOfCalendarDay(to),
1042
- compare?.from,
1043
- compare?.to
1044
- ],
1045
- () => {
1046
- const agentInfo = getAgentInfoMap({});
1047
- const agentInfoMap = new Map(agentInfo.map((agent) => [agent.name, agent]));
1048
- return buildDashboard(scanResult.sessions, {
1049
- byAgentNames: Object.keys(scanResult.byAgent),
1050
- scope,
1051
- from,
1052
- to,
1053
- agentInfoMap,
1054
- compare
1055
- });
1056
- }
1057
- );
1058
- const data = {
1059
- ...aggregate,
1060
- recentFileActivities: listFileActivity({
1061
- agent: scope.agent,
1062
- projectKind: scope.projectKind,
1063
- projectKey: scope.projectKey,
1064
- from,
1065
- to,
1066
- limit: 12
1067
- }),
1068
- modelCost: listModelCostDistribution({
1069
- agent: scope.agent,
1070
- projectKind: scope.projectKind,
1071
- projectKey: scope.projectKey,
1072
- from,
1073
- to
1074
- }),
1075
- window: { from, to, days, compareFrom: compare?.from, compareTo: compare?.to }
1076
- };
1077
- const aliases = loadAliasView();
1078
- return c.json({
1079
- ...data,
1080
- recentSessions: data.recentSessions.map((item) => ({
1081
- ...item,
1082
- session: aliases.decorate(item.session, item.reference)
1083
- })),
1084
- recentFileActivities: data.recentFileActivities.map(
1085
- (activity) => decorateFileActivity(activity, aliases)
1086
- )
1087
- });
1088
- }
1089
1290
 
1090
1291
  // src/api/sse-event-buffer.ts
1091
1292
  var MAX_PENDING_CRITICAL_SSE_FRAMES = 64;
1092
1293
  function scanStatusMilestone(status) {
1093
- const agentStates = Object.entries(status.agentStatuses).sort(([left], [right]) => left.localeCompare(right)).map(([agentName, agentStatus]) => [agentName, agentStatus.status, agentStatus.error ?? null]);
1294
+ const agentStates = Object.entries(status.agentStatuses).sort(([left], [right]) => left.localeCompare(right)).map(([agentName, agentStatus]) => [
1295
+ agentName,
1296
+ agentStatus.status,
1297
+ agentStatus.error ?? null,
1298
+ agentStatus.completeness ?? null,
1299
+ agentStatus.sourceFailureCount ?? null,
1300
+ agentStatus.sourceFailureSummary ?? null
1301
+ ]);
1302
+ const partialBackfills = Object.entries(status.backfill.partialAgents ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([agentName, completion]) => [
1303
+ agentName,
1304
+ completion.completeness,
1305
+ completion.sourceFailureCount ?? null,
1306
+ completion.sourceFailureSummary ?? null
1307
+ ]);
1094
1308
  return JSON.stringify([
1095
1309
  status.active,
1096
1310
  status.phase,
@@ -1105,6 +1319,7 @@ function scanStatusMilestone(status) {
1105
1319
  status.backfill.completedAgents,
1106
1320
  status.backfill.failedAgents,
1107
1321
  status.backfill.progress?.phase ?? null,
1322
+ partialBackfills,
1108
1323
  status.searchIndexMaintenance?.active ?? false,
1109
1324
  status.searchIndexMaintenance?.currentAgent ?? null,
1110
1325
  status.searchIndexMaintenance?.pendingAgents ?? [],
@@ -1281,10 +1496,22 @@ function createApiRoutes(scanSource, eventSource, options = {}) {
1281
1496
  }
1282
1497
  api.get("/agents", (c) => handleGetAgents(c, scanSource, listDefaults));
1283
1498
  api.get("/projects", (c) => handleGetProjects(c, scanSource, listDefaults));
1284
- api.get("/sessions", (c) => handleGetSessions(c, scanSource, listDefaults));
1285
- api.get("/search", (c) => handleSearchSessions(c, scanSource, listDefaults));
1286
- api.get("/file-activity", (c) => handleGetFileActivity(c, listDefaults));
1287
- api.get("/sessions/:agent/:id", (c) => handleGetSessionData(c, scanSource));
1499
+ api.get(
1500
+ "/sessions",
1501
+ (c) => handleGetSessions(c, scanSource, listDefaults, options.projectIdentityResolver)
1502
+ );
1503
+ api.get(
1504
+ "/search",
1505
+ (c) => handleSearchSessions(c, scanSource, listDefaults, options.projectIdentityResolver)
1506
+ );
1507
+ api.get(
1508
+ "/file-activity",
1509
+ (c) => handleGetFileActivity(c, listDefaults, options.projectIdentityResolver)
1510
+ );
1511
+ api.get(
1512
+ "/sessions/:agent/:id",
1513
+ (c) => handleGetSessionData(c, scanSource, options.projectIdentityResolver)
1514
+ );
1288
1515
  api.get("/dashboard", (c) => handleGetDashboard(c, scanSource, listDefaults));
1289
1516
  api.get("/bookmarks", (c) => handleGetBookmarks(c, scanSource));
1290
1517
  api.put("/bookmarks", (c) => handlePutBookmark(c));
@@ -1302,6 +1529,124 @@ function createApiRoutes(scanSource, eventSource, options = {}) {
1302
1529
  return api;
1303
1530
  }
1304
1531
 
1532
+ // src/project-identity-resolver.ts
1533
+ import { Worker } from "worker_threads";
1534
+ var PROJECT_IDENTITY_RESOLVER_MAX_WORKERS = 2;
1535
+ var SHUTDOWN_ERROR_MESSAGE = "Project identity resolver shut down";
1536
+ function toError(error) {
1537
+ return error instanceof Error ? error : new Error(String(error));
1538
+ }
1539
+ function isWorkerMessage(message) {
1540
+ return typeof message === "object" && message !== null && "type" in message && (message.type === "resolved" && "requestId" in message && "projection" in message || message.type === "failed" && "requestId" in message && "error" in message);
1541
+ }
1542
+ var ThreadProjectIdentityResolver = class {
1543
+ constructor(workerUrl, maxWorkers = PROJECT_IDENTITY_RESOLVER_MAX_WORKERS) {
1544
+ this.workerUrl = workerUrl;
1545
+ this.maxWorkers = maxWorkers;
1546
+ }
1547
+ workerUrl;
1548
+ maxWorkers;
1549
+ workers = /* @__PURE__ */ new Set();
1550
+ queue = [];
1551
+ nextRequestId = 1;
1552
+ isShuttingDown = false;
1553
+ resolve(cwd) {
1554
+ if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE));
1555
+ return new Promise((resolve4, reject) => {
1556
+ this.queue.push({ requestId: this.nextRequestId++, cwd, resolve: resolve4, reject });
1557
+ this.pump();
1558
+ });
1559
+ }
1560
+ async shutdown() {
1561
+ if (this.isShuttingDown) return;
1562
+ this.isShuttingDown = true;
1563
+ const error = new Error(SHUTDOWN_ERROR_MESSAGE);
1564
+ for (const request of this.queue.splice(0)) request.reject(error);
1565
+ const workers = [...this.workers];
1566
+ this.workers.clear();
1567
+ for (const slot of workers) {
1568
+ slot.closed = true;
1569
+ slot.pending?.reject(error);
1570
+ slot.pending = null;
1571
+ }
1572
+ await Promise.allSettled(workers.map(({ worker }) => worker.terminate()));
1573
+ }
1574
+ pump() {
1575
+ while (!this.isShuttingDown && this.queue.length > 0) {
1576
+ const slot = this.idleWorker() ?? this.createWorker();
1577
+ if (!slot) return;
1578
+ const request = this.queue.shift();
1579
+ if (!request) return;
1580
+ slot.pending = request;
1581
+ try {
1582
+ slot.worker.postMessage({
1583
+ type: "resolve",
1584
+ requestId: request.requestId,
1585
+ cwd: request.cwd
1586
+ });
1587
+ } catch (error) {
1588
+ this.closeWorker(slot, toError(error));
1589
+ }
1590
+ }
1591
+ }
1592
+ idleWorker() {
1593
+ for (const slot of this.workers) {
1594
+ if (!slot.closed && slot.pending == null) return slot;
1595
+ }
1596
+ return null;
1597
+ }
1598
+ createWorker() {
1599
+ if (this.workers.size >= this.maxWorkers) return null;
1600
+ let worker;
1601
+ try {
1602
+ worker = new Worker(this.workerUrl);
1603
+ } catch (error) {
1604
+ this.rejectQueued(toError(error));
1605
+ return null;
1606
+ }
1607
+ const slot = { worker, pending: null, closed: false };
1608
+ this.workers.add(slot);
1609
+ worker.unref();
1610
+ worker.on("message", (message) => {
1611
+ if (appLogger.consumeWorkerMessage(message)) return;
1612
+ this.handleWorkerMessage(slot, message);
1613
+ });
1614
+ worker.on("error", (error) => this.closeWorker(slot, toError(error)));
1615
+ worker.on("exit", (code) => {
1616
+ if (!slot.closed) {
1617
+ this.closeWorker(slot, new Error(`Project identity worker exited with code ${code}`));
1618
+ }
1619
+ });
1620
+ return slot;
1621
+ }
1622
+ handleWorkerMessage(slot, message) {
1623
+ if (!isWorkerMessage(message) || slot.closed) return;
1624
+ const request = slot.pending;
1625
+ if (!request || message.requestId !== request.requestId) return;
1626
+ slot.pending = null;
1627
+ if (message.type === "resolved") request.resolve(message.projection);
1628
+ else request.reject(new Error(message.error));
1629
+ this.pump();
1630
+ }
1631
+ closeWorker(slot, error) {
1632
+ if (slot.closed) return;
1633
+ slot.closed = true;
1634
+ this.workers.delete(slot);
1635
+ const request = slot.pending;
1636
+ slot.pending = null;
1637
+ request?.reject(error);
1638
+ if (!this.isShuttingDown) this.pump();
1639
+ }
1640
+ rejectQueued(error) {
1641
+ for (const request of this.queue.splice(0)) request.reject(error);
1642
+ }
1643
+ };
1644
+ function createProjectIdentityResolver() {
1645
+ return new ThreadProjectIdentityResolver(
1646
+ new URL("./project-identity-worker.js", import.meta.url)
1647
+ );
1648
+ }
1649
+
1305
1650
  // src/loopback-authority.ts
1306
1651
  import { isIP } from "net";
1307
1652
  var LOOPBACK_AUTHORITY_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
@@ -1366,7 +1711,6 @@ function validateLoopbackAuthority(rawHeaders, listenerHostname, listenerPort) {
1366
1711
  }
1367
1712
 
1368
1713
  // src/loopback-write-guard.ts
1369
- var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
1370
1714
  var JSON_BODY_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
1371
1715
  var ALLOWED_FETCH_SITES = /* @__PURE__ */ new Set(["same-origin", "none"]);
1372
1716
  function mediaType(contentType) {
@@ -1381,7 +1725,6 @@ function isSameOrigin(origin, requestOrigin) {
1381
1725
  }
1382
1726
  function evaluateLoopbackWriteRequest(request) {
1383
1727
  const method = request.method.toUpperCase();
1384
- if (SAFE_METHODS.has(method)) return { allowed: true };
1385
1728
  if (JSON_BODY_METHODS.has(method) && mediaType(request.contentType) !== "application/json") {
1386
1729
  return { allowed: false, reason: "content-type", status: 415 };
1387
1730
  }
@@ -1404,12 +1747,12 @@ function loopbackWriteGuard() {
1404
1747
  requestOrigin: url.origin
1405
1748
  });
1406
1749
  if (!decision.allowed) {
1407
- appLogger.warn("http.loopback_write.rejected", {
1750
+ appLogger.warn("http.loopback_request.rejected", {
1408
1751
  method: c.req.method,
1409
1752
  path: url.pathname,
1410
1753
  reason: decision.reason
1411
1754
  });
1412
- const error = decision.reason === "content-type" ? "Write requests require application/json" : "Cross-origin write request rejected";
1755
+ const error = decision.reason === "content-type" ? "Write requests require application/json" : "Cross-origin API request rejected";
1413
1756
  return c.json({ error }, decision.status);
1414
1757
  }
1415
1758
  await next();
@@ -1572,6 +1915,7 @@ async function createServer(port, store, options = {}) {
1572
1915
  const remoteAccessToken = accessPolicy.authenticationRequired ? options.remoteAccessToken ?? (options.remoteAccess ? createRemoteAccessToken() : null) : null;
1573
1916
  const loopbackAuthorityEnabled = !accessPolicy.authenticationRequired;
1574
1917
  const shutdownController = new AbortController();
1918
+ const projectIdentityResolver = options.projectIdentityResolver ?? createProjectIdentityResolver();
1575
1919
  let actualPort = null;
1576
1920
  appLogger.info("server.access_policy", {
1577
1921
  bind_category: accessPolicy.bindCategory,
@@ -1667,7 +2011,8 @@ async function createServer(port, store, options = {}) {
1667
2011
  defaultSessionFrom: options.defaultSessionFrom,
1668
2012
  defaultSessionTo: options.defaultSessionTo,
1669
2013
  defaultSessionDays: options.defaultSessionDays,
1670
- shutdownSignal: shutdownController.signal
2014
+ shutdownSignal: shutdownController.signal,
2015
+ projectIdentityResolver
1671
2016
  };
1672
2017
  app.route("/api", createApiRoutes(store, store, routeOptions));
1673
2018
  const webDistPath = options.webDistPath ?? findWebDistPath();
@@ -1752,8 +2097,12 @@ async function createServer(port, store, options = {}) {
1752
2097
  appLogger.info("server.shutdown.phase", { phase: "http-closing", port: actualPort });
1753
2098
  await closeHttpServer(server, actualPort);
1754
2099
  } finally {
1755
- appLogger.info("server.shutdown.phase", { phase: "store-closing", port: actualPort });
1756
- await store.shutdown();
2100
+ try {
2101
+ await projectIdentityResolver.shutdown();
2102
+ } finally {
2103
+ appLogger.info("server.shutdown.phase", { phase: "store-closing", port: actualPort });
2104
+ await store.shutdown();
2105
+ }
1757
2106
  }
1758
2107
  appLogger.info("server.shutdown.phase", { phase: "complete", port: actualPort });
1759
2108
  }
@@ -1800,7 +2149,9 @@ var AgentOperationScheduler = class {
1800
2149
  state.timer = setTimeout(() => {
1801
2150
  state.timer = null;
1802
2151
  state.timerDeadline = 0;
1803
- void this.refresh(agentName);
2152
+ this.refresh(agentName).catch((error) => {
2153
+ appLogger.error("scan.refresh.uncaught", { agent: agentName, error });
2154
+ });
1804
2155
  }, effectiveDelayMs);
1805
2156
  }
1806
2157
  async refresh(agentName) {
@@ -1946,9 +2297,16 @@ var BackfillLifecycle = class {
1946
2297
  });
1947
2298
  return true;
1948
2299
  }
2300
+ recordCompletion(attempt, completion) {
2301
+ const current = this.matchingRunningAttempt(attempt);
2302
+ if (!current) return false;
2303
+ this.attempts.set(attempt.agentName, { ...current, completion: { ...completion } });
2304
+ return true;
2305
+ }
1949
2306
  complete(attempt, status) {
1950
- if (!this.matchingRunningAttempt(attempt)) return false;
1951
- this.attempts.set(attempt.agentName, { ...attempt, status });
2307
+ const current = this.matchingRunningAttempt(attempt);
2308
+ if (!current) return false;
2309
+ this.attempts.set(attempt.agentName, { ...current, status });
1952
2310
  return true;
1953
2311
  }
1954
2312
  cancelAll() {
@@ -1965,17 +2323,25 @@ var BackfillLifecycle = class {
1965
2323
  stateFor(agentName) {
1966
2324
  const state = this.attempts.get(agentName);
1967
2325
  if (!state) return void 0;
1968
- return state.status === "running" && state.progress ? { ...state, progress: { ...state.progress } } : { ...state };
2326
+ const completion = state.completion ? { completion: { ...state.completion } } : {};
2327
+ return state.status === "running" && state.progress ? { ...state, progress: { ...state.progress }, ...completion } : { ...state, ...completion };
1969
2328
  }
1970
2329
  status() {
1971
2330
  const states = [...this.attempts.values()];
1972
2331
  const queued = states.filter((attempt) => attempt.status === "queued").sort((left, right) => left.attemptId - right.attemptId);
1973
2332
  const running = states.find((attempt) => attempt.status === "running");
2333
+ const partialAgents = {};
2334
+ for (const attempt of states) {
2335
+ if (attempt.status === "committed" && attempt.completion?.completeness === "partial") {
2336
+ partialAgents[attempt.agentName] = { ...attempt.completion };
2337
+ }
2338
+ }
1974
2339
  const status = {
1975
2340
  active: running != null || queued.length > 0,
1976
2341
  pendingAgents: queued.map((attempt) => attempt.agentName),
1977
2342
  completedAgents: states.filter((attempt) => attempt.status === "committed").map((attempt) => attempt.agentName),
1978
- failedAgents: states.filter((attempt) => attempt.status === "failed").map((attempt) => attempt.agentName)
2343
+ failedAgents: states.filter((attempt) => attempt.status === "failed").map((attempt) => attempt.agentName),
2344
+ ...Object.keys(partialAgents).length > 0 ? { partialAgents } : {}
1979
2345
  };
1980
2346
  if (running) {
1981
2347
  status.currentAgent = running.agentName;
@@ -1995,6 +2361,7 @@ var LiveSessionIndex = class {
1995
2361
  agentsByName = /* @__PURE__ */ new Map();
1996
2362
  byAgent = {};
1997
2363
  sessions = [];
2364
+ cacheFailures = {};
1998
2365
  scanFailures = {};
1999
2366
  signatureCaches = /* @__PURE__ */ new Map();
2000
2367
  initialize(snapshot, options = {}) {
@@ -2007,6 +2374,7 @@ var LiveSessionIndex = class {
2007
2374
  (agent) => !options.allowedAgents || options.allowedAgents.has(agent.name.toLowerCase())
2008
2375
  );
2009
2376
  this.agentsByName = new Map(this.agents.map((agent) => [agent.name, agent]));
2377
+ this.cacheFailures = { ...snapshot.cacheFailures };
2010
2378
  this.scanFailures = { ...snapshot.scanFailures };
2011
2379
  this.byAgent = Object.fromEntries(
2012
2380
  this.agents.flatMap((agent) => {
@@ -2022,6 +2390,7 @@ var LiveSessionIndex = class {
2022
2390
  agents: this.agents,
2023
2391
  byAgent: this.byAgent,
2024
2392
  sessions: this.sessions,
2393
+ ...Object.keys(this.cacheFailures).length > 0 ? { cacheFailures: this.cacheFailures } : {},
2025
2394
  ...Object.keys(this.scanFailures).length > 0 ? { scanFailures: this.scanFailures } : {}
2026
2395
  };
2027
2396
  }
@@ -2029,6 +2398,7 @@ var LiveSessionIndex = class {
2029
2398
  return this.agentsByName.get(agentName);
2030
2399
  }
2031
2400
  commitAgentSessions(agentName, nextSessions, candidateChangedIds = []) {
2401
+ delete this.cacheFailures[agentName];
2032
2402
  delete this.scanFailures[agentName];
2033
2403
  const previousSessions = this.byAgent[agentName] ?? [];
2034
2404
  const previousGlobalSessions = this.sessions;
@@ -2083,76 +2453,13 @@ var LiveSessionIndex = class {
2083
2453
  }
2084
2454
  };
2085
2455
 
2086
- // src/latest-value-throttle.ts
2087
- var LatestValueThrottle = class {
2088
- constructor(intervalMs, emit) {
2089
- this.intervalMs = intervalMs;
2090
- this.emit = emit;
2091
- }
2092
- intervalMs;
2093
- emit;
2094
- timer = null;
2095
- phase = null;
2096
- pending;
2097
- hasPending = false;
2098
- push(value, phase) {
2099
- if (this.phase !== phase) {
2100
- this.flush();
2101
- this.phase = phase;
2102
- this.emit(value);
2103
- this.schedule();
2104
- return;
2105
- }
2106
- if (this.timer) {
2107
- this.pending = value;
2108
- this.hasPending = true;
2109
- return;
2110
- }
2111
- this.emit(value);
2112
- this.schedule();
2113
- }
2114
- flush() {
2115
- this.clearTimer();
2116
- if (!this.hasPending) return;
2117
- const pending = this.pending;
2118
- this.pending = void 0;
2119
- this.hasPending = false;
2120
- this.emit(pending);
2121
- }
2122
- cancel() {
2123
- this.dropPending();
2124
- this.phase = null;
2125
- }
2126
- schedule() {
2127
- this.timer = setTimeout(() => {
2128
- this.timer = null;
2129
- if (!this.hasPending) return;
2130
- const pending = this.pending;
2131
- this.pending = void 0;
2132
- this.hasPending = false;
2133
- this.emit(pending);
2134
- this.schedule();
2135
- }, this.intervalMs);
2136
- }
2137
- clearTimer() {
2138
- if (!this.timer) return;
2139
- clearTimeout(this.timer);
2140
- this.timer = null;
2141
- }
2142
- dropPending() {
2143
- this.clearTimer();
2144
- this.pending = void 0;
2145
- this.hasPending = false;
2146
- }
2147
- };
2148
-
2149
2456
  // src/search-index-job-runner.ts
2150
2457
  import { existsSync as existsSync2 } from "fs";
2151
2458
  import { fileURLToPath as fileURLToPath2 } from "url";
2152
- import { Worker } from "worker_threads";
2459
+ import { Worker as Worker2 } from "worker_threads";
2153
2460
 
2154
2461
  // src/errors.ts
2155
- function toError(value) {
2462
+ function toError2(value) {
2156
2463
  return value instanceof Error ? value : new Error(String(value));
2157
2464
  }
2158
2465
 
@@ -2322,7 +2629,7 @@ function changesJobFromPending(pending) {
2322
2629
  }
2323
2630
 
2324
2631
  // src/search-index-job-runner.ts
2325
- var SHUTDOWN_ERROR_MESSAGE = "Live scan store shut down";
2632
+ var SHUTDOWN_ERROR_MESSAGE2 = "Live scan store shut down";
2326
2633
  var SearchIndexJobRunner = class {
2327
2634
  worker = null;
2328
2635
  activeBatch = null;
@@ -2338,7 +2645,7 @@ var SearchIndexJobRunner = class {
2338
2645
  }
2339
2646
  enqueueIn(queue, context, jobs, onStarted) {
2340
2647
  if (jobs.length === 0) return Promise.resolve();
2341
- if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE));
2648
+ if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE2));
2342
2649
  const batchId = this.nextBatchId++;
2343
2650
  const completion = queue.enqueue(batchId, context, jobs, onStarted);
2344
2651
  if (this.activeBatch) {
@@ -2372,7 +2679,7 @@ var SearchIndexJobRunner = class {
2372
2679
  const worker = this.worker;
2373
2680
  this.activeBatch = null;
2374
2681
  this.worker = null;
2375
- const shutdownError = new Error(SHUTDOWN_ERROR_MESSAGE);
2682
+ const shutdownError = new Error(SHUTDOWN_ERROR_MESSAGE2);
2376
2683
  if (activeBatch) this.settle(activeBatch, shutdownError);
2377
2684
  this.pendingJobs.rejectAll(shutdownError);
2378
2685
  this.pendingMaintenanceJobs.rejectAll(shutdownError);
@@ -2391,14 +2698,14 @@ var SearchIndexJobRunner = class {
2391
2698
  appLogger.error("search_index.worker_start_listener_failed", {
2392
2699
  batch_id: batch.id,
2393
2700
  context: batch.context,
2394
- error: toError(error)
2701
+ error: toError2(error)
2395
2702
  });
2396
2703
  });
2397
2704
  this.startBatch(batch);
2398
2705
  }
2399
2706
  startBatch(batch) {
2400
2707
  if (this.isShuttingDown) {
2401
- this.settle(batch, new Error(SHUTDOWN_ERROR_MESSAGE));
2708
+ this.settle(batch, new Error(SHUTDOWN_ERROR_MESSAGE2));
2402
2709
  return;
2403
2710
  }
2404
2711
  const request = {
@@ -2413,7 +2720,7 @@ var SearchIndexJobRunner = class {
2413
2720
  try {
2414
2721
  existingWorker.postMessage(request);
2415
2722
  } catch (error) {
2416
- this.invalidateWorker(existingWorker, toError(error));
2723
+ this.invalidateWorker(existingWorker, toError2(error));
2417
2724
  }
2418
2725
  return;
2419
2726
  }
@@ -2431,9 +2738,9 @@ var SearchIndexJobRunner = class {
2431
2738
  });
2432
2739
  let worker;
2433
2740
  try {
2434
- worker = new Worker(workerUrl, { workerData: request });
2741
+ worker = new Worker2(workerUrl, { workerData: request });
2435
2742
  } catch (error) {
2436
- this.settle(batch, toError(error));
2743
+ this.settle(batch, toError2(error));
2437
2744
  this.startNextBatch();
2438
2745
  return;
2439
2746
  }
@@ -2458,15 +2765,24 @@ var SearchIndexJobRunner = class {
2458
2765
  agent: message.agentName,
2459
2766
  sessions: message.sessions
2460
2767
  });
2461
- this.finishBatch(
2462
- activeBatch,
2463
- new Error(
2464
- `Search index worker failed to persist ${message.stage} for ${message.agentName}`
2465
- )
2466
- );
2768
+ if (message.fatal) {
2769
+ this.finishBatch(
2770
+ activeBatch,
2771
+ new Error(
2772
+ `Search index worker failed to persist ${message.stage} for ${message.agentName}`
2773
+ )
2774
+ );
2775
+ }
2467
2776
  return;
2468
2777
  }
2469
2778
  if (message.type !== "done") return;
2779
+ if (message.failedAgents.length > 0) {
2780
+ appLogger.warn("search_index.batch_partial", {
2781
+ batch_id: activeBatch.id,
2782
+ context: message.context,
2783
+ failed_agents: message.failedAgents
2784
+ });
2785
+ }
2470
2786
  appLogger.info(`${message.context}.done`, {
2471
2787
  duration_ms: Math.round(message.durationMs),
2472
2788
  sessions: message.sessions
@@ -2478,7 +2794,7 @@ var SearchIndexJobRunner = class {
2478
2794
  context: this.activeBatch?.context ?? batch.context,
2479
2795
  error
2480
2796
  });
2481
- this.invalidateWorker(worker, toError(error));
2797
+ this.invalidateWorker(worker, toError2(error));
2482
2798
  });
2483
2799
  worker.on("exit", (code) => this.finishWorker(worker, code));
2484
2800
  }
@@ -2588,7 +2904,7 @@ var SearchIndexMaintenanceScheduler = class {
2588
2904
  this.failedAgents.add(agentName);
2589
2905
  appLogger.error("search_index.maintenance_failed", {
2590
2906
  agent: agentName,
2591
- error: toError(error)
2907
+ error: toError2(error)
2592
2908
  });
2593
2909
  }
2594
2910
  } finally {
@@ -2671,6 +2987,69 @@ var SearchIndexMaintenanceScheduler = class {
2671
2987
  }
2672
2988
  };
2673
2989
 
2990
+ // src/latest-value-throttle.ts
2991
+ var LatestValueThrottle = class {
2992
+ constructor(intervalMs, emit) {
2993
+ this.intervalMs = intervalMs;
2994
+ this.emit = emit;
2995
+ }
2996
+ intervalMs;
2997
+ emit;
2998
+ timer = null;
2999
+ phase = null;
3000
+ pending;
3001
+ hasPending = false;
3002
+ push(value, phase) {
3003
+ if (this.phase !== phase) {
3004
+ this.flush();
3005
+ this.phase = phase;
3006
+ this.emit(value);
3007
+ this.schedule();
3008
+ return;
3009
+ }
3010
+ if (this.timer) {
3011
+ this.pending = value;
3012
+ this.hasPending = true;
3013
+ return;
3014
+ }
3015
+ this.emit(value);
3016
+ this.schedule();
3017
+ }
3018
+ flush() {
3019
+ this.clearTimer();
3020
+ if (!this.hasPending) return;
3021
+ const pending = this.pending;
3022
+ this.pending = void 0;
3023
+ this.hasPending = false;
3024
+ this.emit(pending);
3025
+ }
3026
+ cancel() {
3027
+ this.dropPending();
3028
+ this.phase = null;
3029
+ }
3030
+ schedule() {
3031
+ this.timer = setTimeout(() => {
3032
+ this.timer = null;
3033
+ if (!this.hasPending) return;
3034
+ const pending = this.pending;
3035
+ this.pending = void 0;
3036
+ this.hasPending = false;
3037
+ this.emit(pending);
3038
+ this.schedule();
3039
+ }, this.intervalMs);
3040
+ }
3041
+ clearTimer() {
3042
+ if (!this.timer) return;
3043
+ clearTimeout(this.timer);
3044
+ this.timer = null;
3045
+ }
3046
+ dropPending() {
3047
+ this.clearTimer();
3048
+ this.pending = void 0;
3049
+ this.hasPending = false;
3050
+ }
3051
+ };
3052
+
2674
3053
  // src/scan-status-model.ts
2675
3054
  var ScanStatusModel = class {
2676
3055
  status = {
@@ -2702,6 +3081,16 @@ var ScanStatusModel = class {
2702
3081
  } else {
2703
3082
  delete backfill.progress;
2704
3083
  }
3084
+ if (this.status.backfill.partialAgents) {
3085
+ backfill.partialAgents = Object.fromEntries(
3086
+ Object.entries(this.status.backfill.partialAgents).map(([agentName, completion]) => [
3087
+ agentName,
3088
+ { ...completion }
3089
+ ])
3090
+ );
3091
+ } else {
3092
+ delete backfill.partialAgents;
3093
+ }
2705
3094
  return {
2706
3095
  type: "scan-status",
2707
3096
  ...this.status,
@@ -2847,7 +3236,7 @@ var ScanStatusModel = class {
2847
3236
  updatedAt: now
2848
3237
  });
2849
3238
  }
2850
- finishAgent(agentName, sessionCount) {
3239
+ finishAgent(agentName, sessionCount, completion = { completeness: "complete" }) {
2851
3240
  const pendingAgents = this.status.pendingAgents.filter((agent) => agent !== agentName);
2852
3241
  const scanningAgents = this.status.scanningAgents.filter((agent) => agent !== agentName);
2853
3242
  const completedAgents = [.../* @__PURE__ */ new Set([...this.status.completedAgents, agentName])];
@@ -2860,6 +3249,7 @@ var ScanStatusModel = class {
2860
3249
  [agentName]: {
2861
3250
  agentName,
2862
3251
  status: "complete",
3252
+ ...completion,
2863
3253
  total,
2864
3254
  processed: total,
2865
3255
  sessions: sessionCount ?? previousStatus?.sessions ?? 0,
@@ -2926,6 +3316,7 @@ var ScanStatusModel = class {
2926
3316
  status.status === "failed" ? { ...status, completedAt: status.completedAt ?? now, updatedAt: now } : {
2927
3317
  ...status,
2928
3318
  status: "complete",
3319
+ completeness: status.completeness ?? "complete",
2929
3320
  completedAt: status.completedAt ?? now,
2930
3321
  updatedAt: now
2931
3322
  }
@@ -2943,7 +3334,15 @@ var ScanStatusModel = class {
2943
3334
  pendingAgents: [...backfill.pendingAgents],
2944
3335
  completedAgents: [...backfill.completedAgents],
2945
3336
  failedAgents: [...backfill.failedAgents],
2946
- progress: backfill.progress ? { ...backfill.progress } : void 0
3337
+ progress: backfill.progress ? { ...backfill.progress } : void 0,
3338
+ ...backfill.partialAgents ? {
3339
+ partialAgents: Object.fromEntries(
3340
+ Object.entries(backfill.partialAgents).map(([agentName, completion]) => [
3341
+ agentName,
3342
+ { ...completion }
3343
+ ])
3344
+ )
3345
+ } : {}
2947
3346
  },
2948
3347
  updatedAt: Date.now()
2949
3348
  });
@@ -2967,11 +3366,236 @@ var ScanStatusModel = class {
2967
3366
  })) {
2968
3367
  return "publishing";
2969
3368
  }
2970
- return this.status.phase === "initializing" ? "initializing" : "scanning";
2971
- }
2972
- set(status) {
2973
- this.status = status;
2974
- return this.snapshot();
3369
+ return this.status.phase === "initializing" ? "initializing" : "scanning";
3370
+ }
3371
+ set(status) {
3372
+ this.status = status;
3373
+ return this.snapshot();
3374
+ }
3375
+ };
3376
+
3377
+ // src/scan-status-reporter.ts
3378
+ var STATUS_PROGRESS_INTERVAL_MS = 100;
3379
+ var ScanStatusReporter = class {
3380
+ constructor(options) {
3381
+ this.options = options;
3382
+ }
3383
+ options;
3384
+ scanStatus = new ScanStatusModel();
3385
+ listeners = /* @__PURE__ */ new Set();
3386
+ progressThrottles = /* @__PURE__ */ new Map();
3387
+ shuttingDown = false;
3388
+ subscribe(listener) {
3389
+ this.listeners.add(listener);
3390
+ return () => this.listeners.delete(listener);
3391
+ }
3392
+ status() {
3393
+ return this.scanStatus.snapshot();
3394
+ }
3395
+ markShuttingDown() {
3396
+ this.shuttingDown = true;
3397
+ this.cancelProgressStatuses();
3398
+ }
3399
+ /** Record a failure without publishing (initialization-time bookkeeping). */
3400
+ recordAgentFailure(agentName, message, sessionCount) {
3401
+ this.scanStatus.failAgent(agentName, message, sessionCount);
3402
+ }
3403
+ failAgent(agentName, message) {
3404
+ this.flushProgressStatus(`scan:${agentName}`);
3405
+ this.publishStatus(this.scanStatus.failAgent(agentName, message));
3406
+ }
3407
+ updateSearchIndexMaintenance(status) {
3408
+ this.publishStatus(this.scanStatus.updateSearchIndexMaintenance(status));
3409
+ }
3410
+ startScanBatch(agentNames, phase) {
3411
+ const sessionCounts = Object.fromEntries(
3412
+ agentNames.map((agentName) => [agentName, this.options.sessionCount(agentName) ?? 0])
3413
+ );
3414
+ this.publishStatus(this.scanStatus.startBatch(agentNames, phase, sessionCounts));
3415
+ }
3416
+ setScanPhase(phase) {
3417
+ this.publishStatus(this.scanStatus.setPhase(phase));
3418
+ }
3419
+ beginAgentScan(agentName) {
3420
+ this.cancelProgressStatus(`scan:${agentName}`);
3421
+ if (!this.scanStatus.snapshot().active) this.startScanBatch([agentName], "scanning");
3422
+ this.publishStatus(
3423
+ this.scanStatus.beginAgent(agentName, this.options.sessionCount(agentName) ?? 0)
3424
+ );
3425
+ }
3426
+ updateAgentScanProgress(agentName, progress, backfillAttempt) {
3427
+ const backfills = this.options.backfills;
3428
+ if (backfillAttempt) {
3429
+ const key2 = `backfill:${agentName}`;
3430
+ const current = backfills.stateFor(agentName);
3431
+ if (current?.status !== "running" || current.attemptId !== backfillAttempt.attemptId) {
3432
+ return;
3433
+ }
3434
+ const currentPhase2 = current.progress?.phase ?? "scanning";
3435
+ const nextPhase2 = progress.phase ?? "scanning";
3436
+ if (currentPhase2 && currentPhase2 !== nextPhase2) this.flushProgressStatus(key2);
3437
+ if (backfills.updateProgress(backfillAttempt, {
3438
+ phase: progress.phase,
3439
+ total: progress.total,
3440
+ processed: progress.processed,
3441
+ sessions: progress.sessions
3442
+ })) {
3443
+ this.publishProgressStatus(
3444
+ key2,
3445
+ nextPhase2,
3446
+ this.scanStatus.updateBackfill(backfills.status())
3447
+ );
3448
+ }
3449
+ return;
3450
+ }
3451
+ const key = `scan:${agentName}`;
3452
+ const currentPhase = this.scanStatus.snapshot().agentStatuses[agentName]?.status;
3453
+ const nextPhase = progress.phase === "finalizing" ? "finalizing" : "scanning";
3454
+ if (currentPhase && currentPhase !== nextPhase) this.flushProgressStatus(key);
3455
+ const status = this.scanStatus.updateAgent(agentName, progress);
3456
+ this.publishProgressStatus(key, status?.agentStatuses[agentName]?.status ?? nextPhase, status);
3457
+ }
3458
+ beginAgentPublishing(agentName) {
3459
+ this.flushProgressStatus(`scan:${agentName}`);
3460
+ this.publishStatus(this.scanStatus.publishAgent(agentName));
3461
+ }
3462
+ queueAgentPublication(agentName) {
3463
+ this.flushProgressStatus(`scan:${agentName}`);
3464
+ this.publishStatus(this.scanStatus.queueAgentPublication(agentName));
3465
+ }
3466
+ finishAgentScan(agentName, completion) {
3467
+ this.flushProgressStatus(`scan:${agentName}`);
3468
+ this.publishStatus(
3469
+ this.scanStatus.finishAgent(agentName, this.options.sessionCount(agentName), completion)
3470
+ );
3471
+ }
3472
+ finishScanBatch() {
3473
+ this.flushProgressStatuses("scan:");
3474
+ this.publishStatus(this.scanStatus.finishBatch());
3475
+ }
3476
+ publishBackfillStatus() {
3477
+ this.publishStatus(this.scanStatus.updateBackfill(this.options.backfills.status()));
3478
+ }
3479
+ publishBackfillProgress(key, phase) {
3480
+ this.publishProgressStatus(
3481
+ key,
3482
+ phase,
3483
+ this.scanStatus.updateBackfill(this.options.backfills.status())
3484
+ );
3485
+ }
3486
+ flushProgressStatus(key) {
3487
+ const throttle = this.progressThrottles.get(key);
3488
+ if (!throttle) return;
3489
+ throttle.flush();
3490
+ throttle.cancel();
3491
+ this.progressThrottles.delete(key);
3492
+ }
3493
+ cancelProgressStatus(key) {
3494
+ const throttle = this.progressThrottles.get(key);
3495
+ if (!throttle) return;
3496
+ throttle.cancel();
3497
+ this.progressThrottles.delete(key);
3498
+ }
3499
+ flushProgressStatuses(prefix) {
3500
+ for (const key of this.progressThrottles.keys()) {
3501
+ if (key.startsWith(prefix)) this.flushProgressStatus(key);
3502
+ }
3503
+ }
3504
+ cancelProgressStatuses() {
3505
+ for (const throttle of this.progressThrottles.values()) throttle.cancel();
3506
+ this.progressThrottles.clear();
3507
+ }
3508
+ publishStatus(event) {
3509
+ if (!event || this.shuttingDown) return;
3510
+ for (const listener of this.listeners) {
3511
+ try {
3512
+ listener(event);
3513
+ } catch (error) {
3514
+ appLogger.error("scan.status_listener.error", { error });
3515
+ }
3516
+ }
3517
+ }
3518
+ publishProgressStatus(key, phase, event) {
3519
+ if (!event) return;
3520
+ let throttle = this.progressThrottles.get(key);
3521
+ if (!throttle) {
3522
+ throttle = new LatestValueThrottle(STATUS_PROGRESS_INTERVAL_MS, () => {
3523
+ this.publishStatus(this.scanStatus.snapshot());
3524
+ });
3525
+ this.progressThrottles.set(key, throttle);
3526
+ }
3527
+ throttle.push(void 0, phase);
3528
+ }
3529
+ };
3530
+
3531
+ // src/search-index-publisher.ts
3532
+ var SearchIndexPublisher = class {
3533
+ constructor(options) {
3534
+ this.options = options;
3535
+ }
3536
+ options;
3537
+ nextPublicationId = 1;
3538
+ buildFullSearchIndexJobs(context) {
3539
+ const snapshot = this.options.snapshot();
3540
+ return snapshot.agents.flatMap((agent) => {
3541
+ if (!(agent.name in snapshot.byAgent)) return [];
3542
+ const cached = this.options.readCachedSessions(agent.name);
3543
+ return [
3544
+ cached ? {
3545
+ kind: "full",
3546
+ context,
3547
+ agentName: agent.name,
3548
+ sessions: cached.sessions,
3549
+ meta: cached.meta,
3550
+ completeness: "partial",
3551
+ removedSessionIds: [],
3552
+ searchIndexOptions: { includePendingReindex: false }
3553
+ } : {
3554
+ kind: "full",
3555
+ context,
3556
+ agentName: agent.name,
3557
+ sessions: snapshot.byAgent[agent.name] ?? [],
3558
+ meta: buildAgentCacheMeta(agent),
3559
+ completeness: "partial",
3560
+ removedSessionIds: [],
3561
+ searchIndexOptions: { includePendingReindex: false }
3562
+ }
3563
+ ];
3564
+ });
3565
+ }
3566
+ publicationId(context, agentName) {
3567
+ const id = this.nextPublicationId++;
3568
+ return agentName ? `${context}:${agentName}:${id}` : `${context}:${id}`;
3569
+ }
3570
+ async commitSearchIndex(context, jobs, details) {
3571
+ appLogger.info("session.publication.prepared", {
3572
+ publication_id: details.publicationId,
3573
+ context,
3574
+ agent: details.agent,
3575
+ agents: details.agents,
3576
+ jobs: jobs.length
3577
+ });
3578
+ try {
3579
+ const publicationJobs = jobs.map((job) => ({
3580
+ ...job,
3581
+ publicationId: details.publicationId
3582
+ }));
3583
+ await (details.onStarted ? this.options.jobs.enqueue(context, publicationJobs, details.onStarted) : this.options.jobs.enqueue(context, publicationJobs));
3584
+ } catch (error) {
3585
+ appLogger.error("session.publication.failed", {
3586
+ publication_id: details.publicationId,
3587
+ context,
3588
+ agent: details.agent,
3589
+ stage: "search_index",
3590
+ error
3591
+ });
3592
+ throw error;
3593
+ }
3594
+ appLogger.info("session.publication.index_committed", {
3595
+ publication_id: details.publicationId,
3596
+ context,
3597
+ agent: details.agent
3598
+ });
2975
3599
  }
2976
3600
  };
2977
3601
 
@@ -2981,7 +3605,7 @@ var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
2981
3605
  var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
2982
3606
  var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2983
3607
  var CACHE_TRUNCATION_COVERAGE = 0.5;
2984
- var STATUS_PROGRESS_INTERVAL_MS = 100;
3608
+ var SOURCE_FAILURE_SUMMARY_MAX_LENGTH = 160;
2985
3609
  function buildPersistenceDiff(previousSessions, nextSessions, candidateChangedIds = [], completeness = "complete", explicitRemovedSessionIds = []) {
2986
3610
  const { changes, removedSessionIds } = computeSessionDiff(
2987
3611
  previousSessions,
@@ -2998,9 +3622,15 @@ function buildPersistenceDiff(previousSessions, nextSessions, candidateChangedId
2998
3622
  removedSessionIds: removedSessionIds.filter((sessionId) => explicitRemovals.has(sessionId))
2999
3623
  };
3000
3624
  }
3001
- function sourceFailureError(failures) {
3002
- const noun = failures.length === 1 ? "source" : "sources";
3003
- return new Error(`${failures.length} session ${noun} failed; last-known-good data retained`);
3625
+ function buildScanCompletion(completeness, sourceFailures) {
3626
+ if (sourceFailures.length === 0) return { completeness };
3627
+ const firstFailure = sourceFailures[0];
3628
+ const summary = `${firstFailure.errorClass}: ${firstFailure.message}`;
3629
+ return {
3630
+ completeness: "partial",
3631
+ sourceFailureCount: sourceFailures.length,
3632
+ sourceFailureSummary: summary.length > SOURCE_FAILURE_SUMMARY_MAX_LENGTH ? `${summary.slice(0, SOURCE_FAILURE_SUMMARY_MAX_LENGTH - 1)}\u2026` : summary
3633
+ };
3004
3634
  }
3005
3635
  function restoreAgentCacheMeta(agent, cached) {
3006
3636
  agent.setSessionMetaMap(new Map(Object.entries(cached.meta)));
@@ -3009,10 +3639,19 @@ var AgentSyncEngine = class {
3009
3639
  constructor(options) {
3010
3640
  this.options = options;
3011
3641
  this.scheduler = new AgentOperationScheduler((agentName) => this.performRefresh(agentName));
3642
+ this.statusReporter = new ScanStatusReporter({
3643
+ sessionCount: (agentName) => this.sessionIndex.snapshot().byAgent[agentName]?.length,
3644
+ backfills: this.backfills
3645
+ });
3012
3646
  this.searchIndexJobs = new SearchIndexJobRunner();
3647
+ this.indexPublisher = new SearchIndexPublisher({
3648
+ jobs: this.searchIndexJobs,
3649
+ snapshot: () => this.sessionIndex.snapshot(),
3650
+ readCachedSessions: (agentName) => this.readCachedSessionsOrWarn("search.index", agentName)
3651
+ });
3013
3652
  this.searchIndexMaintenance = new SearchIndexMaintenanceScheduler(
3014
3653
  this.searchIndexJobs,
3015
- (status) => this.publishStatus(this.scanStatus.updateSearchIndexMaintenance(status))
3654
+ (status) => this.statusReporter.updateSearchIndexMaintenance(status)
3016
3655
  );
3017
3656
  }
3018
3657
  options;
@@ -3022,12 +3661,10 @@ var AgentSyncEngine = class {
3022
3661
  backfills = new BackfillLifecycle();
3023
3662
  cacheIntegrityValidUntilByAgent = /* @__PURE__ */ new Map();
3024
3663
  sessionsChangedListeners = /* @__PURE__ */ new Set();
3025
- statusChangedListeners = /* @__PURE__ */ new Set();
3026
- statusProgressThrottles = /* @__PURE__ */ new Map();
3027
- scanStatus = new ScanStatusModel();
3664
+ statusReporter;
3665
+ indexPublisher;
3028
3666
  searchIndexJobs;
3029
3667
  searchIndexMaintenance;
3030
- nextPublicationId = 1;
3031
3668
  backgroundRefreshTimer = null;
3032
3669
  isShuttingDown = false;
3033
3670
  initialize(snapshot, options = {}) {
@@ -3040,7 +3677,7 @@ var AgentSyncEngine = class {
3040
3677
  );
3041
3678
  }
3042
3679
  for (const [agentName, failure] of Object.entries(snapshot.scanFailures ?? {})) {
3043
- this.scanStatus.failAgent(
3680
+ this.statusReporter.recordAgentFailure(
3044
3681
  agentName,
3045
3682
  `${failure.stage}: ${failure.message}`,
3046
3683
  snapshot.byAgent[agentName]?.length ?? 0
@@ -3051,20 +3688,19 @@ var AgentSyncEngine = class {
3051
3688
  return this.sessionIndex.snapshot();
3052
3689
  }
3053
3690
  status() {
3054
- return this.scanStatus.snapshot();
3691
+ return this.statusReporter.status();
3055
3692
  }
3056
3693
  subscribeSessionsChanged(listener) {
3057
3694
  this.sessionsChangedListeners.add(listener);
3058
3695
  return () => this.sessionsChangedListeners.delete(listener);
3059
3696
  }
3060
3697
  subscribeStatusChanged(listener) {
3061
- this.statusChangedListeners.add(listener);
3062
- return () => this.statusChangedListeners.delete(listener);
3698
+ return this.statusReporter.subscribe(listener);
3063
3699
  }
3064
3700
  async syncInitialIndex() {
3065
- const jobs = this.buildFullSearchIndexJobs("scan.initial");
3066
- await this.commitSearchIndex("scan.initial", jobs, {
3067
- publicationId: this.publicationId("scan.initial"),
3701
+ const jobs = this.indexPublisher.buildFullSearchIndexJobs("scan.initial");
3702
+ await this.indexPublisher.commitSearchIndex("scan.initial", jobs, {
3703
+ publicationId: this.indexPublisher.publicationId("scan.initial"),
3068
3704
  agents: jobs.map((job) => job.agentName)
3069
3705
  });
3070
3706
  for (const job of jobs) this.searchIndexMaintenance.enqueue(job.agentName);
@@ -3079,11 +3715,11 @@ var AgentSyncEngine = class {
3079
3715
  startBackgroundRefresh() {
3080
3716
  if (this.backgroundRefreshTimer) return;
3081
3717
  const agentNames = this.sessionIndex.snapshot().agents.map((agent) => agent.name);
3082
- this.startScanBatch(agentNames, "scanning");
3718
+ this.statusReporter.startScanBatch(agentNames, "scanning");
3083
3719
  this.backgroundRefreshTimer = setTimeout(() => {
3084
3720
  this.backgroundRefreshTimer = null;
3085
3721
  for (const agentName of agentNames) this.scheduler.schedule(agentName, 0);
3086
- if (agentNames.length === 0) this.finishScanBatch();
3722
+ if (agentNames.length === 0) this.statusReporter.finishScanBatch();
3087
3723
  }, 0);
3088
3724
  }
3089
3725
  async refresh(agentName) {
@@ -3108,7 +3744,7 @@ var AgentSyncEngine = class {
3108
3744
  }
3109
3745
  this.backfills.cancelAll();
3110
3746
  this.searchIndexMaintenance.stop();
3111
- this.cancelProgressStatuses();
3747
+ this.statusReporter.markShuttingDown();
3112
3748
  this.cacheIntegrityValidUntilByAgent.clear();
3113
3749
  const searchIndexSnapshot = this.searchIndexJobs.snapshot();
3114
3750
  appLogger.info("search_index.shutdown.started", {
@@ -3125,156 +3761,95 @@ var AgentSyncEngine = class {
3125
3761
  pending_batches: stoppedSearchIndexSnapshot.pendingBatches
3126
3762
  });
3127
3763
  }
3128
- startScanBatch(agentNames, phase) {
3129
- const snapshot = this.sessionIndex.snapshot();
3130
- const sessionCounts = Object.fromEntries(
3131
- agentNames.map((agentName) => [agentName, snapshot.byAgent[agentName]?.length ?? 0])
3132
- );
3133
- this.publishStatus(this.scanStatus.startBatch(agentNames, phase, sessionCounts));
3134
- }
3135
- setScanPhase(phase) {
3136
- this.publishStatus(this.scanStatus.setPhase(phase));
3137
- }
3138
- beginAgentScan(agentName) {
3139
- this.cancelProgressStatus(`scan:${agentName}`);
3140
- const snapshot = this.sessionIndex.snapshot();
3141
- if (!this.scanStatus.snapshot().active) this.startScanBatch([agentName], "scanning");
3142
- this.publishStatus(
3143
- this.scanStatus.beginAgent(agentName, snapshot.byAgent[agentName]?.length ?? 0)
3144
- );
3145
- }
3146
- updateAgentScanProgress(agentName, progress, backfillAttempt) {
3147
- if (backfillAttempt) {
3148
- const key2 = `backfill:${agentName}`;
3149
- const current = this.backfills.stateFor(agentName);
3150
- if (current?.status !== "running" || current.attemptId !== backfillAttempt.attemptId) {
3151
- return;
3152
- }
3153
- const currentPhase2 = current.progress?.phase ?? "scanning";
3154
- const nextPhase2 = progress.phase ?? "scanning";
3155
- if (currentPhase2 && currentPhase2 !== nextPhase2) this.flushProgressStatus(key2);
3156
- if (this.backfills.updateProgress(backfillAttempt, {
3157
- phase: progress.phase,
3158
- total: progress.total,
3159
- processed: progress.processed,
3160
- sessions: progress.sessions
3161
- })) {
3162
- this.publishProgressStatus(
3163
- key2,
3164
- nextPhase2,
3165
- this.scanStatus.updateBackfill(this.backfills.status())
3166
- );
3167
- }
3168
- return;
3169
- }
3170
- const key = `scan:${agentName}`;
3171
- const currentPhase = this.scanStatus.snapshot().agentStatuses[agentName]?.status;
3172
- const nextPhase = progress.phase === "finalizing" ? "finalizing" : "scanning";
3173
- if (currentPhase && currentPhase !== nextPhase) this.flushProgressStatus(key);
3174
- const status = this.scanStatus.updateAgent(agentName, progress);
3175
- this.publishProgressStatus(key, status?.agentStatuses[agentName]?.status ?? nextPhase, status);
3176
- }
3177
- beginAgentPublishing(agentName) {
3178
- this.flushProgressStatus(`scan:${agentName}`);
3179
- this.publishStatus(this.scanStatus.publishAgent(agentName));
3180
- }
3181
- queueAgentPublication(agentName) {
3182
- this.flushProgressStatus(`scan:${agentName}`);
3183
- this.publishStatus(this.scanStatus.queueAgentPublication(agentName));
3184
- }
3185
- finishAgentScan(agentName) {
3186
- this.flushProgressStatus(`scan:${agentName}`);
3187
- const count = this.sessionIndex.snapshot().byAgent[agentName]?.length;
3188
- this.publishStatus(this.scanStatus.finishAgent(agentName, count));
3189
- }
3190
- finishScanBatch() {
3191
- this.flushProgressStatuses("scan:");
3192
- this.publishStatus(this.scanStatus.finishBatch());
3193
- }
3194
- publishBackfillStatus() {
3195
- this.publishStatus(this.scanStatus.updateBackfill(this.backfills.status()));
3196
- }
3197
- publishStatus(event) {
3198
- if (!event || this.isShuttingDown) return;
3199
- for (const listener of this.statusChangedListeners) {
3764
+ emitSessionsChanged(change) {
3765
+ if (this.isShuttingDown) return;
3766
+ for (const listener of this.sessionsChangedListeners) {
3200
3767
  try {
3201
- listener(event);
3768
+ listener(change);
3202
3769
  } catch (error) {
3203
- appLogger.error("scan.status_listener.error", { error });
3770
+ this.reportPostCommitError("session.publication", change.agentName, error);
3204
3771
  }
3205
3772
  }
3206
3773
  }
3207
- publishProgressStatus(key, phase, event) {
3208
- if (!event) return;
3209
- let throttle = this.statusProgressThrottles.get(key);
3210
- if (!throttle) {
3211
- throttle = new LatestValueThrottle(STATUS_PROGRESS_INTERVAL_MS, () => {
3212
- this.publishStatus(this.scanStatus.snapshot());
3213
- });
3214
- this.statusProgressThrottles.set(key, throttle);
3215
- }
3216
- throttle.push(void 0, phase);
3217
- }
3218
- flushProgressStatus(key) {
3219
- const throttle = this.statusProgressThrottles.get(key);
3220
- if (!throttle) return;
3221
- throttle.flush();
3222
- throttle.cancel();
3223
- this.statusProgressThrottles.delete(key);
3224
- }
3225
- cancelProgressStatus(key) {
3226
- const throttle = this.statusProgressThrottles.get(key);
3227
- if (!throttle) return;
3228
- throttle.cancel();
3229
- this.statusProgressThrottles.delete(key);
3774
+ reportPostCommitError(operation, agentName, error) {
3775
+ appLogger.error(`${operation}.post_commit_error`, { agent: agentName, error });
3230
3776
  }
3231
- flushProgressStatuses(prefix) {
3232
- for (const key of this.statusProgressThrottles.keys()) {
3233
- if (key.startsWith(prefix)) this.flushProgressStatus(key);
3777
+ finishCommittedAgentScan(agentName, completion) {
3778
+ try {
3779
+ this.statusReporter.finishAgentScan(agentName, completion);
3780
+ } catch (error) {
3781
+ this.reportPostCommitError("scan.refresh", agentName, error);
3782
+ try {
3783
+ this.statusReporter.finishAgentScan(agentName, completion);
3784
+ } catch (recoveryError) {
3785
+ this.reportPostCommitError("scan.refresh", agentName, recoveryError);
3786
+ }
3234
3787
  }
3235
3788
  }
3236
- cancelProgressStatuses() {
3237
- for (const throttle of this.statusProgressThrottles.values()) throttle.cancel();
3238
- this.statusProgressThrottles.clear();
3239
- }
3240
- emitSessionsChanged(change) {
3241
- if (this.isShuttingDown) return;
3242
- for (const listener of this.sessionsChangedListeners) listener(change);
3243
- }
3244
3789
  async performRefresh(agentName) {
3245
- this.beginAgentScan(agentName);
3790
+ this.statusReporter.beginAgentScan(agentName);
3246
3791
  const startedAt = performance.now();
3247
3792
  let failed = false;
3793
+ let durableCommitted = false;
3248
3794
  let cached = null;
3249
- let result = null;
3795
+ let result = "failed";
3796
+ let completion = { completeness: "complete" };
3250
3797
  try {
3251
- if (this.findAgent(agentName)) cached = loadCachedSessions(agentName);
3252
- result = await this.runRefresh(agentName, cached, startedAt);
3253
- return result;
3798
+ if (this.findAgent(agentName))
3799
+ cached = this.readCachedSessionsOrWarn("scan.refresh", agentName);
3800
+ const refresh = await this.runRefresh(agentName, cached, startedAt, (committedCompletion) => {
3801
+ durableCommitted = true;
3802
+ completion = committedCompletion;
3803
+ result = "committed";
3804
+ });
3805
+ result = refresh.result;
3806
+ completion = refresh.completion;
3254
3807
  } catch (error) {
3255
- this.options.workerRunner.discard?.(agentName);
3256
- failed = true;
3257
- const failure = toError(error);
3258
- appLogger.error("scan.refresh.error", { agent: agentName, error });
3259
- console.error(`[${agentName}] Session refresh failed:`, error);
3260
- this.flushProgressStatus(`scan:${agentName}`);
3261
- this.publishStatus(this.scanStatus.failAgent(agentName, failure.message));
3262
- return "failed";
3263
- } finally {
3264
- if (!failed) this.finishAgentScan(agentName);
3808
+ if (durableCommitted) {
3809
+ this.reportPostCommitError("scan.refresh", agentName, error);
3810
+ result = "committed";
3811
+ } else {
3812
+ this.options.workerRunner.discard?.(agentName);
3813
+ failed = true;
3814
+ const failure = toError2(error);
3815
+ if (failure instanceof AgentUnavailableDuringScanError) {
3816
+ appLogger.warn("scan.refresh.worker_agent_unavailable", {
3817
+ agent: agentName,
3818
+ error: failure.message
3819
+ });
3820
+ } else {
3821
+ appLogger.error("scan.refresh.error", { agent: agentName, error });
3822
+ }
3823
+ console.error(`[${agentName}] Session refresh failed:`, error);
3824
+ this.statusReporter.failAgent(agentName, failure.message);
3825
+ }
3826
+ }
3827
+ try {
3828
+ if (!failed) {
3829
+ if (durableCommitted) this.finishCommittedAgentScan(agentName, completion);
3830
+ else this.statusReporter.finishAgentScan(agentName, completion);
3831
+ }
3832
+ } catch (error) {
3833
+ if (!durableCommitted) throw error;
3834
+ this.reportPostCommitError("scan.refresh", agentName, error);
3835
+ }
3836
+ try {
3265
3837
  const agent = this.findAgent(agentName);
3266
3838
  if (agent && this.needsBackfill(agent, cached, failed || result === "committed")) {
3267
3839
  this.enqueueBackfill(agentName);
3268
3840
  }
3269
3841
  if (agent) this.searchIndexMaintenance.enqueue(agentName);
3842
+ } catch (error) {
3843
+ appLogger.warn("scan.refresh.backfill_probe_failed", { agent: agentName, error });
3270
3844
  }
3845
+ return result;
3271
3846
  }
3272
- async runRefresh(agentName, cached, startedAt) {
3847
+ async runRefresh(agentName, cached, startedAt, onDurableCommit) {
3273
3848
  const pendingPathCount = this.scheduler.takePendingSignalCount(agentName);
3274
3849
  const agent = this.findAgent(agentName);
3275
3850
  if (!agent) {
3276
3851
  appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
3277
- return "skipped";
3852
+ return { result: "skipped", completion: { completeness: "complete" } };
3278
3853
  }
3279
3854
  const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
3280
3855
  const refreshBaseline = cached?.sessions ?? previousSessions;
@@ -3288,7 +3863,7 @@ var AgentSyncEngine = class {
3288
3863
  agent: agentName,
3289
3864
  state: "initialization"
3290
3865
  });
3291
- return "unchanged";
3866
+ return { result: "unchanged", completion: { completeness: "complete" } };
3292
3867
  }
3293
3868
  const isInitialized = initialization.value;
3294
3869
  const availabilityStartedAt = performance.now();
@@ -3313,7 +3888,10 @@ var AgentSyncEngine = class {
3313
3888
  }
3314
3889
  if (strategyResult.status === "unchanged") {
3315
3890
  this.options.workerRunner.commit?.(agentName);
3316
- return "unchanged";
3891
+ return {
3892
+ result: "unchanged",
3893
+ completion: buildScanCompletion(strategyResult.completeness, strategyResult.sourceFailures)
3894
+ };
3317
3895
  }
3318
3896
  const nextSessions = attachMissingProjectIdentities(strategyResult.nextSessions);
3319
3897
  const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
@@ -3355,7 +3933,12 @@ var AgentSyncEngine = class {
3355
3933
  saveCache: true,
3356
3934
  ...searchIndexOptions ? { searchIndexOptions } : {}
3357
3935
  };
3358
- this.queueAgentPublication(agentName);
3936
+ const completion = buildScanCompletion(
3937
+ strategyResult.completeness,
3938
+ strategyResult.sourceFailures
3939
+ );
3940
+ this.statusReporter.queueAgentPublication(agentName);
3941
+ let publicationCommitted = false;
3359
3942
  let publication;
3360
3943
  try {
3361
3944
  publication = await this.commitSessionPublication({
@@ -3364,12 +3947,18 @@ var AgentSyncEngine = class {
3364
3947
  sessions: nextSessions,
3365
3948
  candidateChangedIds: strategyResult.preciseChangedIds ?? [],
3366
3949
  indexJob: persistentJob,
3367
- onPublishing: () => this.beginAgentPublishing(agentName)
3950
+ onPublishing: () => this.statusReporter.beginAgentPublishing(agentName),
3951
+ onCommitted: () => {
3952
+ publicationCommitted = true;
3953
+ onDurableCommit(completion);
3954
+ }
3368
3955
  });
3369
3956
  } catch (error) {
3370
- agent.setSessionMetaMap(durableMeta);
3371
- if (durableLastRefreshAt == null) this.lastRefreshAtByAgent.delete(agentName);
3372
- else this.lastRefreshAtByAgent.set(agentName, durableLastRefreshAt);
3957
+ if (!publicationCommitted) {
3958
+ agent.setSessionMetaMap(durableMeta);
3959
+ if (durableLastRefreshAt == null) this.lastRefreshAtByAgent.delete(agentName);
3960
+ else this.lastRefreshAtByAgent.set(agentName, durableLastRefreshAt);
3961
+ }
3373
3962
  throw error;
3374
3963
  }
3375
3964
  this.options.workerRunner.commit?.(agentName);
@@ -3402,9 +3991,8 @@ var AgentSyncEngine = class {
3402
3991
  agent: agentName,
3403
3992
  failures: strategyResult.sourceFailures
3404
3993
  });
3405
- throw sourceFailureError(strategyResult.sourceFailures);
3406
3994
  }
3407
- return "committed";
3995
+ return { result: "committed", completion };
3408
3996
  }
3409
3997
  /**
3410
3998
  * An agent that cannot be reached was never scanned, so there is nothing to
@@ -3425,7 +4013,7 @@ var AgentSyncEngine = class {
3425
4013
  return this.refreshStrategyResult([], "partial", {});
3426
4014
  }
3427
4015
  async initializeAgent(agent, previousSessions) {
3428
- this.setScanPhase("initializing");
4016
+ this.statusReporter.setScanPhase("initializing");
3429
4017
  const scanStartedAt = performance.now();
3430
4018
  const scope = this.startupScanOptions();
3431
4019
  const result = await this.runWorker(agent, previousSessions, { kind: "full-scan" }, scope);
@@ -3483,12 +4071,13 @@ var AgentSyncEngine = class {
3483
4071
  { status: "unchanged", checkDuration }
3484
4072
  );
3485
4073
  }
3486
- this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
3487
4074
  if (!checkResult.hasChanges) {
3488
4075
  const scanStartedAt2 = performance.now();
3489
4076
  const result = await this.runWorker(agent, baseline, { kind: "recompute-derived" }, {});
3490
4077
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
3491
4078
  const sessions2 = attachMissingProjectIdentities(result.sessions);
4079
+ agent.commitChangeCheck();
4080
+ this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
3492
4081
  const persistenceDiff = buildPersistenceDiff(baseline, sessions2);
3493
4082
  if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0) {
3494
4083
  this.logUnchangedRefresh(agent.name, refreshStartedAt);
@@ -3521,6 +4110,8 @@ var AgentSyncEngine = class {
3521
4110
  const result = await this.runWorker(agent, baseline, { kind: "full-scan" }, scope);
3522
4111
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
3523
4112
  const sessions2 = attachMissingProjectIdentities(result.sessions);
4113
+ agent.commitChangeCheck();
4114
+ this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
3524
4115
  return this.refreshStrategyResult(sessions2, result.completeness, scope, {
3525
4116
  // Meta-only changes (e.g. a pricing capture epoch bump) leave the head
3526
4117
  // signature intact; without the worker-reported ids they would never
@@ -3543,6 +4134,8 @@ var AgentSyncEngine = class {
3543
4134
  agent.incrementalScan(baseline, preciseChangedIds, checkResult.refs, scope)
3544
4135
  )
3545
4136
  );
4137
+ agent.commitChangeCheck();
4138
+ this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
3546
4139
  const sourceFailures = checkResult.sourceFailures ?? [];
3547
4140
  const completeness = scope.from == null && scope.to == null && sourceFailures.length === 0 ? "complete" : "partial";
3548
4141
  return this.refreshStrategyResult(sessions, completeness, scope, {
@@ -3595,19 +4188,40 @@ var AgentSyncEngine = class {
3595
4188
  operation,
3596
4189
  scanOptions,
3597
4190
  meta: runOptions.meta ?? buildAgentCacheMeta(agent),
3598
- onProgress: (progress) => this.updateAgentScanProgress(agent.name, progress, runOptions.backfillAttempt),
4191
+ onProgress: (progress) => this.statusReporter.updateAgentScanProgress(
4192
+ agent.name,
4193
+ progress,
4194
+ runOptions.backfillAttempt
4195
+ ),
3599
4196
  onCheckpoint: runOptions.onCheckpoint
3600
4197
  });
3601
4198
  }
3602
4199
  handleBackfillCheckpoint(agentName, checkpoint) {
3603
4200
  if (checkpoint.stage !== "finalizing" || !checkpoint.backfillCursor) return;
3604
- markAgentFullSyncProgress(agentName, checkpoint.backfillCursor);
4201
+ if (!markAgentFullSyncProgress(agentName, checkpoint.backfillCursor)) {
4202
+ appLogger.warn("scan.backfill.checkpoint_not_durable", {
4203
+ agent: agentName,
4204
+ cursor: checkpoint.backfillCursor
4205
+ });
4206
+ return;
4207
+ }
3605
4208
  appLogger.debug("scan.backfill.checkpoint", {
3606
4209
  agent: agentName,
3607
4210
  cursor: checkpoint.backfillCursor,
3608
4211
  sessions: checkpoint.changes.length
3609
4212
  });
3610
4213
  }
4214
+ readCachedSessionsOrWarn(scope, agentName) {
4215
+ const outcome = readCachedSessions(agentName);
4216
+ if (outcome.status === "failed") {
4217
+ appLogger.warn(`${scope}.cache_state_unavailable`, {
4218
+ agent: agentName,
4219
+ state: "cached_sessions"
4220
+ });
4221
+ return null;
4222
+ }
4223
+ return outcome.value;
4224
+ }
3611
4225
  needsBackfill(agent, cached, reloadCached = false) {
3612
4226
  const startupScanOptions = this.startupScanOptions();
3613
4227
  if (startupScanOptions.from == null && startupScanOptions.to == null) return false;
@@ -3627,7 +4241,18 @@ var AgentSyncEngine = class {
3627
4241
  }
3628
4242
  if (!(agent instanceof FileSystemSessionSource)) return false;
3629
4243
  if (!agent.isAvailable()) return false;
3630
- const cachedSessions = reloadCached || cached === void 0 ? loadCachedSessions(agent.name) : cached;
4244
+ let cachedSessions = cached;
4245
+ if (reloadCached || cached === void 0) {
4246
+ const outcome = readCachedSessions(agent.name);
4247
+ if (outcome.status === "failed") {
4248
+ appLogger.warn("scan.backfill.cache_state_unavailable", {
4249
+ agent: agent.name,
4250
+ state: "cached_sessions"
4251
+ });
4252
+ return false;
4253
+ }
4254
+ cachedSessions = outcome.value;
4255
+ }
3631
4256
  const sourceCount = agent.listSessionSources().length;
3632
4257
  const cachedCount = cachedSessions?.sessions.length ?? 0;
3633
4258
  this.cacheIntegrityValidUntilByAgent.set(agent.name, lastSyncAt + BACKFILL_INTERVAL_MS);
@@ -3644,21 +4269,21 @@ var AgentSyncEngine = class {
3644
4269
  }
3645
4270
  enqueueBackfill(agentName) {
3646
4271
  if (this.isShuttingDown || !this.backfills.enqueue(agentName)) return;
3647
- this.publishBackfillStatus();
4272
+ this.statusReporter.publishBackfillStatus();
3648
4273
  this.pumpBackfillQueue();
3649
4274
  }
3650
4275
  pumpBackfillQueue() {
3651
4276
  if (this.isShuttingDown) return;
3652
4277
  const attempt = this.backfills.startNext();
3653
4278
  if (!attempt) return;
3654
- this.publishBackfillStatus();
4279
+ this.statusReporter.publishBackfillStatus();
3655
4280
  void this.runBackfill(attempt).then((result) => this.completeBackfillAttempt(attempt, result)).catch((error) => this.rejectBackfillAttempt(attempt, error));
3656
4281
  }
3657
4282
  completeBackfillAttempt(attempt, result) {
3658
4283
  if (this.isShuttingDown) return;
3659
4284
  const current = this.backfills.stateFor(attempt.agentName);
3660
4285
  if (current?.status === "running" && current.attemptId === attempt.attemptId) {
3661
- this.flushProgressStatus(`backfill:${attempt.agentName}`);
4286
+ this.statusReporter.flushProgressStatus(`backfill:${attempt.agentName}`);
3662
4287
  if (this.backfills.complete(attempt, result)) {
3663
4288
  if (result === "failed") {
3664
4289
  this.cacheIntegrityValidUntilByAgent.delete(attempt.agentName);
@@ -3668,7 +4293,7 @@ var AgentSyncEngine = class {
3668
4293
  Date.now() + BACKFILL_INTERVAL_MS
3669
4294
  );
3670
4295
  }
3671
- this.publishBackfillStatus();
4296
+ this.statusReporter.publishBackfillStatus();
3672
4297
  }
3673
4298
  }
3674
4299
  this.pumpBackfillQueue();
@@ -3686,14 +4311,19 @@ var AgentSyncEngine = class {
3686
4311
  const agent = this.findAgent(agentName);
3687
4312
  if (!agent || !agent.isAvailable()) return "skipped";
3688
4313
  const snapshot = this.sessionIndex.snapshot();
3689
- const cached = loadCachedSessions(agentName);
4314
+ const cached = this.readCachedSessionsOrWarn("scan.backfill", agentName);
3690
4315
  const baseline = cached?.sessions ?? snapshot.byAgent[agentName] ?? [];
3691
4316
  const meta = cached?.meta ?? buildAgentCacheMeta(agent);
3692
4317
  const backfillCursor = getAgentFullSyncCursor(agentName);
3693
4318
  if (cached) restoreAgentCacheMeta(agent, cached);
3694
4319
  let durableCommitted = false;
3695
4320
  try {
3696
- markAgentFullSyncStarted(agentName);
4321
+ if (!markAgentFullSyncStarted(agentName)) {
4322
+ appLogger.warn("scan.backfill.cache_state_unavailable", {
4323
+ agent: agentName,
4324
+ state: "full_sync_started"
4325
+ });
4326
+ }
3697
4327
  const operation = agent instanceof FileSystemSessionSource ? { kind: "source-backfill", cursor: backfillCursor, checkpoint: "durable" } : { kind: "full-backfill", cursor: backfillCursor, checkpoint: "durable" };
3698
4328
  appLogger.info("scan.backfill.started", {
3699
4329
  agent: agentName,
@@ -3713,21 +4343,18 @@ var AgentSyncEngine = class {
3713
4343
  );
3714
4344
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
3715
4345
  const fullSessions = attachMissingProjectIdentities(result.sessions);
3716
- this.flushProgressStatus(`backfill:${agentName}`);
4346
+ const completion = buildScanCompletion(result.completeness, result.sourceFailures ?? []);
4347
+ this.statusReporter.flushProgressStatus(`backfill:${agentName}`);
3717
4348
  const updatePublicationPhase = (phase) => {
3718
4349
  if (this.backfills.updateProgress(attempt, {
3719
4350
  phase,
3720
4351
  sessions: fullSessions.length
3721
4352
  })) {
3722
- this.publishProgressStatus(
3723
- `backfill:${agentName}`,
3724
- phase,
3725
- this.scanStatus.updateBackfill(this.backfills.status())
3726
- );
4353
+ this.statusReporter.publishBackfillProgress(`backfill:${agentName}`, phase);
3727
4354
  }
3728
4355
  };
3729
4356
  updatePublicationPhase("publish-queued");
3730
- await this.commitSessionPublication({
4357
+ const publication = await this.commitSessionPublication({
3731
4358
  context: "scan.backfill",
3732
4359
  agentName,
3733
4360
  sessions: fullSessions,
@@ -3742,92 +4369,54 @@ var AgentSyncEngine = class {
3742
4369
  removedSessionIds: result.explicitRemovedSessionIds,
3743
4370
  saveCache: true
3744
4371
  },
3745
- onPublishing: () => updatePublicationPhase("publishing")
4372
+ onPublishing: () => updatePublicationPhase("publishing"),
4373
+ onCommitted: () => {
4374
+ durableCommitted = true;
4375
+ if (completion.completeness === "partial") {
4376
+ this.backfills.recordCompletion(attempt, completion);
4377
+ }
4378
+ }
3746
4379
  });
3747
- durableCommitted = true;
4380
+ durableCommitted = publication.durableCommitted;
3748
4381
  this.options.workerRunner.commit?.(agentName);
3749
- if (result.sourceFailures?.length) throw sourceFailureError(result.sourceFailures);
3750
- markAgentFullSyncCompleted(agentName);
3751
- appLogger.info("scan.backfill.done", {
3752
- agent: agentName,
3753
- duration_ms: Math.round(performance.now() - startedAt),
3754
- sessions: fullSessions.length,
3755
- changed: result.changedIds?.length ?? 0
3756
- });
4382
+ if (!markAgentFullSyncCompleted(agentName)) {
4383
+ appLogger.warn("scan.backfill.completion_not_durable", { agent: agentName });
4384
+ }
4385
+ appLogger.info(
4386
+ result.sourceFailures?.length ? "scan.backfill.partial" : "scan.backfill.done",
4387
+ {
4388
+ agent: agentName,
4389
+ duration_ms: Math.round(performance.now() - startedAt),
4390
+ sessions: fullSessions.length,
4391
+ changed: result.changedIds?.length ?? 0,
4392
+ failed_sources: result.sourceFailures?.length ?? 0
4393
+ }
4394
+ );
4395
+ if (result.sourceFailures?.length) {
4396
+ appLogger.warn("scan.backfill.source_failures", {
4397
+ agent: agentName,
4398
+ failures: result.sourceFailures
4399
+ });
4400
+ }
3757
4401
  return "committed";
3758
4402
  } catch (error) {
3759
- if (!durableCommitted) agent.setSessionMetaMap(new Map(Object.entries(meta)));
4403
+ if (durableCommitted) {
4404
+ this.reportPostCommitError("scan.backfill", agentName, error);
4405
+ return "committed";
4406
+ }
4407
+ agent.setSessionMetaMap(new Map(Object.entries(meta)));
3760
4408
  this.options.workerRunner.discard?.(agentName);
3761
4409
  appLogger.error("scan.backfill.error", { agent: agentName, error });
3762
4410
  console.error(`[${agentName}] Backfill failed:`, error);
3763
4411
  return "failed";
3764
4412
  }
3765
4413
  }
3766
- buildFullSearchIndexJobs(context) {
3767
- const snapshot = this.sessionIndex.snapshot();
3768
- return snapshot.agents.flatMap((agent) => {
3769
- if (!(agent.name in snapshot.byAgent)) return [];
3770
- const cached = loadCachedSessions(agent.name);
3771
- return [
3772
- cached ? {
3773
- kind: "full",
3774
- context,
3775
- agentName: agent.name,
3776
- sessions: cached.sessions,
3777
- meta: cached.meta,
3778
- completeness: "partial",
3779
- removedSessionIds: [],
3780
- searchIndexOptions: { includePendingReindex: false }
3781
- } : {
3782
- kind: "full",
3783
- context,
3784
- agentName: agent.name,
3785
- sessions: snapshot.byAgent[agent.name] ?? [],
3786
- meta: buildAgentCacheMeta(agent),
3787
- completeness: "partial",
3788
- removedSessionIds: [],
3789
- searchIndexOptions: { includePendingReindex: false }
3790
- }
3791
- ];
3792
- });
3793
- }
3794
- publicationId(context, agentName) {
3795
- const id = this.nextPublicationId++;
3796
- return agentName ? `${context}:${agentName}:${id}` : `${context}:${id}`;
3797
- }
3798
- async commitSearchIndex(context, jobs, details) {
3799
- appLogger.info("session.publication.prepared", {
3800
- publication_id: details.publicationId,
3801
- context,
3802
- agent: details.agent,
3803
- agents: details.agents,
3804
- jobs: jobs.length
3805
- });
3806
- try {
3807
- const publicationJobs = jobs.map((job) => ({
3808
- ...job,
3809
- publicationId: details.publicationId
3810
- }));
3811
- await (details.onStarted ? this.searchIndexJobs.enqueue(context, publicationJobs, details.onStarted) : this.searchIndexJobs.enqueue(context, publicationJobs));
3812
- } catch (error) {
3813
- appLogger.error("session.publication.failed", {
3814
- publication_id: details.publicationId,
3815
- context,
3816
- agent: details.agent,
3817
- stage: "search_index",
3818
- error
3819
- });
3820
- throw error;
3821
- }
3822
- appLogger.info("session.publication.index_committed", {
3823
- publication_id: details.publicationId,
3824
- context,
3825
- agent: details.agent
3826
- });
3827
- }
3828
4414
  async commitSessionPublication(publication) {
3829
- const publicationId = this.publicationId(publication.context, publication.agentName);
3830
- await this.commitSearchIndex(publication.context, [publication.indexJob], {
4415
+ const publicationId = this.indexPublisher.publicationId(
4416
+ publication.context,
4417
+ publication.agentName
4418
+ );
4419
+ await this.indexPublisher.commitSearchIndex(publication.context, [publication.indexJob], {
3831
4420
  publicationId,
3832
4421
  agent: publication.agentName,
3833
4422
  ...publication.onPublishing ? { onStarted: publication.onPublishing } : {}
@@ -3839,6 +4428,8 @@ var AgentSyncEngine = class {
3839
4428
  publication.candidateChangedIds
3840
4429
  );
3841
4430
  const diffDuration = performance.now() - diffStartedAt;
4431
+ const result = { durableCommitted: true, event, diffDuration };
4432
+ publication.onCommitted?.(result);
3842
4433
  this.emitSessionsChanged({
3843
4434
  agentName: publication.agentName,
3844
4435
  sessions: this.sessionIndex.snapshot().byAgent[publication.agentName] ?? [],
@@ -3851,7 +4442,7 @@ var AgentSyncEngine = class {
3851
4442
  sessions: publication.sessions.length,
3852
4443
  has_event: event != null
3853
4444
  });
3854
- return { event, diffDuration };
4445
+ return result;
3855
4446
  }
3856
4447
  findAgent(agentName) {
3857
4448
  return this.sessionIndex.findAgent(agentName);
@@ -4158,7 +4749,11 @@ var SessionWatcher = class {
4158
4749
  emitAgentsChanged(agentNames) {
4159
4750
  if (agentNames.size === 0) return;
4160
4751
  for (const listener of this.listeners) {
4161
- listener(new Set(agentNames));
4752
+ try {
4753
+ listener(new Set(agentNames));
4754
+ } catch (error) {
4755
+ appLogger.error("watch.agents_listener.error", { error });
4756
+ }
4162
4757
  }
4163
4758
  }
4164
4759
  reportWatchError(event, data) {
@@ -4168,8 +4763,8 @@ var SessionWatcher = class {
4168
4763
  };
4169
4764
 
4170
4765
  // src/worker-runner.ts
4171
- import { Worker as Worker2 } from "worker_threads";
4172
- var SHUTDOWN_ERROR_MESSAGE2 = "Scan refresh worker shut down";
4766
+ import { Worker as Worker3 } from "worker_threads";
4767
+ var SHUTDOWN_ERROR_MESSAGE3 = "Scan refresh worker shut down";
4173
4768
  function applySessionChanges2(previousSessions, changes, removedSessionIds) {
4174
4769
  if (changes.length === 0 && removedSessionIds.length === 0) return previousSessions;
4175
4770
  const replacedIds = new Set(removedSessionIds);
@@ -4216,7 +4811,7 @@ var ThreadWorkerRunner = class {
4216
4811
  return count;
4217
4812
  }
4218
4813
  run(agentName, payload) {
4219
- if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE2));
4814
+ if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE3));
4220
4815
  const existingSlot = this.workers.get(agentName);
4221
4816
  if (existingSlot && (existingSlot.pending.size > 0 || existingSlot.awaitingCommit)) {
4222
4817
  return Promise.reject(new Error(`Scan refresh worker for ${agentName} is busy`));
@@ -4239,7 +4834,7 @@ var ThreadWorkerRunner = class {
4239
4834
  try {
4240
4835
  slot = this.createWorker(agentName, request);
4241
4836
  } catch (error) {
4242
- reject(toError(error));
4837
+ reject(toError2(error));
4243
4838
  return;
4244
4839
  }
4245
4840
  }
@@ -4256,7 +4851,7 @@ var ThreadWorkerRunner = class {
4256
4851
  slot.worker.postMessage(request);
4257
4852
  } catch (error) {
4258
4853
  slot.pending.delete(request.requestId);
4259
- reject(toError(error));
4854
+ reject(toError2(error));
4260
4855
  this.invalidateWorker(agentName, slot);
4261
4856
  }
4262
4857
  }
@@ -4287,7 +4882,7 @@ var ThreadWorkerRunner = class {
4287
4882
  this.isShuttingDown = true;
4288
4883
  const slots = [...this.workers.values()];
4289
4884
  this.workers.clear();
4290
- const shutdownError = new Error(SHUTDOWN_ERROR_MESSAGE2);
4885
+ const shutdownError = new Error(SHUTDOWN_ERROR_MESSAGE3);
4291
4886
  for (const slot of slots) {
4292
4887
  slot.closed = true;
4293
4888
  for (const pending of slot.pending.values()) pending.reject(shutdownError);
@@ -4296,7 +4891,7 @@ var ThreadWorkerRunner = class {
4296
4891
  await Promise.allSettled(slots.map((slot) => slot.worker.terminate()));
4297
4892
  }
4298
4893
  createWorker(agentName, request) {
4299
- const worker = new Worker2(this.workerUrl, { workerData: request });
4894
+ const worker = new Worker3(this.workerUrl, { workerData: request });
4300
4895
  const slot = {
4301
4896
  agentName,
4302
4897
  worker,
@@ -4312,7 +4907,7 @@ var ThreadWorkerRunner = class {
4312
4907
  this.handleMessage(slot, message);
4313
4908
  });
4314
4909
  worker.on("error", (error) => {
4315
- this.closeWorker(agentName, slot, toError(error));
4910
+ this.closeWorker(agentName, slot, toError2(error));
4316
4911
  });
4317
4912
  worker.on("exit", (code) => {
4318
4913
  if (slot.closed) return;
@@ -4345,14 +4940,16 @@ var ThreadWorkerRunner = class {
4345
4940
  pending.onCheckpoint?.(message.checkpoint);
4346
4941
  } catch (error) {
4347
4942
  slot.pending.delete(message.requestId);
4348
- pending.reject(toError(error));
4943
+ pending.reject(toError2(error));
4349
4944
  this.invalidateWorker(slot.agentName, slot);
4350
4945
  }
4351
4946
  return;
4352
4947
  }
4353
4948
  slot.pending.delete(message.requestId);
4354
4949
  if (message.type === "error") {
4355
- pending.reject(new Error(message.error));
4950
+ pending.reject(
4951
+ message.errorCode === AGENT_UNAVAILABLE_DURING_SCAN_ERROR_CODE ? new AgentUnavailableDuringScanError(slot.agentName) : new Error(message.error)
4952
+ );
4356
4953
  this.invalidateWorker(slot.agentName, slot);
4357
4954
  return;
4358
4955
  }
@@ -4378,7 +4975,7 @@ var ThreadWorkerRunner = class {
4378
4975
  };
4379
4976
  pending.resolve(result);
4380
4977
  } catch (error) {
4381
- pending.reject(toError(error));
4978
+ pending.reject(toError2(error));
4382
4979
  this.invalidateWorker(slot.agentName, slot);
4383
4980
  }
4384
4981
  }
@@ -4530,7 +5127,13 @@ var LiveScanStore = class {
4530
5127
  this.emitNow(event);
4531
5128
  }
4532
5129
  emitNow(event) {
4533
- for (const listener of this.listeners) listener(event);
5130
+ for (const listener of this.listeners) {
5131
+ try {
5132
+ listener(event);
5133
+ } catch (error) {
5134
+ appLogger.error("scan.sessions_listener.error", { error });
5135
+ }
5136
+ }
4534
5137
  }
4535
5138
  queueEvent(event) {
4536
5139
  this.pendingEvent = this.pendingEvent ? mergeSessionsUpdatedEvents(this.pendingEvent, event) : event;
@@ -4590,6 +5193,11 @@ function formatScanFailureDiagnostics(snapshot) {
4590
5193
  return `[${failure.agentName}] Scan failed during ${failure.stage}${source} (${failure.errorClass}): ${failure.message}`;
4591
5194
  });
4592
5195
  }
5196
+ function formatCacheFailureDiagnostics(snapshot) {
5197
+ return Object.values(snapshot.cacheFailures ?? {}).map(
5198
+ (failure) => `[${failure.agentName}] Cache persistence failed; serving in-memory results without advancing the durable baseline`
5199
+ );
5200
+ }
4593
5201
  function buildSessionIndexOutput(snapshot, window = {}) {
4594
5202
  const sessions = filterSessionTreeByActivityWindow(snapshot.sessions, window.from, window.to);
4595
5203
  const agents = getAgentInfoMap(
@@ -4683,6 +5291,10 @@ function startPricingRefresh(timeoutMs = REFRESH_TIMEOUT_MS) {
4683
5291
  }
4684
5292
 
4685
5293
  // src/index.ts
5294
+ process.on("unhandledRejection", (reason) => {
5295
+ appLogger.error("process.unhandled_rejection", { error: reason });
5296
+ throw reason;
5297
+ });
4686
5298
  var main = defineCommand({
4687
5299
  meta: {
4688
5300
  name: "codesesh",
@@ -4808,15 +5420,19 @@ var main = defineCommand({
4808
5420
  }
4809
5421
  appLogger.info("cli.start", {
4810
5422
  version: VERSION,
4811
- argv: process.argv.slice(2),
4812
5423
  port,
5424
+ host: hostname,
5425
+ transport: transport.kind,
5426
+ remote_access: remoteAccess,
4813
5427
  json: jsonOnly,
4814
5428
  no_open: noOpen,
4815
5429
  cache: useCache,
5430
+ clear_cache: clearCache,
5431
+ trace,
4816
5432
  log_path: appLogger.getLogPath()
4817
5433
  });
4818
5434
  if (clearCache) {
4819
- const { clearCache: clear } = await import("./dist-6QTGTYZT.js");
5435
+ const { clearCache: clear } = await import("./dist-RHDY2EZX.js");
4820
5436
  clear();
4821
5437
  appLogger.info("cache.clear");
4822
5438
  console.log("Cache cleared.");
@@ -4873,6 +5489,7 @@ var main = defineCommand({
4873
5489
  process.exitCode = 1;
4874
5490
  return;
4875
5491
  }
5492
+ for (const diagnostic of formatCacheFailureDiagnostics(result)) console.error(diagnostic);
4876
5493
  const output = buildSessionIndexOutput(result, { from: listDefaultFrom, to: listDefaultTo });
4877
5494
  appLogger.info("cli.json_output", {
4878
5495
  sessions: output.sessions.length,
@@ -4912,12 +5529,14 @@ var main = defineCommand({
4912
5529
  await app.shutdown();
4913
5530
  process.exit(0);
4914
5531
  };
4915
- process.once("SIGINT", (signal) => {
4916
- void shutdown(signal);
4917
- });
4918
- process.once("SIGTERM", (signal) => {
4919
- void shutdown(signal);
4920
- });
5532
+ const shutdownOnSignal = (signal) => {
5533
+ shutdown(signal).catch((error) => {
5534
+ appLogger.error("cli.shutdown_failed", { signal, error });
5535
+ process.exit(1);
5536
+ });
5537
+ };
5538
+ process.once("SIGINT", shutdownOnSignal);
5539
+ process.once("SIGTERM", shutdownOnSignal);
4921
5540
  console.log(` ${url}`);
4922
5541
  console.log("");
4923
5542
  appLogger.info("cli.ready", {