codesesh 1.0.4 → 1.0.5

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 (53) hide show
  1. package/README.md +4 -4
  2. package/dist/{chunk-MBZ4VV4C.js → chunk-FCYCBGEL.js} +3 -3
  3. package/dist/{chunk-W4PZHL2C.js → chunk-NBJJGH5P.js} +57 -39
  4. package/dist/chunk-NBJJGH5P.js.map +1 -0
  5. package/dist/chunk-PAP2Q3WV.js +1271 -0
  6. package/dist/chunk-PAP2Q3WV.js.map +1 -0
  7. package/dist/{chunk-BYB77UAJ.js → chunk-TS3IWH5U.js} +115 -72
  8. package/dist/chunk-TS3IWH5U.js.map +1 -0
  9. package/dist/{chunk-YCUN52HA.js → chunk-WUYSZAYR.js} +75 -114
  10. package/dist/chunk-WUYSZAYR.js.map +1 -0
  11. package/dist/{chunk-3SXCTDD6.js → chunk-XKZSX5BM.js} +4 -3
  12. package/dist/chunk-YQFEGFWD.js +89 -0
  13. package/dist/chunk-YQFEGFWD.js.map +1 -0
  14. package/dist/{discovery-K3GLISI6.js → discovery-D23RV7FY.js} +11 -5
  15. package/dist/index.js +1415 -948
  16. package/dist/index.js.map +1 -1
  17. package/dist/project-identity-worker.js +4 -4
  18. package/dist/scan-refresh-worker.js +17 -7
  19. package/dist/scan-refresh-worker.js.map +1 -1
  20. package/dist/search-index-worker.js +16 -8
  21. package/dist/search-index-worker.js.map +1 -1
  22. package/dist/session-detail-worker.js +79 -0
  23. package/dist/session-detail-worker.js.map +1 -0
  24. package/dist/smart-tag-worker.js +3 -3
  25. package/dist/web/assets/CustomTimeWindowDialog-C9XTZRm0.js +1 -0
  26. package/dist/web/assets/{InteractiveReceipt-Dk50TBsH.js → InteractiveReceipt-Sca1smpv.js} +1 -1
  27. package/dist/web/assets/OverviewScreen-Vq6BQR9_.js +1 -0
  28. package/dist/web/assets/{PrismHighlighter-B1a2dPJO.js → PrismHighlighter-LWITDffT.js} +1 -1
  29. package/dist/web/assets/Projects-CwicV3Dk.js +1 -0
  30. package/dist/web/assets/{SearchResultsPanel-DfheEwpz.js → SearchResultsPanel-DCO0Ak0c.js} +1 -1
  31. package/dist/web/assets/SessionDetail-mdmTQgo1.js +53 -0
  32. package/dist/web/assets/api-CXVT9XcZ.js +1 -0
  33. package/dist/web/assets/contract-BS78VBCV.js +2 -0
  34. package/dist/web/assets/{index-Bd-YkWkZ.css → index-C5cyw58A.css} +1 -1
  35. package/dist/web/assets/index-H2cT-6c6.js +1423 -0
  36. package/dist/web/assets/{session-indexes-BsgjUoc1.js → session-indexes-yyyWpKLf.js} +2 -2
  37. package/dist/web/index.html +6 -6
  38. package/package.json +1 -1
  39. package/dist/chunk-BYB77UAJ.js.map +0 -1
  40. package/dist/chunk-G4HRD3AR.js +0 -246
  41. package/dist/chunk-G4HRD3AR.js.map +0 -1
  42. package/dist/chunk-W4PZHL2C.js.map +0 -1
  43. package/dist/chunk-YCUN52HA.js.map +0 -1
  44. package/dist/web/assets/OverviewScreen-5hvQVp4H.js +0 -1
  45. package/dist/web/assets/Projects-Cy_v2yep.js +0 -1
  46. package/dist/web/assets/SessionDetail-DRI8R3st.js +0 -53
  47. package/dist/web/assets/api-GP9UCedC.js +0 -1
  48. package/dist/web/assets/contract-xJyW5u7a.js +0 -2
  49. package/dist/web/assets/index-DEjvzumm.js +0 -1410
  50. package/dist/web/assets/panel-ND0J63d3.js +0 -1
  51. /package/dist/{chunk-MBZ4VV4C.js.map → chunk-FCYCBGEL.js.map} +0 -0
  52. /package/dist/{chunk-3SXCTDD6.js.map → chunk-XKZSX5BM.js.map} +0 -0
  53. /package/dist/{discovery-K3GLISI6.js.map → discovery-D23RV7FY.js.map} +0 -0
package/dist/index.js CHANGED
@@ -4,22 +4,25 @@ import {
4
4
  buildDashboard,
5
5
  getSessionActivityTime,
6
6
  summarizeProjects
7
- } from "./chunk-MBZ4VV4C.js";
7
+ } from "./chunk-FCYCBGEL.js";
8
8
  import {
9
9
  AGENT_UNAVAILABLE_DURING_SCAN_ERROR_CODE,
10
10
  AgentUnavailableDuringScanError
11
11
  } from "./chunk-RF3EEWPH.js";
12
+ import {
13
+ terminateWorkerAfterLogDrain
14
+ } from "./chunk-YQFEGFWD.js";
12
15
  import "./chunk-QH2HOXJH.js";
13
16
  import {
14
17
  appLogger,
15
18
  logSearchIndexSync
16
- } from "./chunk-G4HRD3AR.js";
19
+ } from "./chunk-PAP2Q3WV.js";
17
20
  import {
18
21
  attachMissingProjectIdentities,
22
+ beginAgentRefresh,
19
23
  buildAgentCacheMeta,
20
24
  buildSessionPersistenceDiff,
21
25
  closeCacheStorage,
22
- commitAgentRefreshCheck,
23
26
  computeSessionDiff,
24
27
  filterIndexedSessionReferences,
25
28
  getAgentFullSyncCursor,
@@ -32,6 +35,8 @@ import {
32
35
  markAgentFullSyncCompleted,
33
36
  markAgentFullSyncProgress,
34
37
  markAgentFullSyncStarted,
38
+ matchesSessionQueryScope,
39
+ materializeCachedSessionDetailResponse,
35
40
  materializeSessionDetailResponse,
36
41
  mergeSearchQueryOptions,
37
42
  readAgentCacheInitialization,
@@ -42,22 +47,24 @@ import {
42
47
  scanSessions,
43
48
  searchFileActivitySessions,
44
49
  searchSessions,
45
- selectAgentRefresh,
46
50
  sessionMatchesSearchCost,
47
51
  sessionSignature,
48
52
  sortSessions
49
- } from "./chunk-BYB77UAJ.js";
53
+ } from "./chunk-TS3IWH5U.js";
50
54
  import {
51
55
  PROJECT_IDENTITY_RESOLVER_REVISION,
56
+ createProjectScopeMatcher,
52
57
  createProjectScopeMatcherFromIdentity,
53
58
  matchesProjectScope,
54
59
  normalizeProjectDirectory
55
- } from "./chunk-3SXCTDD6.js";
60
+ } from "./chunk-XKZSX5BM.js";
56
61
  import {
57
62
  createRegisteredAgents,
58
63
  getAgentInfoMap
59
- } from "./chunk-YCUN52HA.js";
64
+ } from "./chunk-WUYSZAYR.js";
60
65
  import {
66
+ CODESESH_OPERATION_ID_HEADER,
67
+ CODESESH_REQUEST_ID_HEADER,
61
68
  addCalendarDays,
62
69
  assertIdentifiedSessionHead,
63
70
  assertSessionIdentity,
@@ -78,6 +85,7 @@ import {
78
85
  mergeSortedSessions,
79
86
  normalizeSessionReference,
80
87
  openDb,
88
+ parseCalendarDayBoundary,
81
89
  perf,
82
90
  publishPendingPricing,
83
91
  refreshPricingCache,
@@ -88,162 +96,49 @@ import {
88
96
  tableExists,
89
97
  toPublicReferencedSessionHead,
90
98
  toPublicSessionHead
91
- } from "./chunk-W4PZHL2C.js";
99
+ } from "./chunk-NBJJGH5P.js";
92
100
 
93
101
  // src/index.ts
94
102
  import { defineCommand, runMain } from "citty";
95
103
 
104
+ // src/cli-exit.ts
105
+ var LOG_CLOSE_TIMEOUT_MS = 2e3;
106
+ var LOG_CLOSE_TIMEOUT_MESSAGE = "[codesesh] Timed out closing application log; forcing exit\n";
107
+ async function closeLoggerBeforeTermination(closeLogger, exitCode) {
108
+ let watchdog;
109
+ const timedOut = new Promise((resolve4) => {
110
+ watchdog = setTimeout(() => resolve4(false), LOG_CLOSE_TIMEOUT_MS);
111
+ });
112
+ try {
113
+ const closed = await Promise.race([closeLogger().then(() => true), timedOut]);
114
+ if (closed) return;
115
+ try {
116
+ process.stderr.write(LOG_CLOSE_TIMEOUT_MESSAGE);
117
+ } finally {
118
+ process.exit(exitCode);
119
+ }
120
+ } finally {
121
+ if (watchdog) clearTimeout(watchdog);
122
+ }
123
+ }
124
+
96
125
  // src/server.ts
97
126
  import { Hono as Hono2 } from "hono";
127
+ import { matchedRoutes } from "hono/route";
98
128
  import { bodyLimit } from "hono/body-limit";
99
129
  import { compress } from "hono/compress";
100
130
  import { secureHeaders } from "hono/secure-headers";
101
131
  import { serve } from "@hono/node-server";
102
132
  import { serveStatic } from "@hono/node-server/serve-static";
103
133
  import { existsSync } from "fs";
134
+ import { randomUUID as randomUUID3 } from "crypto";
104
135
  import { createServer as createHttpsServer } from "https";
105
136
  import { resolve, dirname } from "path";
106
137
  import { fileURLToPath } from "url";
107
138
 
108
139
  // src/api/routes.ts
109
140
  import { Hono } from "hono";
110
-
111
- // ../core/dist/runtime/search.mjs
112
- function executeSessionSearch(query, options, snapshot, context = {}) {
113
- const merged = mergeSearchQueryOptions(query, options);
114
- if (!needsIndexedSearch(merged.text, merged.options)) {
115
- return searchRecentSessions(snapshot, merged.options, context);
116
- }
117
- return searchIndexedSessions(query, merged.text, merged.parsed, merged.options);
118
- }
119
- function needsIndexedSearch(textQuery, options) {
120
- return Boolean(textQuery || options.file || options.fileKind || options.tools?.length);
121
- }
122
- function matchesRecentSearchFilters(session, options, projectScope, inclusiveCost) {
123
- if (options.projectKind || options.projectKey) {
124
- if (!options.projectKind || !options.projectKey || !matchesProjectIdentity(session.project_identity, {
125
- kind: options.projectKind,
126
- key: options.projectKey
127
- })) {
128
- return false;
129
- }
130
- }
131
- if (projectScope && !matchesProjectScope(session, projectScope)) return false;
132
- if (options.project) {
133
- const projectNeedle = options.project.toLowerCase();
134
- const projectText = [
135
- session.project_identity?.key,
136
- session.project_identity?.displayName,
137
- session.directory
138
- ].filter(Boolean).join("\n").toLowerCase();
139
- if (!projectText.includes(projectNeedle)) return false;
140
- }
141
- if (options.tags?.length && !options.tags.every((tag) => session.smart_tags?.includes(tag))) {
142
- return false;
143
- }
144
- if (!sessionMatchesSearchCost(session, options, inclusiveCost)) return false;
145
- return true;
146
- }
147
- function matchesSessionSearchFilters(agentName, session, options, projectScope = null, inclusiveCost = session.stats.total_cost) {
148
- if (options.agent && agentName !== options.agent) return false;
149
- const activity = getSessionActivityTime(session);
150
- if (options.from != null && activity < options.from) return false;
151
- if (options.to != null && activity > options.to) return false;
152
- return matchesRecentSearchFilters(session, options, projectScope, inclusiveCost);
153
- }
154
- function filterSessionSearchCandidates(candidates, options, context = {}) {
155
- const projectScope = options.projectScope ?? null;
156
- const sessionSnapshot = context.sessionSnapshot ?? candidates.map((candidate) => candidate.session);
157
- const inclusiveCosts = buildInclusiveCostLookup(sessionSnapshot, options, context.sessionTree);
158
- const headMatches = candidates.filter(
159
- (candidate) => matchesSessionSearchFilters(
160
- candidate.reference.agentName,
161
- candidate.session,
162
- options,
163
- projectScope,
164
- inclusiveCostFor(candidate.reference.agentName, candidate.session, inclusiveCosts)
165
- )
166
- );
167
- if (!options.file && !options.fileKind && !options.tools?.length) return headMatches;
168
- const indexedMatches = filterIndexedSessionReferences(
169
- headMatches.map((candidate) => ({
170
- agentName: candidate.reference.agentName,
171
- sessionId: candidate.reference.sessionId
172
- })),
173
- {
174
- file: options.file,
175
- fileKind: options.fileKind,
176
- tools: options.tools
177
- }
178
- );
179
- return headMatches.filter(
180
- (candidate) => indexedMatches.has(getSessionReferenceKey(candidate.reference))
181
- );
182
- }
183
- function searchRecentSessions(snapshot, options, context) {
184
- const limit = Math.max(0, Math.trunc(options.limit ?? 50));
185
- if (limit === 0) return [];
186
- const projectScope = options.projectScope ?? null;
187
- const inclusiveCosts = buildInclusiveCostLookup(snapshot.sessions, options, context.sessionTree);
188
- const sessions = options.agent ? snapshot.byAgent[options.agent] ?? [] : snapshot.sessions;
189
- const results = [];
190
- for (const session of sessions) {
191
- const agentName = options.agent ?? session.reference.agentName;
192
- if (!matchesSessionSearchFilters(
193
- agentName,
194
- session,
195
- options,
196
- projectScope,
197
- inclusiveCostFor(agentName, session, inclusiveCosts)
198
- )) {
199
- continue;
200
- }
201
- results.push({
202
- reference: session.reference,
203
- session,
204
- snippet: `Recent session \xB7 ${session.directory}`,
205
- snippetHighlights: [],
206
- matchType: "recent"
207
- });
208
- if (results.length >= limit) break;
209
- }
210
- return results;
211
- }
212
- function buildInclusiveCostLookup(sessions, options, sessionTree) {
213
- if (options.costMin == null && options.costMax == null) return null;
214
- return (sessionTree ?? buildSessionTree(sessions)).byRouteKey;
215
- }
216
- function inclusiveCostFor(agentName, session, lookup) {
217
- return lookup?.get(getSessionReferenceKey({ agentName, sessionId: session.reference.sessionId }))?.inclusiveStats.cost ?? session.stats.total_cost;
218
- }
219
- function deriveFileQuery(query, parsed, options) {
220
- return options.file ?? (!parsed.text ? parsed.filters.file : void 0) ?? (!parsed.hasQualifiers && query ? parsed.text || query : "");
221
- }
222
- function mergeSearchResultSources(results, limit) {
223
- const seen = /* @__PURE__ */ new Set();
224
- const merged = [];
225
- for (const result of results) {
226
- const key = getSessionReferenceKey(result.reference);
227
- if (seen.has(key)) continue;
228
- seen.add(key);
229
- merged.push(result);
230
- if (merged.length >= limit) break;
231
- }
232
- return merged;
233
- }
234
- function canSkipSessionsSearch(fileQuery, textQuery) {
235
- return Boolean(fileQuery && !textQuery);
236
- }
237
- function searchIndexedSessions(query, textQuery, parsed, options) {
238
- const fileQuery = deriveFileQuery(query, parsed, options);
239
- const fileResults = fileQuery ? searchFileActivitySessions(fileQuery, options) : [];
240
- const sessionResults = canSkipSessionsSearch(fileQuery, textQuery) ? [] : searchSessions(query, options);
241
- const textMatchReferences = textQuery && options.file ? new Set(sessionResults.map((result) => getSessionReferenceKey(result.reference))) : null;
242
- const matchingFileResults = textMatchReferences ? fileResults.filter(
243
- (result) => textMatchReferences.has(getSessionReferenceKey(result.reference))
244
- ) : fileResults;
245
- return mergeSearchResultSources([...matchingFileResults, ...sessionResults], options.limit ?? 50);
246
- }
141
+ import { randomUUID as randomUUID2 } from "crypto";
247
142
 
248
143
  // src/project-identity-resolver.ts
249
144
  import { Worker } from "worker_threads";
@@ -524,59 +419,6 @@ function createProjectIdentityResolver() {
524
419
  );
525
420
  }
526
421
 
527
- // src/time-window-resolution.ts
528
- var ROLLING_DAY_MS = 24 * 60 * 60 * 1e3;
529
- var DEFAULT_DASHBOARD_DAYS = 30;
530
- function resolveTimeWindow(request) {
531
- return request.mode === "cli" ? resolveCliWindow(request) : resolveDashboardWindow(request);
532
- }
533
- function resolveCliWindow(request) {
534
- const now = request.now ?? Date.now();
535
- const from = parseRequiredDate(request.from);
536
- const to = parseRequiredDate(request.to);
537
- if (from != null) return { from, to };
538
- const days = parseDays(request.days);
539
- if (days === 0) return { to, days };
540
- if (days == null || days < 0) return { to };
541
- const rollingFrom = now - days * ROLLING_DAY_MS;
542
- return Number.isFinite(rollingFrom) ? { from: rollingFrom, to, days } : { to };
543
- }
544
- function resolveDashboardWindow(request) {
545
- const now = request.now ?? Date.now();
546
- const to = request.window.to ?? now;
547
- const hasQueryDays = Boolean(request.days?.trim());
548
- const parsedDays = hasQueryDays ? parseDays(request.days) : void 0;
549
- let days = parsedDays != null && parsedDays > 0 ? parsedDays : request.defaultDays;
550
- if (request.window.hasExplicitFrom && request.window.from != null) {
551
- days ??= countCalendarDays(request.window.from, to);
552
- return { from: request.window.from, to, days };
553
- }
554
- if (parsedDays === 0 || !hasQueryDays && request.defaultDays === 0) {
555
- return { to, days: 0 };
556
- }
557
- if (request.window.from != null) {
558
- days ??= countCalendarDays(request.window.from, to);
559
- return { from: request.window.from, to, days };
560
- }
561
- const resolvedDays = days != null && days > 0 ? days : DEFAULT_DASHBOARD_DAYS;
562
- return {
563
- from: addCalendarDays(startOfCalendarDay(to), -(resolvedDays - 1)),
564
- to,
565
- days: resolvedDays
566
- };
567
- }
568
- function parseRequiredDate(value) {
569
- if (!value) return void 0;
570
- const timestamp = new Date(value).getTime();
571
- if (Number.isNaN(timestamp)) throw new Error(`Invalid date: ${value}`);
572
- return timestamp;
573
- }
574
- function parseDays(value) {
575
- if (value == null) return void 0;
576
- const days = Number.parseInt(value, 10);
577
- return Number.isSafeInteger(days) && Number.isSafeInteger(days * ROLLING_DAY_MS) ? days : void 0;
578
- }
579
-
580
422
  // src/api/query-params.ts
581
423
  var SEARCH_LIMIT_POLICY = { defaultValue: 50, maxValue: 100 };
582
424
  var FILE_ACTIVITY_LIMIT_POLICY = { defaultValue: 50, maxValue: 200 };
@@ -687,6 +529,49 @@ function filterSessionsByActivityWindow(sessions, from, to) {
687
529
  return filterSessionTreeByActivityWindow(sessions, from, to);
688
530
  }
689
531
 
532
+ // src/api/handler-support.ts
533
+ var KNOWN_AGENT_NAMES = getAgentInfoMap({}).map((agent) => agent.name);
534
+ var KNOWN_AGENT_NAME_SET = new Set(KNOWN_AGENT_NAMES);
535
+ async function resolveProjectScope(cwd, sessions, resolver, signal) {
536
+ const normalizedCwd = normalizeProjectDirectory(cwd);
537
+ const matchingSession = sessions.find(
538
+ (session) => normalizeProjectDirectory(session.directory) === normalizedCwd && session.project_identity_resolver_revision === PROJECT_IDENTITY_RESOLVER_REVISION && Boolean(session.project_identity_input_signature)
539
+ );
540
+ if (matchingSession) {
541
+ return createProjectScopeMatcherFromIdentity(cwd, matchingSession.project_identity);
542
+ }
543
+ if (!resolver) throw new Error("Project identity resolver is unavailable");
544
+ const projection = await resolver.resolve(cwd, signal);
545
+ return createProjectScopeMatcherFromIdentity(cwd, projection.identity);
546
+ }
547
+ function projectScopeResolutionFailureResponse(c, endpoint, error) {
548
+ if (error instanceof ProjectIdentityRequestAbortedError) throw error;
549
+ appLogger.warn("api.project_scope.unavailable", {
550
+ endpoint,
551
+ error: error instanceof Error ? error.message : String(error)
552
+ });
553
+ if (error instanceof ProjectIdentityQueueFullError) {
554
+ return c.json({ error: "Project scope busy; retry later" }, 429);
555
+ }
556
+ return c.json({ error: "Project scope unavailable" }, 503);
557
+ }
558
+ function reportInvalidQueryParameter(endpoint, parameter, validationOutcome) {
559
+ appLogger.warn("api.query_parameter.invalid", {
560
+ endpoint,
561
+ parameter,
562
+ validation_outcome: validationOutcome
563
+ });
564
+ }
565
+ function parseDateWindowRequest(c, endpoint, defaults) {
566
+ const outcome = parseDateWindow(searchParams(c), defaults);
567
+ if (outcome.kind === "valid") return outcome;
568
+ reportInvalidQueryParameter(endpoint, outcome.parameter, "rejected");
569
+ return {
570
+ kind: "rejected",
571
+ response: c.json({ error: `${outcome.parameter} ${outcome.error}` }, 400)
572
+ };
573
+ }
574
+
690
575
  // src/api/snapshot-pagination.ts
691
576
  import { createHash, randomUUID } from "crypto";
692
577
  var identityVersions = /* @__PURE__ */ new WeakMap();
@@ -790,14 +675,17 @@ function getDashboardCostFacts(source, sessions, from, to, cacheTo, analyticsRev
790
675
  }
791
676
  function getDashboardStorageAggregation(source, sessions, scope, from, to, cacheTo, analyticsRevision) {
792
677
  const build = () => ({
793
- recentFileActivities: listFileActivity({
794
- agent: scope.agent,
795
- projectKind: scope.projectKind,
796
- projectKey: scope.projectKey,
797
- from,
798
- to,
799
- limit: 12
800
- })
678
+ recentFileActivities: listFileActivity(
679
+ {
680
+ agent: scope.agent,
681
+ projectKind: scope.projectKind,
682
+ projectKey: scope.projectKey,
683
+ from,
684
+ to,
685
+ limit: 12
686
+ },
687
+ source.queryScope
688
+ )
801
689
  });
802
690
  const startedAt = performance.now();
803
691
  const log = (cache) => {
@@ -829,157 +717,309 @@ function getDashboardStorageAggregation(source, sessions, scope, from, to, cache
829
717
  );
830
718
  }
831
719
 
832
- // src/api/request-payloads.ts
833
- function isRecord(value) {
834
- return typeof value === "object" && value !== null;
720
+ // src/api/catalog-handlers.ts
721
+ function handleGetConfig(c, defaults) {
722
+ const payload = {
723
+ window: {
724
+ from: defaults.from,
725
+ to: defaults.to,
726
+ days: defaults.days
727
+ }
728
+ };
729
+ return c.json(payload);
835
730
  }
836
- function parseSessionReferencePayload(value) {
837
- if (!isRecord(value) || typeof value.agentName !== "string" || typeof value.sessionId !== "string" || !value.agentName.trim() || !value.sessionId) {
838
- return null;
839
- }
840
- return normalizeSessionReference({
841
- agentName: value.agentName.trim().toLowerCase(),
842
- sessionId: value.sessionId
843
- });
731
+ function handleGetScanStatus(c, scanSource) {
732
+ return c.json(scanSource.getScanStatus());
844
733
  }
845
- function parseBookmarkReference(value) {
846
- if (!isRecord(value)) return null;
847
- const reference = parseSessionReferencePayload(value.reference);
848
- if (reference) return reference;
849
- if (typeof value.agentKey !== "string" || typeof value.sessionId !== "string" || !value.agentKey.trim() || !value.sessionId) {
850
- return null;
851
- }
852
- return normalizeSessionReference({
853
- agentName: value.agentKey.trim().toLowerCase(),
854
- sessionId: value.sessionId
734
+ function handleGetAgents(c, scanSource, defaults = {}) {
735
+ const scanResult = scanSource.getSnapshot();
736
+ const window = parseDateWindowRequest(c, "agents", defaults);
737
+ if (window.kind === "rejected") return window.response;
738
+ const { from, to } = window;
739
+ const agents = getSnapshotAggregation(
740
+ scanSource,
741
+ scanResult.sessions,
742
+ ["agents", from, to],
743
+ () => {
744
+ const counts = Object.fromEntries(
745
+ Object.entries(scanResult.byAgent).map(([agentName, sessions]) => [
746
+ agentName,
747
+ filterSessionsByActivityWindow(sessions, from, to).length
748
+ ])
749
+ );
750
+ return getAgentInfoMap(counts);
751
+ }
752
+ );
753
+ return c.json(agents);
754
+ }
755
+ function handleGetProjects(c, scanSource, defaults = {}) {
756
+ const scanResult = scanSource.getSnapshot();
757
+ const params = searchParams(c);
758
+ const limit = parseLimit(params.get("limit"), PROJECT_PAGE_LIMIT_POLICY);
759
+ if (limit.kind === "invalid") {
760
+ reportInvalidQueryParameter("projects", "limit", "rejected");
761
+ return c.json({ error: limit.error }, 400);
762
+ }
763
+ const projectIdentity = parseProjectIdentityFilter(
764
+ c.req.query("projectKind"),
765
+ c.req.query("projectKey")
766
+ );
767
+ if (projectIdentity === null) {
768
+ return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
769
+ }
770
+ const window = parseDateWindowRequest(c, "projects", defaults);
771
+ if (window.kind === "rejected") return window.response;
772
+ const { from, to } = window;
773
+ const analyticsRevision = getAnalyticsRevision();
774
+ const catalog = getSnapshotAggregation(
775
+ scanSource,
776
+ scanResult.sessions,
777
+ ["projects", from, to, analyticsRevision],
778
+ () => {
779
+ const tree = getSnapshotSessionTree(scanSource, scanResult.sessions);
780
+ const costFacts = listDashboardCostFacts({ from, to, includeModelCosts: false });
781
+ const projects2 = attachProjectMetricsFromTree(
782
+ listCachedProjectGroups(scanResult.sessions),
783
+ tree,
784
+ from,
785
+ to,
786
+ costFacts
787
+ ).filter(
788
+ (project) => project.sessionCount > 0 || project.messages > 0 || project.tokens > 0 || project.cost > 0
789
+ );
790
+ return { projects: projects2, summary: summarizeProjects(projects2) };
791
+ }
792
+ );
793
+ const projects = projectIdentity ? catalog.projects.filter(
794
+ (project) => project.identityKind === projectIdentity.kind && project.identityKey === projectIdentity.key
795
+ ) : catalog.projects;
796
+ const page = paginateSnapshot(projects, {
797
+ cursor: params.get("cursor") ?? void 0,
798
+ limit: limit.value,
799
+ query: params,
800
+ snapshotIdentity: scanResult.sessions,
801
+ viewIdentity: catalog.projects
802
+ });
803
+ if (page.kind === "invalid_cursor") {
804
+ reportInvalidQueryParameter("projects", "cursor", "rejected");
805
+ return c.json({ error: "cursor is invalid for this request" }, 400);
806
+ }
807
+ if (page.kind === "stale_snapshot") {
808
+ return c.json({ error: "project snapshot changed; restart pagination" }, 409);
809
+ }
810
+ return c.json({
811
+ projects: page.items,
812
+ summary: projectIdentity ? summarizeProjects(projects) : catalog.summary,
813
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {}
855
814
  });
856
815
  }
857
- function parseBookmarkImport(value) {
858
- if (!isRecord(value)) return null;
859
- const reference = parseBookmarkReference(value);
860
- if (!reference) return null;
861
- const timestamp = value.bookmarkedAt ?? value.bookmarked_at;
862
- if (timestamp != null && (typeof timestamp !== "number" || !Number.isFinite(timestamp))) {
863
- return null;
816
+
817
+ // src/time-window-resolution.ts
818
+ var DAY_MS = 24 * 60 * 60 * 1e3;
819
+ var DEFAULT_DASHBOARD_DAYS = 30;
820
+ function resolveTimeWindow(request) {
821
+ return request.mode === "cli" ? resolveCliWindow(request) : resolveDashboardWindow(request);
822
+ }
823
+ function resolveCliWindow(request) {
824
+ const now = request.now ?? Date.now();
825
+ const from = parseRequiredDate(request.from, "start");
826
+ const to = parseRequiredDate(request.to, "end");
827
+ if (from != null && to != null && from > to) {
828
+ throw new Error("Invalid time window: from must not be after to");
829
+ }
830
+ if (from != null) return { from, to };
831
+ const days = parseDays(request.days);
832
+ if (days === 0) return { to, days };
833
+ if (days == null || days < 0) return { to };
834
+ const calendarFrom = addCalendarDays(startOfCalendarDay(to ?? now), -(days - 1));
835
+ return Number.isFinite(calendarFrom) ? { from: calendarFrom, to, days } : { to };
836
+ }
837
+ function resolveDashboardWindow(request) {
838
+ const now = request.now ?? Date.now();
839
+ const to = request.window.to ?? now;
840
+ const hasQueryDays = Boolean(request.days?.trim());
841
+ const parsedDays = hasQueryDays ? parseDays(request.days) : void 0;
842
+ if (request.window.hasExplicitFrom && request.window.from != null) {
843
+ return { from: request.window.from, to, days: countCalendarDays(request.window.from, to) };
844
+ }
845
+ const days = parsedDays ?? request.defaultDays;
846
+ if (days === 0) {
847
+ return { to, days: 0 };
848
+ }
849
+ if (days != null && days > 0) {
850
+ return {
851
+ from: addCalendarDays(startOfCalendarDay(to), -(days - 1)),
852
+ to,
853
+ days
854
+ };
855
+ }
856
+ if (request.window.from != null) {
857
+ return { from: request.window.from, to, days: countCalendarDays(request.window.from, to) };
864
858
  }
865
859
  return {
866
- reference,
867
- bookmarkedAt: typeof timestamp === "number" ? timestamp : Date.now()
860
+ from: addCalendarDays(startOfCalendarDay(to), -(DEFAULT_DASHBOARD_DAYS - 1)),
861
+ to,
862
+ days: DEFAULT_DASHBOARD_DAYS
868
863
  };
869
864
  }
870
- function sanitizeClientLogData(value) {
871
- if (!isRecord(value)) return {};
872
- return Object.fromEntries(
873
- Object.entries(value).slice(0, 30).map(([key, item]) => {
874
- if (typeof item === "string") return [key, item.slice(0, 300)];
875
- if (typeof item === "number" || typeof item === "boolean" || item == null) {
876
- return [key, item];
877
- }
878
- return [key, String(item).slice(0, 300)];
879
- })
880
- );
865
+ function parseRequiredDate(value, boundary) {
866
+ if (!value) return void 0;
867
+ const calendarBoundary = parseCalendarDayBoundary(value, boundary);
868
+ if (calendarBoundary === null) throw new Error(`Invalid date: ${value}`);
869
+ if (calendarBoundary != null) return calendarBoundary;
870
+ const timestamp = new Date(value).getTime();
871
+ if (Number.isNaN(timestamp)) throw new Error(`Invalid date: ${value}`);
872
+ return timestamp;
873
+ }
874
+ function parseDays(value) {
875
+ if (value == null) return void 0;
876
+ const normalized = value.trim();
877
+ if (!/^-?\d+$/.test(normalized)) return void 0;
878
+ const days = Number(normalized);
879
+ return Number.isSafeInteger(days) && Number.isSafeInteger(days * DAY_MS) ? days : void 0;
881
880
  }
882
881
 
883
- // src/api/session-detail-stream.ts
884
- var SESSION_DETAIL_STREAM_BATCH_CHARS = 64 * 1024;
885
- function createSessionDetailJsonResponse(data, messages) {
886
- const encoder = new TextEncoder();
887
- const headerJson = JSON.stringify(data);
888
- const headerPrefix = headerJson === "{}" ? '{"messages":[' : `${headerJson.slice(0, -1)},"messages":[`;
889
- const iterator = messages[Symbol.iterator]();
890
- let wroteHeader = false;
891
- let wroteMessage = false;
892
- return new Response(
893
- new ReadableStream({
894
- pull(controller) {
895
- if (!wroteHeader) {
896
- controller.enqueue(encoder.encode(headerPrefix));
897
- wroteHeader = true;
898
- return;
899
- }
900
- const batch = [];
901
- let batchLength = 0;
902
- let next;
903
- try {
904
- next = iterator.next();
905
- while (!next.done) {
906
- const prefix = wroteMessage ? "," : "";
907
- batch.push(prefix, next.value);
908
- batchLength += prefix.length + next.value.length;
909
- wroteMessage = true;
910
- if (batchLength >= SESSION_DETAIL_STREAM_BATCH_CHARS) break;
911
- next = iterator.next();
912
- }
913
- } catch (error) {
914
- try {
915
- iterator.return?.();
916
- } catch {
917
- }
918
- controller.error(error);
919
- return;
920
- }
921
- if (next.done) {
922
- batch.push("]}");
923
- controller.enqueue(encoder.encode(batch.join("")));
924
- controller.close();
925
- return;
926
- }
927
- controller.enqueue(encoder.encode(batch.join("")));
928
- },
929
- cancel() {
930
- iterator.return?.();
931
- }
932
- }),
933
- { headers: { "Content-Type": "application/json; charset=UTF-8" } }
882
+ // ../core/dist/runtime/search.mjs
883
+ function executeSessionSearch(query, options, snapshot, context = {}) {
884
+ const merged = mergeSearchQueryOptions(query, options);
885
+ if (!needsIndexedSearch(merged.text, merged.options)) {
886
+ return searchRecentSessions(snapshot, merged.options, context);
887
+ }
888
+ return searchIndexedSessions(
889
+ query,
890
+ merged.text,
891
+ merged.parsed,
892
+ merged.options,
893
+ context.queryScope
934
894
  );
935
895
  }
936
-
937
- // src/api/search-result-merge.ts
938
- function withParentContext(results, getSessionTree, aliases) {
939
- if (!results.some((result) => result.session.parent_reference)) return results;
940
- const byRouteKey = getSessionTree().byRouteKey;
941
- return results.map((result) => {
942
- const parentReference = result.session.parent_reference;
943
- if (!parentReference) return result;
944
- const parent = byRouteKey.get(formatSessionReference(parentReference))?.session;
945
- if (!parent) return result;
946
- const reference = normalizeSessionReference(parentReference);
947
- return { ...result, parent: { reference, title: aliases.get(reference) ?? parent.title } };
948
- });
896
+ function needsIndexedSearch(textQuery, options) {
897
+ return Boolean(textQuery || options.file || options.fileKind || options.tools?.length);
949
898
  }
950
- var ALIAS_SEARCH_RESULT_SHARE = 0.25;
951
- function searchResultKey(result) {
952
- return `${result.reference.agentName}\0${result.reference.sessionId}`;
899
+ function matchesRecentSearchFilters(session, options, projectScope, inclusiveCost) {
900
+ if (options.projectKind || options.projectKey) {
901
+ if (!options.projectKind || !options.projectKey || !matchesProjectIdentity(session.project_identity, {
902
+ kind: options.projectKind,
903
+ key: options.projectKey
904
+ })) {
905
+ return false;
906
+ }
907
+ }
908
+ if (projectScope && !matchesProjectScope(session, projectScope)) return false;
909
+ if (options.project) {
910
+ const projectNeedle = options.project.toLowerCase();
911
+ const projectText = [
912
+ session.project_identity?.key,
913
+ session.project_identity?.displayName,
914
+ session.directory
915
+ ].filter(Boolean).join("\n").toLowerCase();
916
+ if (!projectText.includes(projectNeedle)) return false;
917
+ }
918
+ if (options.tags?.length && !options.tags.every((tag) => session.smart_tags?.includes(tag))) {
919
+ return false;
920
+ }
921
+ if (!sessionMatchesSearchCost(session, options, inclusiveCost)) return false;
922
+ return true;
953
923
  }
954
- function uniqueSearchResults(results) {
955
- const seen = /* @__PURE__ */ new Set();
956
- return results.filter((result) => {
957
- const key = searchResultKey(result);
958
- if (seen.has(key)) return false;
959
- seen.add(key);
960
- return true;
961
- });
924
+ function matchesSessionSearchFilters(agentName, session, options, projectScope = null, inclusiveCost = session.stats.total_cost) {
925
+ if (options.agent && agentName !== options.agent) return false;
926
+ const activity = getSessionActivityTime(session);
927
+ if (options.from != null && activity < options.from) return false;
928
+ if (options.to != null && activity > options.to) return false;
929
+ return matchesRecentSearchFilters(session, options, projectScope, inclusiveCost);
962
930
  }
963
- function mergeAliasSearchResults(rankedResults, aliasResults, limit) {
964
- if (limit <= 0) return [];
965
- const ranked = uniqueSearchResults(rankedResults);
966
- const rankedKeys = new Set(ranked.map(searchResultKey));
967
- const aliases = uniqueSearchResults(aliasResults).filter(
968
- (result) => !rankedKeys.has(searchResultKey(result))
931
+ function filterSessionSearchCandidates(candidates, options, context = {}) {
932
+ const projectScope = options.projectScope ?? null;
933
+ const sessionSnapshot = context.sessionSnapshot ?? candidates.map((candidate) => candidate.session);
934
+ const inclusiveCosts = buildInclusiveCostLookup(sessionSnapshot, options, context.sessionTree);
935
+ const headMatches = candidates.filter(
936
+ (candidate) => matchesSessionQueryScope(candidate.session, context.queryScope) && matchesSessionSearchFilters(
937
+ candidate.reference.agentName,
938
+ candidate.session,
939
+ options,
940
+ projectScope,
941
+ inclusiveCostFor(candidate.reference.agentName, candidate.session, inclusiveCosts)
942
+ )
969
943
  );
970
- if (ranked.length === 0) return aliases.slice(0, limit);
971
- if (aliases.length === 0) return ranked.slice(0, limit);
972
- const aliasQuota = Math.min(
973
- limit - 1,
974
- Math.max(1, Math.floor(limit * ALIAS_SEARCH_RESULT_SHARE))
944
+ if (!options.file && !options.fileKind && !options.tools?.length) return headMatches;
945
+ const indexedMatches = filterIndexedSessionReferences(
946
+ headMatches.map((candidate) => ({
947
+ agentName: candidate.reference.agentName,
948
+ sessionId: candidate.reference.sessionId
949
+ })),
950
+ {
951
+ file: options.file,
952
+ fileKind: options.fileKind,
953
+ tools: options.tools
954
+ }
955
+ );
956
+ return headMatches.filter(
957
+ (candidate) => indexedMatches.has(getSessionReferenceKey(candidate.reference))
975
958
  );
976
- const rankedQuota = limit - aliasQuota;
977
- return [
978
- ...ranked.slice(0, rankedQuota),
979
- ...aliases.slice(0, aliasQuota),
980
- ...ranked.slice(rankedQuota),
981
- ...aliases.slice(aliasQuota)
982
- ].slice(0, limit);
959
+ }
960
+ function searchRecentSessions(snapshot, options, context) {
961
+ const limit = Math.max(0, Math.trunc(options.limit ?? 50));
962
+ if (limit === 0) return [];
963
+ const projectScope = options.projectScope ?? null;
964
+ const inclusiveCosts = buildInclusiveCostLookup(snapshot.sessions, options, context.sessionTree);
965
+ const sessions = options.agent ? snapshot.byAgent[options.agent] ?? [] : snapshot.sessions;
966
+ const results = [];
967
+ for (const session of sessions) {
968
+ const agentName = options.agent ?? session.reference.agentName;
969
+ if (!matchesSessionQueryScope(session, context.queryScope) || !matchesSessionSearchFilters(
970
+ agentName,
971
+ session,
972
+ options,
973
+ projectScope,
974
+ inclusiveCostFor(agentName, session, inclusiveCosts)
975
+ )) {
976
+ continue;
977
+ }
978
+ results.push({
979
+ reference: session.reference,
980
+ session,
981
+ snippet: `Recent session \xB7 ${session.directory}`,
982
+ snippetHighlights: [],
983
+ matchType: "recent"
984
+ });
985
+ if (results.length >= limit) break;
986
+ }
987
+ return results;
988
+ }
989
+ function buildInclusiveCostLookup(sessions, options, sessionTree) {
990
+ if (options.costMin == null && options.costMax == null) return null;
991
+ return (sessionTree ?? buildSessionTree(sessions)).byRouteKey;
992
+ }
993
+ function inclusiveCostFor(agentName, session, lookup) {
994
+ return lookup?.get(getSessionReferenceKey({ agentName, sessionId: session.reference.sessionId }))?.inclusiveStats.cost ?? session.stats.total_cost;
995
+ }
996
+ function deriveFileQuery(query, parsed, options) {
997
+ return options.file ?? (!parsed.text ? parsed.filters.file : void 0) ?? (!parsed.hasQualifiers && query ? parsed.text || query : "");
998
+ }
999
+ function mergeSearchResultSources(results, limit) {
1000
+ const seen = /* @__PURE__ */ new Set();
1001
+ const merged = [];
1002
+ for (const result of results) {
1003
+ const key = getSessionReferenceKey(result.reference);
1004
+ if (seen.has(key)) continue;
1005
+ seen.add(key);
1006
+ merged.push(result);
1007
+ if (merged.length >= limit) break;
1008
+ }
1009
+ return merged;
1010
+ }
1011
+ function canSkipSessionsSearch(fileQuery, textQuery) {
1012
+ return Boolean(fileQuery && !textQuery);
1013
+ }
1014
+ function searchIndexedSessions(query, textQuery, parsed, options, queryScope) {
1015
+ const fileQuery = deriveFileQuery(query, parsed, options);
1016
+ const fileResults = fileQuery ? searchFileActivitySessions(fileQuery, options, queryScope) : [];
1017
+ const sessionResults = canSkipSessionsSearch(fileQuery, textQuery) ? [] : searchSessions(query, options, queryScope);
1018
+ const textMatchReferences = textQuery && options.file ? new Set(sessionResults.map((result) => getSessionReferenceKey(result.reference))) : null;
1019
+ const matchingFileResults = textMatchReferences ? fileResults.filter(
1020
+ (result) => textMatchReferences.has(getSessionReferenceKey(result.reference))
1021
+ ) : fileResults;
1022
+ return mergeSearchResultSources([...matchingFileResults, ...sessionResults], options.limit ?? 50);
983
1023
  }
984
1024
 
985
1025
  // ../core/dist/runtime/state.mjs
@@ -1471,99 +1511,243 @@ function findAliasSearchResults(query, options, scanResult, aliases, context = {
1471
1511
  }
1472
1512
  return filterSessionSearchCandidates(results, search.options, {
1473
1513
  sessionSnapshot: scanResult.sessions,
1474
- sessionTree: context.sessionTree
1514
+ sessionTree: context.sessionTree,
1515
+ queryScope: context.queryScope
1475
1516
  }).sort((a, b) => getSessionActivityTime(b.session) - getSessionActivityTime(a.session));
1476
1517
  }
1477
1518
 
1478
- // src/api/handlers.ts
1479
- var KNOWN_AGENT_NAMES = getAgentInfoMap({}).map((agent) => agent.name);
1480
- var KNOWN_AGENT_NAME_SET = new Set(KNOWN_AGENT_NAMES);
1481
- async function resolveProjectScope(cwd, sessions, resolver, signal) {
1482
- const normalizedCwd = normalizeProjectDirectory(cwd);
1483
- const matchingSession = sessions.find(
1484
- (session) => normalizeProjectDirectory(session.directory) === normalizedCwd && session.project_identity_resolver_revision === PROJECT_IDENTITY_RESOLVER_REVISION && Boolean(session.project_identity_input_signature)
1519
+ // src/api/dashboard-handler.ts
1520
+ function handleGetDashboard(c, scanSource, defaults = {}) {
1521
+ const scanResult = scanSource.getSnapshot();
1522
+ const projectIdentity = parseProjectIdentityFilter(
1523
+ c.req.query("projectKind"),
1524
+ c.req.query("projectKey")
1485
1525
  );
1486
- if (matchingSession) {
1487
- return createProjectScopeMatcherFromIdentity(cwd, matchingSession.project_identity);
1488
- }
1489
- if (!resolver) throw new Error("Project identity resolver is unavailable");
1490
- const projection = await resolver.resolve(cwd, signal);
1491
- return createProjectScopeMatcherFromIdentity(cwd, projection.identity);
1492
- }
1493
- function reportProjectScopeResolutionFailure(endpoint, error) {
1494
- appLogger.warn("api.project_scope.unavailable", {
1495
- endpoint,
1496
- error: error instanceof Error ? error.message : String(error)
1497
- });
1498
- }
1499
- function projectScopeResolutionFailureResponse(c, endpoint, error) {
1500
- if (error instanceof ProjectIdentityRequestAbortedError) throw error;
1501
- reportProjectScopeResolutionFailure(endpoint, error);
1502
- if (error instanceof ProjectIdentityQueueFullError) {
1503
- return c.json({ error: "Project scope busy; retry later" }, 429);
1526
+ if (projectIdentity === null) {
1527
+ return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
1504
1528
  }
1505
- return c.json({ error: "Project scope unavailable" }, 503);
1506
- }
1507
- function reportInvalidQueryParameter(endpoint, parameter, validationOutcome) {
1508
- appLogger.warn("api.query_parameter.invalid", {
1509
- endpoint,
1510
- parameter,
1511
- validation_outcome: validationOutcome
1529
+ const dateWindow = parseDateWindowRequest(c, "dashboard", defaults);
1530
+ if (dateWindow.kind === "rejected") return dateWindow.response;
1531
+ const { from, to, days } = resolveTimeWindow({
1532
+ mode: "dashboard",
1533
+ window: dateWindow,
1534
+ days: c.req.query("days"),
1535
+ defaultDays: defaults.days
1512
1536
  });
1513
- }
1514
- function parseDateWindowRequest(c, endpoint, defaults) {
1515
- const outcome = parseDateWindow(searchParams(c), defaults);
1516
- if (outcome.kind === "valid") return outcome;
1517
- reportInvalidQueryParameter(endpoint, outcome.parameter, "rejected");
1518
- return {
1519
- kind: "rejected",
1520
- response: c.json({ error: `${outcome.parameter} ${outcome.error}` }, 400)
1537
+ const scope = {
1538
+ agent: optionalQueryValue(c.req.query("agent"))?.toLowerCase(),
1539
+ projectKind: projectIdentity?.kind,
1540
+ projectKey: projectIdentity?.key
1521
1541
  };
1522
- }
1523
- function getSessionHeadReference(session) {
1524
- return session.reference;
1525
- }
1526
- function handleGetConfig(c, defaults) {
1527
- const payload = {
1528
- window: {
1529
- from: defaults.from,
1530
- to: defaults.to,
1531
- days: defaults.days
1542
+ const compare = from == null ? void 0 : { from: addCalendarDays(from, -days), to: from - 1 };
1543
+ const fixedTo = dateWindow.to;
1544
+ const cacheTo = fixedTo ?? startOfCalendarDay(to);
1545
+ const analyticsRevision = getAnalyticsRevision();
1546
+ const costFacts = getDashboardCostFacts(
1547
+ scanSource,
1548
+ scanResult.sessions,
1549
+ compare?.from ?? from,
1550
+ to,
1551
+ cacheTo,
1552
+ analyticsRevision
1553
+ );
1554
+ const aggregate = getSnapshotAggregation(
1555
+ scanSource,
1556
+ scanResult.sessions,
1557
+ [
1558
+ "dashboard",
1559
+ scope.agent,
1560
+ scope.projectKind,
1561
+ scope.projectKey,
1562
+ from,
1563
+ cacheTo,
1564
+ compare?.from,
1565
+ compare?.to,
1566
+ analyticsRevision
1567
+ ],
1568
+ () => {
1569
+ const agentInfo = getAgentInfoMap({});
1570
+ const agentInfoMap = new Map(agentInfo.map((agent) => [agent.name, agent]));
1571
+ return buildDashboard(scanResult.sessions, {
1572
+ byAgentNames: Object.keys(scanResult.byAgent),
1573
+ scope,
1574
+ from,
1575
+ to,
1576
+ agentInfoMap,
1577
+ compare,
1578
+ costFacts
1579
+ });
1532
1580
  }
1581
+ );
1582
+ const storageAggregation = getDashboardStorageAggregation(
1583
+ scanSource,
1584
+ scanResult.sessions,
1585
+ scope,
1586
+ from,
1587
+ to,
1588
+ cacheTo,
1589
+ analyticsRevision
1590
+ );
1591
+ const data = {
1592
+ ...aggregate,
1593
+ ...storageAggregation,
1594
+ window: { from, to, days, compareFrom: compare?.from, compareTo: compare?.to }
1533
1595
  };
1534
- return c.json(payload);
1596
+ const aliases = loadAliasView();
1597
+ return c.json({
1598
+ ...data,
1599
+ recentSessions: data.recentSessions.map(
1600
+ (item) => toPublicReferencedSessionHead({
1601
+ ...item,
1602
+ session: aliases.decorate(item.session, item.reference)
1603
+ })
1604
+ ),
1605
+ recentFileActivities: data.recentFileActivities.map(
1606
+ (activity) => toPublicReferencedSessionHead(decorateFileActivity(activity, aliases))
1607
+ )
1608
+ });
1609
+ }
1610
+
1611
+ // src/api/search-result-merge.ts
1612
+ function withParentContext(results, getSessionTree, aliases) {
1613
+ if (!results.some((result) => result.session.parent_reference)) return results;
1614
+ const byRouteKey = getSessionTree().byRouteKey;
1615
+ return results.map((result) => {
1616
+ const parentReference = result.session.parent_reference;
1617
+ if (!parentReference) return result;
1618
+ const parent = byRouteKey.get(formatSessionReference(parentReference))?.session;
1619
+ if (!parent) return result;
1620
+ const reference = normalizeSessionReference(parentReference);
1621
+ return { ...result, parent: { reference, title: aliases.get(reference) ?? parent.title } };
1622
+ });
1623
+ }
1624
+ var ALIAS_SEARCH_RESULT_SHARE = 0.25;
1625
+ function searchResultKey(result) {
1626
+ return `${result.reference.agentName}\0${result.reference.sessionId}`;
1535
1627
  }
1536
- function handleGetScanStatus(c, scanSource) {
1537
- return c.json(scanSource.getScanStatus());
1628
+ function uniqueSearchResults(results) {
1629
+ const seen = /* @__PURE__ */ new Set();
1630
+ return results.filter((result) => {
1631
+ const key = searchResultKey(result);
1632
+ if (seen.has(key)) return false;
1633
+ seen.add(key);
1634
+ return true;
1635
+ });
1538
1636
  }
1539
- function handleGetAgents(c, scanSource, defaults = {}) {
1637
+ function mergeAliasSearchResults(rankedResults, aliasResults, limit) {
1638
+ if (limit <= 0) return [];
1639
+ const ranked = uniqueSearchResults(rankedResults);
1640
+ const rankedKeys = new Set(ranked.map(searchResultKey));
1641
+ const aliases = uniqueSearchResults(aliasResults).filter(
1642
+ (result) => !rankedKeys.has(searchResultKey(result))
1643
+ );
1644
+ if (ranked.length === 0) return aliases.slice(0, limit);
1645
+ if (aliases.length === 0) return ranked.slice(0, limit);
1646
+ const aliasQuota = Math.min(
1647
+ limit - 1,
1648
+ Math.max(1, Math.floor(limit * ALIAS_SEARCH_RESULT_SHARE))
1649
+ );
1650
+ const rankedQuota = limit - aliasQuota;
1651
+ return [
1652
+ ...ranked.slice(0, rankedQuota),
1653
+ ...aliases.slice(0, aliasQuota),
1654
+ ...ranked.slice(rankedQuota),
1655
+ ...aliases.slice(aliasQuota)
1656
+ ].slice(0, limit);
1657
+ }
1658
+
1659
+ // src/api/search-handlers.ts
1660
+ async function handleSearchSessions(c, scanSource, defaults = {}, resolver) {
1661
+ const query = c.req.query("q")?.trim() ?? "";
1540
1662
  const scanResult = scanSource.getSnapshot();
1541
- const window = parseDateWindowRequest(c, "agents", defaults);
1663
+ const sessionQuery = parseSessionQuery(searchParams(c), KNOWN_AGENT_NAMES, SEARCH_LIMIT_POLICY);
1664
+ if (sessionQuery.limit.kind === "invalid") {
1665
+ reportInvalidQueryParameter("search", "limit", "rejected");
1666
+ return c.json({ error: sessionQuery.limit.error }, 400);
1667
+ }
1668
+ const projectIdentity = parseProjectIdentityFilter(
1669
+ c.req.query("projectKind"),
1670
+ c.req.query("projectKey")
1671
+ );
1672
+ if (projectIdentity === null) {
1673
+ return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
1674
+ }
1675
+ const window = parseDateWindowRequest(c, "search", defaults);
1542
1676
  if (window.kind === "rejected") return window.response;
1543
- const { from, to } = window;
1544
- const agents = getSnapshotAggregation(
1545
- scanSource,
1546
- scanResult.sessions,
1547
- ["agents", from, to],
1548
- () => {
1549
- const counts = Object.fromEntries(
1550
- Object.entries(scanResult.byAgent).map(([agentName, sessions]) => [
1551
- agentName,
1552
- filterSessionsByActivityWindow(sessions, from, to).length
1553
- ])
1677
+ if (sessionQuery.agent.kind === "unknown") {
1678
+ reportInvalidQueryParameter("search", "agent", "empty_result");
1679
+ return c.json({ results: [] });
1680
+ }
1681
+ const searchRequestOptions = parseSearchOptions(
1682
+ c,
1683
+ window,
1684
+ {
1685
+ agent: sessionQuery.agent.kind === "known" ? sessionQuery.agent.agentName : void 0,
1686
+ limit: sessionQuery.limit.value
1687
+ },
1688
+ projectIdentity
1689
+ );
1690
+ const cwd = getSearchProjectDirectory(query, searchRequestOptions);
1691
+ let projectScope;
1692
+ if (cwd) {
1693
+ try {
1694
+ projectScope = await resolveProjectScope(
1695
+ cwd,
1696
+ scanResult.sessions,
1697
+ resolver,
1698
+ c.req.raw.signal
1554
1699
  );
1555
- return getAgentInfoMap(counts);
1700
+ } catch (error) {
1701
+ return projectScopeResolutionFailureResponse(c, "search", error);
1556
1702
  }
1703
+ }
1704
+ const { cwd: _cwd, ...searchOptions } = searchRequestOptions;
1705
+ const resolvedSearchOptions = projectScope ? { ...searchOptions, projectScope } : searchOptions;
1706
+ const aliases = loadAliasView();
1707
+ let sessionTree;
1708
+ const getSessionTree = () => sessionTree ??= getSnapshotSessionTree(scanSource, scanResult.sessions);
1709
+ const effectiveSearchOptions = mergeSearchQueryOptions(query, resolvedSearchOptions).options;
1710
+ const searchContext = {
1711
+ queryScope: scanSource.queryScope,
1712
+ ...effectiveSearchOptions.costMin != null || effectiveSearchOptions.costMax != null ? { sessionTree: getSessionTree() } : {}
1713
+ };
1714
+ const searchResults = executeSessionSearch(
1715
+ query,
1716
+ resolvedSearchOptions,
1717
+ scanResult,
1718
+ searchContext
1557
1719
  );
1558
- return c.json(agents);
1720
+ const results = searchResults.map((result) => ({
1721
+ ...result,
1722
+ session: aliases.decorate(result.session, result.reference)
1723
+ }));
1724
+ const aliasResults = findAliasSearchResults(
1725
+ query,
1726
+ resolvedSearchOptions,
1727
+ scanResult,
1728
+ aliases,
1729
+ searchContext
1730
+ );
1731
+ const mergedResults = mergeAliasSearchResults(
1732
+ results,
1733
+ aliasResults,
1734
+ resolvedSearchOptions.limit ?? 50
1735
+ );
1736
+ return c.json({
1737
+ results: withParentContext(mergedResults, getSessionTree, aliases).map(
1738
+ toPublicReferencedSessionHead
1739
+ )
1740
+ });
1559
1741
  }
1560
- function handleGetProjects(c, scanSource, defaults = {}) {
1561
- const scanResult = scanSource.getSnapshot();
1562
- const params = searchParams(c);
1563
- const limit = parseLimit(params.get("limit"), PROJECT_PAGE_LIMIT_POLICY);
1564
- if (limit.kind === "invalid") {
1565
- reportInvalidQueryParameter("projects", "limit", "rejected");
1566
- return c.json({ error: limit.error }, 400);
1742
+ async function handleGetFileActivity(c, scanSource, defaults = {}, resolver) {
1743
+ const sessionQuery = parseSessionQuery(
1744
+ searchParams(c),
1745
+ KNOWN_AGENT_NAMES,
1746
+ FILE_ACTIVITY_LIMIT_POLICY
1747
+ );
1748
+ if (sessionQuery.limit.kind === "invalid") {
1749
+ reportInvalidQueryParameter("file-activity", "limit", "rejected");
1750
+ return c.json({ error: sessionQuery.limit.error }, 400);
1567
1751
  }
1568
1752
  const projectIdentity = parseProjectIdentityFilter(
1569
1753
  c.req.query("projectKind"),
@@ -1572,51 +1756,184 @@ function handleGetProjects(c, scanSource, defaults = {}) {
1572
1756
  if (projectIdentity === null) {
1573
1757
  return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
1574
1758
  }
1575
- const window = parseDateWindowRequest(c, "projects", defaults);
1759
+ const window = parseDateWindowRequest(c, "file-activity", defaults);
1576
1760
  if (window.kind === "rejected") return window.response;
1577
- const { from, to } = window;
1578
- const analyticsRevision = getAnalyticsRevision();
1579
- const catalog = getSnapshotAggregation(
1580
- scanSource,
1581
- scanResult.sessions,
1582
- ["projects", from, to, analyticsRevision],
1583
- () => {
1584
- const tree = getSnapshotSessionTree(scanSource, scanResult.sessions);
1585
- const costFacts = listDashboardCostFacts({ from, to, includeModelCosts: false });
1586
- const projects2 = attachProjectMetricsFromTree(
1587
- listCachedProjectGroups(scanResult.sessions),
1588
- tree,
1589
- from,
1590
- to,
1591
- costFacts
1592
- ).filter(
1593
- (project) => project.sessionCount > 0 || project.messages > 0 || project.tokens > 0 || project.cost > 0
1594
- );
1595
- return { projects: projects2, summary: summarizeProjects(projects2) };
1761
+ if (sessionQuery.agent.kind === "unknown") {
1762
+ reportInvalidQueryParameter("file-activity", "agent", "empty_result");
1763
+ return c.json({ activity: [] });
1764
+ }
1765
+ const cwd = optionalQueryValue(c.req.query("cwd"));
1766
+ let projectScope;
1767
+ if (cwd) {
1768
+ try {
1769
+ projectScope = await resolveProjectScope(cwd, [], resolver, c.req.raw.signal);
1770
+ } catch (error) {
1771
+ return projectScopeResolutionFailureResponse(c, "file-activity", error);
1772
+ }
1773
+ }
1774
+ const aliases = loadAliasView();
1775
+ return c.json({
1776
+ activity: listFileActivity(
1777
+ {
1778
+ agent: sessionQuery.agent.kind === "known" ? sessionQuery.agent.agentName : void 0,
1779
+ sessionId: optionalQueryValue(c.req.query("sessionId")),
1780
+ projectKind: projectIdentity?.kind,
1781
+ projectKey: projectIdentity?.key,
1782
+ project: optionalQueryValue(c.req.query("project")),
1783
+ projectScope,
1784
+ path: optionalQueryValue(c.req.query("path")),
1785
+ kind: parseFileActivityKind(optionalQueryValue(c.req.query("kind"))),
1786
+ from: window.from,
1787
+ to: window.to,
1788
+ limit: sessionQuery.limit.value
1789
+ },
1790
+ scanSource.queryScope
1791
+ ).map((activity) => toPublicReferencedSessionHead(decorateFileActivity(activity, aliases)))
1792
+ });
1793
+ }
1794
+
1795
+ // src/session-detail-loader.ts
1796
+ import { Worker as Worker2 } from "worker_threads";
1797
+ var SessionDetailBusyError = class extends Error {
1798
+ constructor() {
1799
+ super("Session detail workers are busy");
1800
+ }
1801
+ };
1802
+ var MAX_DETAIL_WORKERS = 2;
1803
+ var DETAIL_TIMEOUT_MS = 6e4;
1804
+ var ThreadSessionDetailLoader = class {
1805
+ constructor(workerUrl = new URL("./session-detail-worker.js", import.meta.url)) {
1806
+ this.workerUrl = workerUrl;
1807
+ }
1808
+ workerUrl;
1809
+ workers = /* @__PURE__ */ new Set();
1810
+ shutdownController = new AbortController();
1811
+ load = async (snapshot, reference, options = {}, signal) => {
1812
+ const requestSignal = signal ? AbortSignal.any([signal, this.shutdownController.signal]) : this.shutdownController.signal;
1813
+ requestSignal.throwIfAborted();
1814
+ const cached = materializeCachedSessionDetailResponse(snapshot, reference, options);
1815
+ if (cached) return cached;
1816
+ if (this.workers.size >= MAX_DETAIL_WORKERS) throw new SessionDetailBusyError();
1817
+ const agent = snapshot.agents.find((item) => item.name === reference.agentName);
1818
+ const head = snapshot.byAgent[reference.agentName]?.find(
1819
+ (item) => item.reference.sessionId === reference.sessionId
1820
+ );
1821
+ const meta = agent?.getSessionCacheMeta(reference.sessionId);
1822
+ const request = {
1823
+ reference,
1824
+ head,
1825
+ meta: meta ? { [reference.sessionId]: meta } : {},
1826
+ messageCursor: options.messageCursor,
1827
+ pricingGenerationId: getPricingGeneration().id,
1828
+ logContext: appLogger.captureContext()
1829
+ };
1830
+ appLogger.info("session_detail.worker.started", {
1831
+ agent: reference.agentName,
1832
+ session_id: reference.sessionId
1833
+ });
1834
+ const worker = new Worker2(this.workerUrl, { workerData: request });
1835
+ const exited = new Promise(
1836
+ (resolve4) => worker.once("exit", () => {
1837
+ this.workers.delete(worker);
1838
+ resolve4();
1839
+ })
1840
+ );
1841
+ let timeout;
1842
+ let abort = () => {
1843
+ };
1844
+ try {
1845
+ return await new Promise((resolve4, reject) => {
1846
+ abort = () => reject(requestSignal.reason);
1847
+ requestSignal.addEventListener("abort", abort, { once: true });
1848
+ this.workers.add(worker);
1849
+ timeout = setTimeout(
1850
+ () => reject(new Error("Session detail loading timed out")),
1851
+ DETAIL_TIMEOUT_MS
1852
+ );
1853
+ timeout.unref();
1854
+ worker.on("message", (message) => {
1855
+ if (appLogger.consumeWorkerMessage(message)) return;
1856
+ if (message.type === "result") resolve4(message.result);
1857
+ else if (message.type === "error") reject(new Error(message.error));
1858
+ });
1859
+ worker.once("error", reject);
1860
+ worker.once(
1861
+ "exit",
1862
+ (code) => reject(new Error(`Session detail worker exited without a result (${code})`))
1863
+ );
1864
+ });
1865
+ } finally {
1866
+ clearTimeout(timeout);
1867
+ requestSignal.removeEventListener("abort", abort);
1868
+ await terminateWorkerAfterLogDrain(worker);
1869
+ await exited;
1596
1870
  }
1871
+ };
1872
+ async shutdown() {
1873
+ this.shutdownController.abort();
1874
+ await Promise.allSettled(
1875
+ [...this.workers].map((worker) => terminateWorkerAfterLogDrain(worker))
1876
+ );
1877
+ }
1878
+ };
1879
+
1880
+ // src/api/session-detail-stream.ts
1881
+ var SESSION_DETAIL_STREAM_BATCH_CHARS = 64 * 1024;
1882
+ function createSessionDetailJsonResponse(data, messages) {
1883
+ const encoder = new TextEncoder();
1884
+ const headerJson = JSON.stringify(data);
1885
+ const headerPrefix = headerJson === "{}" ? '{"messages":[' : `${headerJson.slice(0, -1)},"messages":[`;
1886
+ const iterator = messages[Symbol.iterator]();
1887
+ let wroteHeader = false;
1888
+ let wroteMessage = false;
1889
+ return new Response(
1890
+ new ReadableStream({
1891
+ pull(controller) {
1892
+ if (!wroteHeader) {
1893
+ controller.enqueue(encoder.encode(headerPrefix));
1894
+ wroteHeader = true;
1895
+ return;
1896
+ }
1897
+ const batch = [];
1898
+ let batchLength = 0;
1899
+ let next;
1900
+ try {
1901
+ next = iterator.next();
1902
+ while (!next.done) {
1903
+ const prefix = wroteMessage ? "," : "";
1904
+ batch.push(prefix, next.value);
1905
+ batchLength += prefix.length + next.value.length;
1906
+ wroteMessage = true;
1907
+ if (batchLength >= SESSION_DETAIL_STREAM_BATCH_CHARS) break;
1908
+ next = iterator.next();
1909
+ }
1910
+ } catch (error) {
1911
+ try {
1912
+ iterator.return?.();
1913
+ } catch {
1914
+ }
1915
+ controller.error(error);
1916
+ return;
1917
+ }
1918
+ if (next.done) {
1919
+ batch.push("]}");
1920
+ controller.enqueue(encoder.encode(batch.join("")));
1921
+ controller.close();
1922
+ return;
1923
+ }
1924
+ controller.enqueue(encoder.encode(batch.join("")));
1925
+ },
1926
+ cancel() {
1927
+ iterator.return?.();
1928
+ }
1929
+ }),
1930
+ { headers: { "Content-Type": "application/json; charset=UTF-8" } }
1597
1931
  );
1598
- const projects = projectIdentity ? catalog.projects.filter(
1599
- (project) => project.identityKind === projectIdentity.kind && project.identityKey === projectIdentity.key
1600
- ) : catalog.projects;
1601
- const page = paginateSnapshot(projects, {
1602
- cursor: params.get("cursor") ?? void 0,
1603
- limit: limit.value,
1604
- query: params,
1605
- snapshotIdentity: scanResult.sessions,
1606
- viewIdentity: catalog.projects
1607
- });
1608
- if (page.kind === "invalid_cursor") {
1609
- reportInvalidQueryParameter("projects", "cursor", "rejected");
1610
- return c.json({ error: "cursor is invalid for this request" }, 400);
1611
- }
1612
- if (page.kind === "stale_snapshot") {
1613
- return c.json({ error: "project snapshot changed; restart pagination" }, 409);
1614
- }
1615
- return c.json({
1616
- projects: page.items,
1617
- summary: projectIdentity ? summarizeProjects(projects) : catalog.summary,
1618
- ...page.nextCursor ? { nextCursor: page.nextCursor } : {}
1619
- });
1932
+ }
1933
+
1934
+ // src/api/session-handlers.ts
1935
+ function getSessionHeadReference(session) {
1936
+ return session.reference;
1620
1937
  }
1621
1938
  async function handleGetSessions(c, scanSource, defaults = {}, resolver) {
1622
1939
  const scanResult = scanSource.getSnapshot();
@@ -1720,149 +2037,20 @@ async function handleGetSessions(c, scanSource, defaults = {}, resolver) {
1720
2037
  )
1721
2038
  });
1722
2039
  }
1723
- async function handleSearchSessions(c, scanSource, defaults = {}, resolver) {
1724
- const query = c.req.query("q")?.trim() ?? "";
1725
- const scanResult = scanSource.getSnapshot();
1726
- const sessionQuery = parseSessionQuery(searchParams(c), KNOWN_AGENT_NAMES, SEARCH_LIMIT_POLICY);
1727
- if (sessionQuery.limit.kind === "invalid") {
1728
- reportInvalidQueryParameter("search", "limit", "rejected");
1729
- return c.json({ error: sessionQuery.limit.error }, 400);
1730
- }
1731
- const projectIdentity = parseProjectIdentityFilter(
1732
- c.req.query("projectKind"),
1733
- c.req.query("projectKey")
1734
- );
1735
- if (projectIdentity === null) {
1736
- return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
1737
- }
1738
- const window = parseDateWindowRequest(c, "search", defaults);
1739
- if (window.kind === "rejected") return window.response;
1740
- if (sessionQuery.agent.kind === "unknown") {
1741
- reportInvalidQueryParameter("search", "agent", "empty_result");
1742
- return c.json({ results: [] });
1743
- }
1744
- const searchRequestOptions = parseSearchOptions(
1745
- c,
1746
- window,
1747
- {
1748
- agent: sessionQuery.agent.kind === "known" ? sessionQuery.agent.agentName : void 0,
1749
- limit: sessionQuery.limit.value
1750
- },
1751
- projectIdentity
1752
- );
1753
- const cwd = getSearchProjectDirectory(query, searchRequestOptions);
1754
- let projectScope;
1755
- if (cwd) {
1756
- try {
1757
- projectScope = await resolveProjectScope(
1758
- cwd,
1759
- scanResult.sessions,
1760
- resolver,
1761
- c.req.raw.signal
1762
- );
1763
- } catch (error) {
1764
- return projectScopeResolutionFailureResponse(c, "search", error);
1765
- }
1766
- }
1767
- const { cwd: _cwd, ...searchOptions } = searchRequestOptions;
1768
- const resolvedSearchOptions = projectScope ? { ...searchOptions, projectScope } : searchOptions;
1769
- const aliases = loadAliasView();
1770
- let sessionTree;
1771
- const getSessionTree = () => sessionTree ??= getSnapshotSessionTree(scanSource, scanResult.sessions);
1772
- const effectiveSearchOptions = mergeSearchQueryOptions(query, resolvedSearchOptions).options;
1773
- const searchContext = effectiveSearchOptions.costMin != null || effectiveSearchOptions.costMax != null ? { sessionTree: getSessionTree() } : void 0;
1774
- const searchResults = searchContext ? executeSessionSearch(query, resolvedSearchOptions, scanResult, searchContext) : executeSessionSearch(query, resolvedSearchOptions, scanResult);
1775
- const results = searchResults.map((result) => ({
1776
- ...result,
1777
- session: aliases.decorate(result.session, result.reference)
1778
- }));
1779
- const aliasResults = findAliasSearchResults(
1780
- query,
1781
- resolvedSearchOptions,
1782
- scanResult,
1783
- aliases,
1784
- searchContext
1785
- );
1786
- const mergedResults = mergeAliasSearchResults(
1787
- results,
1788
- aliasResults,
1789
- resolvedSearchOptions.limit ?? 50
1790
- );
1791
- return c.json({
1792
- results: withParentContext(mergedResults, getSessionTree, aliases).map(
1793
- toPublicReferencedSessionHead
1794
- )
1795
- });
1796
- }
1797
- async function handleGetFileActivity(c, defaults = {}, resolver) {
1798
- const sessionQuery = parseSessionQuery(
1799
- searchParams(c),
1800
- KNOWN_AGENT_NAMES,
1801
- FILE_ACTIVITY_LIMIT_POLICY
1802
- );
1803
- if (sessionQuery.limit.kind === "invalid") {
1804
- reportInvalidQueryParameter("file-activity", "limit", "rejected");
1805
- return c.json({ error: sessionQuery.limit.error }, 400);
1806
- }
1807
- const projectIdentity = parseProjectIdentityFilter(
1808
- c.req.query("projectKind"),
1809
- c.req.query("projectKey")
1810
- );
1811
- if (projectIdentity === null) {
1812
- return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
1813
- }
1814
- const window = parseDateWindowRequest(c, "file-activity", defaults);
1815
- if (window.kind === "rejected") return window.response;
1816
- if (sessionQuery.agent.kind === "unknown") {
1817
- reportInvalidQueryParameter("file-activity", "agent", "empty_result");
1818
- return c.json({ activity: [] });
1819
- }
1820
- const cwd = optionalQueryValue(c.req.query("cwd"));
1821
- let projectScope;
1822
- if (cwd) {
1823
- try {
1824
- projectScope = await resolveProjectScope(cwd, [], resolver, c.req.raw.signal);
1825
- } catch (error) {
1826
- return projectScopeResolutionFailureResponse(c, "file-activity", error);
1827
- }
1828
- }
1829
- const aliases = loadAliasView();
1830
- return c.json({
1831
- activity: listFileActivity({
1832
- agent: sessionQuery.agent.kind === "known" ? sessionQuery.agent.agentName : void 0,
1833
- sessionId: optionalQueryValue(c.req.query("sessionId")),
1834
- projectKind: projectIdentity?.kind,
1835
- projectKey: projectIdentity?.key,
1836
- project: optionalQueryValue(c.req.query("project")),
1837
- projectScope,
1838
- path: optionalQueryValue(c.req.query("path")),
1839
- kind: parseFileActivityKind(optionalQueryValue(c.req.query("kind"))),
1840
- from: window.from,
1841
- to: window.to,
1842
- limit: sessionQuery.limit.value
1843
- }).map((activity) => toPublicReferencedSessionHead(decorateFileActivity(activity, aliases)))
1844
- });
1845
- }
1846
- async function handleGetSessionData(c, scanSource) {
2040
+ async function handleGetSessionData(c, scanSource, loadDetail = materializeSessionDetailResponse) {
1847
2041
  const startedAt = performance.now();
1848
2042
  const agentName = c.req.param("agent");
1849
2043
  const sessionId = c.req.param("id");
1850
- if (!agentName) {
1851
- return c.json({ error: "Missing agent name" }, 400);
1852
- }
1853
- if (!sessionId) {
1854
- return c.json({ error: "Missing session ID" }, 400);
1855
- }
2044
+ if (!agentName) return c.json({ error: "Missing agent name" }, 400);
2045
+ if (!sessionId) return c.json({ error: "Missing session ID" }, 400);
1856
2046
  try {
1857
- const reference = {
1858
- agentName,
1859
- sessionId
1860
- };
2047
+ const reference = { agentName, sessionId };
1861
2048
  const messageCursor = optionalQueryValue(c.req.query("messageCursor"));
1862
- const result = materializeSessionDetailResponse(
2049
+ const result = await loadDetail(
1863
2050
  scanSource.getSnapshot(),
1864
2051
  reference,
1865
- messageCursor ? { messageCursor } : {}
2052
+ messageCursor ? { messageCursor } : {},
2053
+ c.req.raw.signal
1866
2054
  );
1867
2055
  if (result.status === "unknown-agent") {
1868
2056
  return c.json({ error: `Unknown agent: ${agentName}` }, 404);
@@ -1879,129 +2067,148 @@ async function handleGetSessionData(c, scanSource) {
1879
2067
  agent: agentName,
1880
2068
  session_id: sessionId,
1881
2069
  messages: result.status === "found-json" ? result.messageCount : result.data.messages.length,
1882
- sent_messages: result.status === "found-json" ? result.sentMessageCount : result.data.messages.length,
1883
- message_update: result.data.message_update ?? "reset",
1884
- duration_ms: Math.round(performance.now() - startedAt)
1885
- });
1886
- const aliases = loadAliasView();
1887
- if (result.status === "found-json") {
1888
- return createSessionDetailJsonResponse(
1889
- aliases.decorate(result.data, result.data.reference),
1890
- result.messages
1891
- );
1892
- }
1893
- return c.json(aliases.decorate(result.data, result.data.reference));
1894
- } catch (err) {
1895
- const message = err instanceof Error ? err.message : "Failed to load session";
1896
- appLogger.error("api.session_data.error", {
1897
- agent: agentName,
1898
- session_id: sessionId,
1899
- duration_ms: Math.round(performance.now() - startedAt),
1900
- error: message
1901
- });
1902
- return c.json({ error: "Failed to load session" }, 500);
1903
- }
1904
- }
1905
- async function handlePostClientLog(c) {
1906
- const payload = await c.req.json().catch(() => null);
1907
- const rawEvent = payload?.event;
1908
- if (typeof rawEvent !== "string" || !rawEvent.trim()) {
1909
- return c.json({ ok: false }, 400);
1910
- }
1911
- const event = rawEvent.trim().replace(/[^a-zA-Z0-9_.:-]/g, "_").slice(0, 120);
1912
- appLogger.info(`client.${event}`, sanitizeClientLogData(payload?.data));
1913
- return c.json({ ok: true });
1914
- }
1915
- function handleGetDashboard(c, scanSource, defaults = {}) {
1916
- const scanResult = scanSource.getSnapshot();
1917
- const projectIdentity = parseProjectIdentityFilter(
1918
- c.req.query("projectKind"),
1919
- c.req.query("projectKey")
1920
- );
1921
- if (projectIdentity === null) {
1922
- return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
1923
- }
1924
- const dateWindow = parseDateWindowRequest(c, "dashboard", defaults);
1925
- if (dateWindow.kind === "rejected") return dateWindow.response;
1926
- const { from, to, days } = resolveTimeWindow({
1927
- mode: "dashboard",
1928
- window: dateWindow,
1929
- days: c.req.query("days"),
1930
- defaultDays: defaults.days
1931
- });
1932
- const scope = {
1933
- agent: optionalQueryValue(c.req.query("agent"))?.toLowerCase(),
1934
- projectKind: projectIdentity?.kind,
1935
- projectKey: projectIdentity?.key
1936
- };
1937
- const compare = from == null ? void 0 : { from: addCalendarDays(from, -(days ?? countCalendarDays(from, to))), to: from - 1 };
1938
- const fixedTo = dateWindow.to;
1939
- const cacheTo = fixedTo ?? startOfCalendarDay(to);
1940
- const analyticsRevision = getAnalyticsRevision();
1941
- const costFacts = getDashboardCostFacts(
1942
- scanSource,
1943
- scanResult.sessions,
1944
- compare?.from ?? from,
1945
- to,
1946
- cacheTo,
1947
- analyticsRevision
1948
- );
1949
- const aggregate = getSnapshotAggregation(
1950
- scanSource,
1951
- scanResult.sessions,
1952
- [
1953
- "dashboard",
1954
- scope.agent,
1955
- scope.projectKind,
1956
- scope.projectKey,
1957
- from,
1958
- cacheTo,
1959
- compare?.from,
1960
- compare?.to,
1961
- analyticsRevision
1962
- ],
1963
- () => {
1964
- const agentInfo = getAgentInfoMap({});
1965
- const agentInfoMap = new Map(agentInfo.map((agent) => [agent.name, agent]));
1966
- return buildDashboard(scanResult.sessions, {
1967
- byAgentNames: Object.keys(scanResult.byAgent),
1968
- scope,
1969
- from,
1970
- to,
1971
- agentInfoMap,
1972
- compare,
1973
- costFacts
1974
- });
1975
- }
1976
- );
1977
- const storageAggregation = getDashboardStorageAggregation(
1978
- scanSource,
1979
- scanResult.sessions,
1980
- scope,
1981
- from,
1982
- to,
1983
- cacheTo,
1984
- analyticsRevision
1985
- );
1986
- const data = {
1987
- ...aggregate,
1988
- ...storageAggregation,
1989
- window: { from, to, days, compareFrom: compare?.from, compareTo: compare?.to }
1990
- };
1991
- const aliases = loadAliasView();
1992
- return c.json({
1993
- ...data,
1994
- recentSessions: data.recentSessions.map(
1995
- (item) => toPublicReferencedSessionHead({
1996
- ...item,
1997
- session: aliases.decorate(item.session, item.reference)
1998
- })
1999
- ),
2000
- recentFileActivities: data.recentFileActivities.map(
2001
- (activity) => toPublicReferencedSessionHead(decorateFileActivity(activity, aliases))
2002
- )
2070
+ sent_messages: result.status === "found-json" ? result.sentMessageCount : result.data.messages.length,
2071
+ message_update: result.data.message_update ?? "reset",
2072
+ duration_ms: Math.round(performance.now() - startedAt)
2073
+ });
2074
+ const aliases = loadAliasView();
2075
+ if (result.status === "found-json") {
2076
+ return createSessionDetailJsonResponse(
2077
+ aliases.decorate(result.data, result.data.reference),
2078
+ result.messages
2079
+ );
2080
+ }
2081
+ return c.json(aliases.decorate(result.data, result.data.reference));
2082
+ } catch (error) {
2083
+ if (error instanceof SessionDetailBusyError) {
2084
+ c.header("Retry-After", "1");
2085
+ return c.json({ error: "Session details busy; retry later" }, 503);
2086
+ }
2087
+ const message = error instanceof Error ? error.message : "Failed to load session";
2088
+ appLogger.error("api.session_data.error", {
2089
+ agent: agentName,
2090
+ session_id: sessionId,
2091
+ duration_ms: Math.round(performance.now() - startedAt),
2092
+ error: message
2093
+ });
2094
+ return c.json({ error: "Failed to load session" }, 500);
2095
+ }
2096
+ }
2097
+
2098
+ // src/api/request-payloads.ts
2099
+ function isRecord(value) {
2100
+ return typeof value === "object" && value !== null;
2101
+ }
2102
+ function parseSessionReferencePayload(value) {
2103
+ if (!isRecord(value) || typeof value.agentName !== "string" || typeof value.sessionId !== "string" || !value.agentName.trim() || !value.sessionId) {
2104
+ return null;
2105
+ }
2106
+ return normalizeSessionReference({
2107
+ agentName: value.agentName.trim().toLowerCase(),
2108
+ sessionId: value.sessionId
2109
+ });
2110
+ }
2111
+ function parseBookmarkReference(value) {
2112
+ if (!isRecord(value)) return null;
2113
+ const reference = parseSessionReferencePayload(value.reference);
2114
+ if (reference) return reference;
2115
+ if (typeof value.agentKey !== "string" || typeof value.sessionId !== "string" || !value.agentKey.trim() || !value.sessionId) {
2116
+ return null;
2117
+ }
2118
+ return normalizeSessionReference({
2119
+ agentName: value.agentKey.trim().toLowerCase(),
2120
+ sessionId: value.sessionId
2003
2121
  });
2004
2122
  }
2123
+ function parseBookmarkImport(value) {
2124
+ if (!isRecord(value)) return null;
2125
+ const reference = parseBookmarkReference(value);
2126
+ if (!reference) return null;
2127
+ const timestamp = value.bookmarkedAt ?? value.bookmarked_at;
2128
+ if (timestamp != null && (typeof timestamp !== "number" || !Number.isFinite(timestamp))) {
2129
+ return null;
2130
+ }
2131
+ return {
2132
+ reference,
2133
+ bookmarkedAt: typeof timestamp === "number" ? timestamp : Date.now()
2134
+ };
2135
+ }
2136
+ function sanitizeClientLogData(value) {
2137
+ if (!isRecord(value)) return {};
2138
+ const sanitized = {};
2139
+ for (const [key, item] of Object.entries(value)) {
2140
+ if (CLIENT_LOG_STRING_FIELDS.has(key) && typeof item === "string") {
2141
+ if (key === "operation_id" && !UUID_PATTERN.test(item)) continue;
2142
+ sanitized[key] = item.slice(0, 300);
2143
+ continue;
2144
+ }
2145
+ if (CLIENT_LOG_NUMBER_FIELDS.has(key) && typeof item === "number" && Number.isFinite(item) && item >= 0) {
2146
+ sanitized[key] = item;
2147
+ continue;
2148
+ }
2149
+ if (CLIENT_LOG_NULLABLE_FIELDS.has(key) && item === null) sanitized[key] = null;
2150
+ }
2151
+ return sanitized;
2152
+ }
2153
+ var CLIENT_LOG_STRING_FIELDS = /* @__PURE__ */ new Set([
2154
+ "agent",
2155
+ "error_name",
2156
+ "mode",
2157
+ "operation_id",
2158
+ "phase",
2159
+ "profiler_id",
2160
+ "reason",
2161
+ "request_key",
2162
+ "session",
2163
+ "source",
2164
+ "trigger"
2165
+ ]);
2166
+ var CLIENT_LOG_NUMBER_FIELDS = /* @__PURE__ */ new Set([
2167
+ "actual_duration_ms",
2168
+ "agents",
2169
+ "base_duration_ms",
2170
+ "commit_time_ms",
2171
+ "duration_ms",
2172
+ "error_status",
2173
+ "messages",
2174
+ "query_length",
2175
+ "results",
2176
+ "sessions",
2177
+ "start_time_ms"
2178
+ ]);
2179
+ var CLIENT_LOG_NULLABLE_FIELDS = /* @__PURE__ */ new Set(["agent", "session"]);
2180
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2181
+
2182
+ // src/api/client-log-handler.ts
2183
+ var CLIENT_LOG_EVENTS = /* @__PURE__ */ new Set([
2184
+ "app.load.done",
2185
+ "app.load.error",
2186
+ "app.load.start",
2187
+ "bookmark.add",
2188
+ "bookmark.delete",
2189
+ "react.profiler.commit",
2190
+ "route.change",
2191
+ "search.done",
2192
+ "search.error",
2193
+ "search.start",
2194
+ "session.markdown_copy.done",
2195
+ "session.markdown_copy.error",
2196
+ "session.open.cancel",
2197
+ "session.open.done",
2198
+ "session.open.error",
2199
+ "session.open.start"
2200
+ ]);
2201
+ async function handlePostClientLog(c) {
2202
+ const payload = await c.req.json().catch(() => null);
2203
+ const rawEvent = payload?.event;
2204
+ if (typeof rawEvent !== "string") {
2205
+ return c.json({ ok: false }, 400);
2206
+ }
2207
+ const event = rawEvent.trim();
2208
+ if (!CLIENT_LOG_EVENTS.has(event)) return c.json({ ok: false }, 400);
2209
+ appLogger.info(`client.${event}`, sanitizeClientLogData(payload?.data));
2210
+ return c.json({ ok: true });
2211
+ }
2005
2212
 
2006
2213
  // src/api/bookmark-handlers.ts
2007
2214
  function withStorageErrors(handler, onUnavailable) {
@@ -2296,7 +2503,7 @@ function createSseConnectionBudget(maxConnections) {
2296
2503
  }
2297
2504
  };
2298
2505
  }
2299
- function createSseResponse(eventSource, signal, releaseConnection) {
2506
+ function createSseResponse(eventSource, requestSignal, shutdownSignal, closeConnection) {
2300
2507
  let cancelStream = () => {
2301
2508
  };
2302
2509
  let drainStream = () => {
@@ -2309,26 +2516,32 @@ function createSseResponse(eventSource, signal, releaseConnection) {
2309
2516
  };
2310
2517
  let unsubscribeScanStatus = () => {
2311
2518
  };
2312
- let abortStream = () => {
2519
+ let disconnectStream = () => {
2520
+ };
2521
+ let shutdownStream = () => {
2313
2522
  };
2314
2523
  let heartbeat;
2315
2524
  let buffer;
2316
- const cleanup = () => {
2525
+ const cleanup = (reason) => {
2317
2526
  if (isClosed) return false;
2318
2527
  isClosed = true;
2319
2528
  buffer?.close();
2320
2529
  if (heartbeat) clearInterval(heartbeat);
2321
2530
  unsubscribeSessions();
2322
2531
  unsubscribeScanStatus();
2323
- signal.removeEventListener("abort", abortStream);
2324
- releaseConnection();
2532
+ requestSignal.removeEventListener("abort", disconnectStream);
2533
+ shutdownSignal?.removeEventListener("abort", shutdownStream);
2534
+ closeConnection(reason);
2325
2535
  return true;
2326
2536
  };
2327
- abortStream = () => {
2328
- if (cleanup()) controller.close();
2537
+ disconnectStream = () => {
2538
+ if (cleanup("client_disconnected")) controller.close();
2539
+ };
2540
+ shutdownStream = () => {
2541
+ if (cleanup("server_shutdown")) controller.close();
2329
2542
  };
2330
2543
  buffer = new SseEventBuffer(controller, () => {
2331
- if (cleanup()) controller.error(new Error("SSE client fell behind"));
2544
+ if (cleanup("client_too_slow")) controller.error(new Error("SSE client fell behind"));
2332
2545
  });
2333
2546
  drainStream = () => buffer.drain();
2334
2547
  try {
@@ -2342,12 +2555,16 @@ function createSseResponse(eventSource, signal, releaseConnection) {
2342
2555
  });
2343
2556
  heartbeat = setInterval(() => buffer?.enqueueHeartbeat(), 15e3);
2344
2557
  cancelStream = () => {
2345
- cleanup();
2558
+ cleanup("client_cancelled");
2346
2559
  };
2347
- if (signal.aborted) abortStream();
2348
- else signal.addEventListener("abort", abortStream, { once: true });
2560
+ if (shutdownSignal?.aborted) shutdownStream();
2561
+ else if (requestSignal.aborted) disconnectStream();
2562
+ else {
2563
+ requestSignal.addEventListener("abort", disconnectStream, { once: true });
2564
+ shutdownSignal?.addEventListener("abort", shutdownStream, { once: true });
2565
+ }
2349
2566
  } catch (error) {
2350
- cleanup();
2567
+ cleanup("setup_failed");
2351
2568
  throw error;
2352
2569
  }
2353
2570
  },
@@ -2391,9 +2608,12 @@ function createApiRoutes(scanSource, eventSource, options = {}) {
2391
2608
  );
2392
2609
  api.get(
2393
2610
  "/file-activity",
2394
- (c) => handleGetFileActivity(c, listDefaults, options.projectIdentityResolver)
2611
+ (c) => handleGetFileActivity(c, scanSource, listDefaults, options.projectIdentityResolver)
2612
+ );
2613
+ api.get(
2614
+ "/sessions/:agent/:id",
2615
+ (c) => handleGetSessionData(c, scanSource, options.loadSessionDetail)
2395
2616
  );
2396
- api.get("/sessions/:agent/:id", (c) => handleGetSessionData(c, scanSource));
2397
2617
  api.get("/dashboard", (c) => handleGetDashboard(c, scanSource, listDefaults));
2398
2618
  api.get("/bookmarks", (c) => handleGetBookmarks(c, scanSource));
2399
2619
  api.put("/bookmarks", (c) => handlePutBookmark(c));
@@ -2413,11 +2633,34 @@ function createApiRoutes(scanSource, eventSource, options = {}) {
2413
2633
  c.header("Retry-After", "1");
2414
2634
  return c.json({ error: "Too many active event streams" }, 429);
2415
2635
  }
2416
- const signal = options.shutdownSignal ? AbortSignal.any([c.req.raw.signal, options.shutdownSignal]) : c.req.raw.signal;
2636
+ const connectionId = randomUUID2();
2637
+ const connectedAt = performance.now();
2638
+ let isClosed = false;
2639
+ appLogger.info("api.sse.connection.opened", {
2640
+ connection_id: connectionId,
2641
+ active_connections: sseConnections.activeCount(),
2642
+ connection_limit: MAX_ACTIVE_SSE_CONNECTIONS
2643
+ });
2644
+ const closeConnection = (reason) => {
2645
+ if (isClosed) return;
2646
+ isClosed = true;
2647
+ releaseConnection();
2648
+ appLogger.info("api.sse.connection.closed", {
2649
+ connection_id: connectionId,
2650
+ close_reason: reason,
2651
+ duration_ms: Math.max(0, Math.round(performance.now() - connectedAt)),
2652
+ active_connections: sseConnections.activeCount()
2653
+ });
2654
+ };
2417
2655
  try {
2418
- return createSseResponse(eventSource, signal, releaseConnection);
2656
+ return createSseResponse(
2657
+ eventSource,
2658
+ c.req.raw.signal,
2659
+ options.shutdownSignal,
2660
+ closeConnection
2661
+ );
2419
2662
  } catch (error) {
2420
- releaseConnection();
2663
+ closeConnection("setup_failed");
2421
2664
  throw error;
2422
2665
  }
2423
2666
  });
@@ -2527,7 +2770,6 @@ function loopbackWriteGuard() {
2527
2770
  if (!decision.allowed) {
2528
2771
  appLogger.warn("http.loopback_request.rejected", {
2529
2772
  method: c.req.method,
2530
- path: url.pathname,
2531
2773
  reason: decision.reason
2532
2774
  });
2533
2775
  const error = decision.reason === "content-type" ? "Write requests require application/json" : "Cross-origin API request rejected";
@@ -2661,6 +2903,29 @@ function accessTokenAuth(expectedToken) {
2661
2903
  // src/server.ts
2662
2904
  var MAX_API_REQUEST_BYTES = 1024 * 1024;
2663
2905
  var SERVER_CLOSE_GRACE_MS = 1e3;
2906
+ var OPERATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2907
+ var OBSERVABLE_QUERY_KEYS = /* @__PURE__ */ new Set([
2908
+ "agent",
2909
+ "costMax",
2910
+ "costMin",
2911
+ "cursor",
2912
+ "cwd",
2913
+ "days",
2914
+ "fileActivity",
2915
+ "from",
2916
+ "kind",
2917
+ "limit",
2918
+ "messageCursor",
2919
+ "path",
2920
+ "project",
2921
+ "projectKey",
2922
+ "projectKind",
2923
+ "q",
2924
+ "sessionId",
2925
+ "tag",
2926
+ "to",
2927
+ "tool"
2928
+ ]);
2664
2929
  function findWebDistPath() {
2665
2930
  const __dirname2 = dirname(fileURLToPath(import.meta.url));
2666
2931
  const packagedPath = resolve(__dirname2, "web");
@@ -2700,6 +2965,32 @@ function getListeningPort(server, fallback) {
2700
2965
  const address = server.address();
2701
2966
  return typeof address === "object" && address !== null ? address.port : fallback;
2702
2967
  }
2968
+ function optionalOperationId(value) {
2969
+ return value && OPERATION_ID_PATTERN.test(value) ? value : void 0;
2970
+ }
2971
+ function errorStatus(error) {
2972
+ try {
2973
+ if (typeof error !== "object" || error === null) return 500;
2974
+ const status = Reflect.get(error, "status");
2975
+ if (typeof status === "number" && status >= 400 && status <= 599) return status;
2976
+ } catch {
2977
+ }
2978
+ return 500;
2979
+ }
2980
+ function matchedRoute(context) {
2981
+ try {
2982
+ return matchedRoutes(context).map((route) => route.path).filter((path) => !path.includes("*")).at(-1) ?? "<unmatched>";
2983
+ } catch {
2984
+ return "<unmatched>";
2985
+ }
2986
+ }
2987
+ function queryKeys(url) {
2988
+ try {
2989
+ return [...new Set(new URL(url).searchParams.keys())].filter((key) => OBSERVABLE_QUERY_KEYS.has(key)).toSorted();
2990
+ } catch {
2991
+ return [];
2992
+ }
2993
+ }
2703
2994
  async function closeHttpServer(server, port) {
2704
2995
  if (!server) return;
2705
2996
  let forceCloseTimer;
@@ -2731,6 +3022,7 @@ async function createServer(port, store, options = {}) {
2731
3022
  const loopbackAuthorityEnabled = !accessPolicy.remoteAccessRequired;
2732
3023
  const shutdownController = new AbortController();
2733
3024
  const projectIdentityResolver = options.projectIdentityResolver ?? createProjectIdentityResolver();
3025
+ const sessionDetailLoader = new ThreadSessionDetailLoader();
2734
3026
  let actualPort = null;
2735
3027
  appLogger.info("server.access_policy", {
2736
3028
  bind_category: accessPolicy.bindCategory,
@@ -2772,41 +3064,48 @@ async function createServer(port, store, options = {}) {
2772
3064
  xXssProtection: false
2773
3065
  })
2774
3066
  );
3067
+ app.use("*", async (c, next) => {
3068
+ const startedAt = performance.now();
3069
+ const requestId = randomUUID3();
3070
+ const actualPath = c.req.path;
3071
+ const operationId = optionalOperationId(c.req.header(CODESESH_OPERATION_ID_HEADER));
3072
+ const context = actualPath === "/api/events" ? {} : { request_id: requestId, ...operationId ? { operation_id: operationId } : {} };
3073
+ c.header(CODESESH_REQUEST_ID_HEADER, requestId);
3074
+ return appLogger.restoreContext(context, async () => {
3075
+ let thrown;
3076
+ try {
3077
+ await next();
3078
+ } catch (error) {
3079
+ thrown = error;
3080
+ throw error;
3081
+ } finally {
3082
+ const requestError = thrown ?? c.error;
3083
+ appLogger.info("http.request", {
3084
+ method: c.req.method,
3085
+ route: matchedRoute(c),
3086
+ query_keys: queryKeys(c.req.url),
3087
+ status: requestError == null ? c.res.status : errorStatus(requestError),
3088
+ duration_ms: Math.round(performance.now() - startedAt),
3089
+ ...requestError == null ? {} : { error: requestError }
3090
+ });
3091
+ }
3092
+ });
3093
+ });
2775
3094
  if (loopbackAuthorityEnabled) {
2776
3095
  app.use("*", async (c, next) => {
2777
3096
  const decision = validateLoopbackAuthority(c.env.incoming.rawHeaders, hostname, actualPort);
2778
3097
  if (!decision.allowed) {
2779
3098
  appLogger.warn("http.loopback_authority.rejected", {
2780
3099
  method: c.req.method,
2781
- path: new URL(c.req.url).pathname,
3100
+ route: "<rejected>",
2782
3101
  reason: decision.reason,
2783
- authority: decision.authority
3102
+ authority_present: Boolean(decision.authority)
2784
3103
  });
2785
3104
  return c.json({ error: "Loopback request authority rejected" }, 403);
2786
3105
  }
2787
3106
  await next();
2788
3107
  });
2789
3108
  }
2790
- app.use("*", async (c, next) => {
2791
- const startedAt = performance.now();
2792
- let thrown;
2793
- try {
2794
- await next();
2795
- } catch (error) {
2796
- thrown = error;
2797
- throw error;
2798
- } finally {
2799
- const url2 = new URL(c.req.url);
2800
- appLogger.info("http.request", {
2801
- method: c.req.method,
2802
- path: url2.pathname,
2803
- query_keys: [...url2.searchParams.keys()].toSorted(),
2804
- status: c.res.status,
2805
- duration_ms: Math.round(performance.now() - startedAt),
2806
- error: thrown instanceof Error ? thrown.message : void 0
2807
- });
2808
- }
2809
- });
2810
3109
  if (transport.kind === "trusted-proxy") {
2811
3110
  app.use("/api/*", requireProxyTls());
2812
3111
  }
@@ -2825,7 +3124,8 @@ async function createServer(port, store, options = {}) {
2825
3124
  defaultSessionTo: options.defaultSessionTo,
2826
3125
  defaultSessionDays: options.defaultSessionDays,
2827
3126
  shutdownSignal: shutdownController.signal,
2828
- projectIdentityResolver
3127
+ projectIdentityResolver,
3128
+ loadSessionDetail: sessionDetailLoader.load
2829
3129
  };
2830
3130
  app.route("/api", createApiRoutes(store, store, routeOptions));
2831
3131
  const webDistPath = options.webDistPath ?? findWebDistPath();
@@ -2909,6 +3209,7 @@ async function createServer(port, store, options = {}) {
2909
3209
  appLogger.info("server.shutdown", { port: actualPort });
2910
3210
  shutdownController.abort();
2911
3211
  try {
3212
+ await sessionDetailLoader.shutdown();
2912
3213
  appLogger.info("server.shutdown.phase", { phase: "http-closing", port: actualPort });
2913
3214
  await closeHttpServer(server, actualPort);
2914
3215
  } finally {
@@ -3074,14 +3375,17 @@ var AgentOperationScheduler = class {
3074
3375
  const run = previous.then(async () => {
3075
3376
  if (this.isStopped) return "skipped";
3076
3377
  const lifecycle = this.beginOperation(agentName, kind);
3077
- try {
3078
- const result = await operation();
3079
- this.completeOperation(lifecycle, result);
3080
- return result;
3081
- } catch (error) {
3082
- this.completeOperation(lifecycle, "failed");
3083
- throw error;
3084
- }
3378
+ return appLogger.runWithContext({ operation_id: lifecycle.operationId }, async () => {
3379
+ this.logOperationStarted(lifecycle);
3380
+ try {
3381
+ const result = await operation();
3382
+ this.completeOperation(lifecycle, result);
3383
+ return result;
3384
+ } catch (error) {
3385
+ this.completeOperation(lifecycle, "failed");
3386
+ throw error;
3387
+ }
3388
+ });
3085
3389
  });
3086
3390
  const tail = run.then(
3087
3391
  () => void 0,
@@ -3139,13 +3443,21 @@ var AgentOperationScheduler = class {
3139
3443
  const generation = (this.operationGenerations.get(agentName) ?? 0) + 1;
3140
3444
  const startedAt = Date.now();
3141
3445
  this.operationGenerations.set(agentName, generation);
3142
- appLogger.info("scan.agent_operation.started", {
3143
- agent: agentName,
3144
- operation: kind,
3446
+ return {
3447
+ agentName,
3448
+ kind,
3145
3449
  generation,
3146
- started_at: startedAt
3450
+ operationId: `${agentName}:${generation}`,
3451
+ startedAt
3452
+ };
3453
+ }
3454
+ logOperationStarted(lifecycle) {
3455
+ appLogger.info("scan.agent_operation.started", {
3456
+ agent: lifecycle.agentName,
3457
+ operation: lifecycle.kind,
3458
+ generation: lifecycle.generation,
3459
+ started_at: lifecycle.startedAt
3147
3460
  });
3148
- return { agentName, kind, generation, startedAt };
3149
3461
  }
3150
3462
  completeOperation(lifecycle, result) {
3151
3463
  const completedAt = Date.now();
@@ -3256,23 +3568,27 @@ var BackfillLifecycle = class {
3256
3568
  var LiveSessionIndex = class {
3257
3569
  agents = [];
3258
3570
  agentsByName = /* @__PURE__ */ new Map();
3571
+ queryScope;
3572
+ allByAgent = {};
3259
3573
  byAgent = {};
3260
3574
  sessions = [];
3261
3575
  cacheFailures = {};
3262
3576
  scanFailures = {};
3263
3577
  signatureCaches = /* @__PURE__ */ new Map();
3264
3578
  initialize(snapshot, options = {}) {
3579
+ this.queryScope = options.queryScope;
3265
3580
  const agentMap = /* @__PURE__ */ new Map();
3266
3581
  for (const agent of snapshot.agents) agentMap.set(agent.name, agent);
3267
3582
  for (const agent of options.registeredAgents ?? []) {
3268
3583
  if (!agentMap.has(agent.name)) agentMap.set(agent.name, agent);
3269
3584
  }
3270
3585
  this.agents = [...agentMap.values()].filter(
3271
- (agent) => !options.allowedAgents || options.allowedAgents.has(agent.name.toLowerCase())
3586
+ (agent) => !this.queryScope?.agents?.length || this.queryScope.agents.includes(agent.name.toLowerCase())
3272
3587
  );
3273
3588
  this.agentsByName = new Map(this.agents.map((agent) => [agent.name, agent]));
3274
3589
  this.cacheFailures = { ...snapshot.cacheFailures };
3275
3590
  this.scanFailures = { ...snapshot.scanFailures };
3591
+ this.allByAgent = {};
3276
3592
  this.byAgent = Object.fromEntries(
3277
3593
  this.agents.flatMap((agent) => {
3278
3594
  if (this.scanFailures[agent.name] && !(agent.name in snapshot.byAgent)) return [];
@@ -3281,7 +3597,8 @@ var LiveSessionIndex = class {
3281
3597
  assertSessionIdentity(session, agent.name);
3282
3598
  assertIdentifiedSessionHead(session);
3283
3599
  }
3284
- return [[agent.name, sortSessions(sessions)]];
3600
+ this.allByAgent[agent.name] = sortSessions(sessions);
3601
+ return [[agent.name, this.visibleSessions(this.allByAgent[agent.name])]];
3285
3602
  })
3286
3603
  );
3287
3604
  this.sessions = mergeSortedSessions(Object.values(this.byAgent));
@@ -3299,11 +3616,15 @@ var LiveSessionIndex = class {
3299
3616
  findAgent(agentName) {
3300
3617
  return this.agentsByName.get(agentName);
3301
3618
  }
3619
+ agentSessions(agentName) {
3620
+ return this.allByAgent[agentName] ?? [];
3621
+ }
3302
3622
  commitAgentSessions(agentName, nextSessions, candidateChangedIds = []) {
3303
3623
  for (const session of nextSessions) {
3304
3624
  assertSessionIdentity(session, agentName);
3305
3625
  assertIdentifiedSessionHead(session);
3306
3626
  }
3627
+ const visibleSessions = this.visibleSessions(nextSessions);
3307
3628
  delete this.cacheFailures[agentName];
3308
3629
  delete this.scanFailures[agentName];
3309
3630
  const previousSessions = this.byAgent[agentName] ?? [];
@@ -3311,15 +3632,18 @@ var LiveSessionIndex = class {
3311
3632
  const signatureCache = this.signatureCache(agentName);
3312
3633
  const { changes, removedSessionIds, counts } = computeSessionDiff(
3313
3634
  previousSessions,
3314
- nextSessions,
3635
+ visibleSessions,
3315
3636
  candidateChangedIds,
3316
3637
  sessionSignature,
3317
3638
  signatureCache
3318
3639
  );
3319
3640
  for (const removedId of removedSessionIds) signatureCache.delete(removedId);
3320
- this.byAgent[agentName] = sortSessions(nextSessions);
3641
+ this.allByAgent[agentName] = sortSessions(nextSessions);
3642
+ const hasChanges = counts.new > 0 || counts.updated > 0 || counts.removed > 0;
3643
+ if (!hasChanges && agentName in this.byAgent) return null;
3644
+ this.byAgent[agentName] = this.visibleSessions(this.allByAgent[agentName]);
3321
3645
  this.sessions = mergeSortedSessions(Object.values(this.byAgent));
3322
- if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) return null;
3646
+ if (!hasChanges) return null;
3323
3647
  const changedSessionHeads = changes.map(({ session }) => ({
3324
3648
  reference: session.reference,
3325
3649
  session
@@ -3358,12 +3682,16 @@ var LiveSessionIndex = class {
3358
3682
  this.signatureCaches.set(agentName, cache);
3359
3683
  return cache;
3360
3684
  }
3685
+ visibleSessions(sessions) {
3686
+ if (!this.queryScope?.projectScope) return sessions;
3687
+ return sessions.filter((session) => matchesSessionQueryScope(session, this.queryScope));
3688
+ }
3361
3689
  };
3362
3690
 
3363
3691
  // src/search-index-job-runner.ts
3364
3692
  import { existsSync as existsSync2 } from "fs";
3365
3693
  import { fileURLToPath as fileURLToPath2 } from "url";
3366
- import { Worker as Worker2 } from "worker_threads";
3694
+ import { Worker as Worker3 } from "worker_threads";
3367
3695
 
3368
3696
  // src/errors.ts
3369
3697
  function toError2(value) {
@@ -3371,14 +3699,24 @@ function toError2(value) {
3371
3699
  }
3372
3700
 
3373
3701
  // src/pending-search-index-jobs.ts
3702
+ var LOG_CONTEXT_KEYS = ["request_id", "operation_id", "publication_id"];
3703
+ function commonLogContext(left, right) {
3704
+ const common = {};
3705
+ for (const key of LOG_CONTEXT_KEYS) {
3706
+ if (left[key] !== void 0 && left[key] === right[key]) common[key] = left[key];
3707
+ }
3708
+ return common;
3709
+ }
3374
3710
  var PendingSearchIndexJobBatch = class {
3375
- constructor(id, context, jobs, waiter) {
3711
+ constructor(id, context, jobs, waiter, logContext) {
3376
3712
  this.id = id;
3377
3713
  this.context = context;
3378
- this.merge(context, jobs, waiter);
3714
+ this.logContext = logContext;
3715
+ this.merge(context, jobs, waiter, logContext);
3379
3716
  }
3380
3717
  id;
3381
3718
  context;
3719
+ logContext;
3382
3720
  jobsByAgent = /* @__PURE__ */ new Map();
3383
3721
  waiters = [];
3384
3722
  settled = false;
@@ -3420,8 +3758,9 @@ var PendingSearchIndexJobBatch = class {
3420
3758
  }
3421
3759
  }
3422
3760
  }
3423
- merge(context, jobs, waiter) {
3761
+ merge(context, jobs, waiter, logContext) {
3424
3762
  this.context = context;
3763
+ this.logContext = commonLogContext(this.logContext, logContext);
3425
3764
  this.waiters.push(waiter);
3426
3765
  for (const job of jobs) this.mergeJob(job);
3427
3766
  }
@@ -3459,7 +3798,7 @@ var PendingSearchIndexJobs = class {
3459
3798
  get changeCount() {
3460
3799
  return this.pendingBatch?.changeCount ?? 0;
3461
3800
  }
3462
- enqueue(id, context, jobs, onStarted, onProgress) {
3801
+ enqueue(id, context, jobs, onStarted, onProgress, logContext = {}) {
3463
3802
  if (jobs.length === 0) return Promise.resolve();
3464
3803
  return new Promise((resolve4, reject) => {
3465
3804
  const waiter = {
@@ -3470,9 +3809,9 @@ var PendingSearchIndexJobs = class {
3470
3809
  agentNames: new Set(jobs.map((job) => job.agentName))
3471
3810
  };
3472
3811
  if (this.pendingBatch) {
3473
- this.pendingBatch.merge(context, jobs, waiter);
3812
+ this.pendingBatch.merge(context, jobs, waiter, logContext);
3474
3813
  } else {
3475
- this.pendingBatch = new PendingSearchIndexJobBatch(id, context, jobs, waiter);
3814
+ this.pendingBatch = new PendingSearchIndexJobBatch(id, context, jobs, waiter, logContext);
3476
3815
  this.batches.add(this.pendingBatch);
3477
3816
  }
3478
3817
  });
@@ -3560,6 +3899,7 @@ var SearchIndexJobRunner = class {
3560
3899
  pendingJobs = new PendingSearchIndexJobs();
3561
3900
  pendingMaintenanceJobs = new PendingSearchIndexJobs();
3562
3901
  isShuttingDown = false;
3902
+ retirements = /* @__PURE__ */ new Set();
3563
3903
  enqueue(context, jobs, onStarted, onProgress) {
3564
3904
  return this.enqueueIn(this.pendingJobs, context, jobs, onStarted, onProgress);
3565
3905
  }
@@ -3570,7 +3910,14 @@ var SearchIndexJobRunner = class {
3570
3910
  if (jobs.length === 0) return Promise.resolve();
3571
3911
  if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE2));
3572
3912
  const batchId = this.nextBatchId++;
3573
- const completion = queue.enqueue(batchId, context, jobs, onStarted, onProgress);
3913
+ const completion = queue.enqueue(
3914
+ batchId,
3915
+ context,
3916
+ jobs,
3917
+ onStarted,
3918
+ onProgress,
3919
+ appLogger.captureContext()
3920
+ );
3574
3921
  if (this.activeBatch) {
3575
3922
  const snapshot = this.snapshot();
3576
3923
  appLogger.debug("search_index.worker_queued", {
@@ -3606,25 +3953,33 @@ var SearchIndexJobRunner = class {
3606
3953
  if (activeBatch) this.settle(activeBatch, shutdownError);
3607
3954
  this.pendingJobs.rejectAll(shutdownError);
3608
3955
  this.pendingMaintenanceJobs.rejectAll(shutdownError);
3609
- if (worker) await worker.terminate();
3956
+ const activeRetirement = worker ? this.retireWorker(worker) : null;
3957
+ const retirements = [...this.retirements];
3958
+ try {
3959
+ if (activeRetirement) await activeRetirement;
3960
+ } finally {
3961
+ await Promise.allSettled(retirements);
3962
+ }
3610
3963
  }
3611
3964
  startNextBatch() {
3612
3965
  if (this.isShuttingDown || this.activeBatch) return;
3613
3966
  const batch = this.pendingJobs.take() ?? this.pendingMaintenanceJobs.take();
3614
3967
  if (!batch) return;
3615
- appLogger.info("search_index.worker_dequeued", {
3616
- batch_id: batch.id,
3617
- context: batch.context,
3618
- pending_batches: this.pendingJobs.batchCount
3619
- });
3620
- batch.start((error) => {
3621
- appLogger.error("search_index.worker_start_listener_failed", {
3968
+ appLogger.restoreContext(batch.logContext, () => {
3969
+ appLogger.info("search_index.worker_dequeued", {
3622
3970
  batch_id: batch.id,
3623
3971
  context: batch.context,
3624
- error: toError2(error)
3972
+ pending_batches: this.pendingJobs.batchCount
3973
+ });
3974
+ batch.start((error) => {
3975
+ appLogger.error("search_index.worker_start_listener_failed", {
3976
+ batch_id: batch.id,
3977
+ context: batch.context,
3978
+ error: toError2(error)
3979
+ });
3625
3980
  });
3981
+ this.startBatch(batch);
3626
3982
  });
3627
- this.startBatch(batch);
3628
3983
  }
3629
3984
  startBatch(batch) {
3630
3985
  if (this.isShuttingDown) {
@@ -3635,6 +3990,7 @@ var SearchIndexJobRunner = class {
3635
3990
  type: "run",
3636
3991
  pricingGenerationId: getPricingGeneration().id,
3637
3992
  context: batch.context,
3993
+ logContext: batch.logContext,
3638
3994
  jobs: batch.jobs
3639
3995
  };
3640
3996
  const existingWorker = this.worker;
@@ -3661,7 +4017,7 @@ var SearchIndexJobRunner = class {
3661
4017
  });
3662
4018
  let worker;
3663
4019
  try {
3664
- worker = new Worker2(workerUrl, { workerData: request });
4020
+ worker = new Worker3(workerUrl, { workerData: request });
3665
4021
  } catch (error) {
3666
4022
  this.settle(batch, toError2(error));
3667
4023
  this.startNextBatch();
@@ -3674,62 +4030,76 @@ var SearchIndexJobRunner = class {
3674
4030
  if (this.worker !== worker) return;
3675
4031
  const activeBatch = this.activeBatch;
3676
4032
  if (!activeBatch) return;
3677
- if (message.type === "publication-progress") {
3678
- const { agentName, stage } = message;
3679
- activeBatch.progress({ agentName, stage }, (error) => {
3680
- appLogger.error("search_index.worker_progress_listener_failed", {
3681
- batch_id: activeBatch.id,
3682
- context: activeBatch.context,
3683
- error: toError2(error)
3684
- });
3685
- });
3686
- return;
3687
- }
3688
- if (message.type === "sync-result") {
3689
- logSearchIndexSync(message.context, message.result);
3690
- return;
3691
- }
3692
- if (message.type === "persist-failed") {
3693
- appLogger.error("search_index.persist_failed", {
3694
- batch_id: activeBatch.id,
3695
- context: message.context,
3696
- stage: message.stage,
3697
- publication_id: message.publicationId,
3698
- agent: message.agentName,
3699
- sessions: message.sessions
4033
+ appLogger.restoreContext(activeBatch.logContext, () => {
4034
+ this.handleWorkerMessage(activeBatch, message);
4035
+ });
4036
+ });
4037
+ worker.on("error", (error) => {
4038
+ const activeBatch = this.worker === worker ? this.activeBatch : null;
4039
+ appLogger.restoreContext(activeBatch?.logContext ?? {}, () => {
4040
+ appLogger.error("search_index.worker_error", {
4041
+ context: activeBatch?.context,
4042
+ error
3700
4043
  });
3701
- if (message.fatal) {
3702
- this.finishBatch(
3703
- activeBatch,
3704
- new Error(
3705
- `Search index worker failed to persist ${message.stage} for ${message.agentName}`
3706
- )
3707
- );
3708
- }
3709
- return;
3710
- }
3711
- if (message.type !== "done") return;
3712
- if (message.failedAgents.length > 0) {
3713
- appLogger.warn("search_index.batch_partial", {
4044
+ this.invalidateWorker(worker, toError2(error));
4045
+ });
4046
+ });
4047
+ worker.on("exit", (code) => {
4048
+ const activeBatch = this.worker === worker ? this.activeBatch : null;
4049
+ appLogger.restoreContext(
4050
+ activeBatch?.logContext ?? {},
4051
+ () => this.finishWorker(worker, code)
4052
+ );
4053
+ });
4054
+ }
4055
+ handleWorkerMessage(activeBatch, message) {
4056
+ if (message.type === "publication-progress") {
4057
+ const { agentName, stage } = message;
4058
+ activeBatch.progress({ agentName, stage }, (error) => {
4059
+ appLogger.error("search_index.worker_progress_listener_failed", {
3714
4060
  batch_id: activeBatch.id,
3715
- context: message.context,
3716
- failed_agents: message.failedAgents
4061
+ context: activeBatch.context,
4062
+ error: toError2(error)
3717
4063
  });
3718
- }
3719
- appLogger.info(`${message.context}.done`, {
3720
- duration_ms: Math.round(message.durationMs),
4064
+ });
4065
+ return;
4066
+ }
4067
+ if (message.type === "sync-result") {
4068
+ logSearchIndexSync(message.context, message.result);
4069
+ return;
4070
+ }
4071
+ if (message.type === "persist-failed") {
4072
+ appLogger.error("search_index.persist_failed", {
4073
+ batch_id: activeBatch.id,
4074
+ context: message.context,
4075
+ stage: message.stage,
4076
+ publication_id: message.publicationId,
4077
+ agent: message.agentName,
3721
4078
  sessions: message.sessions
3722
4079
  });
3723
- this.finishBatch(activeBatch);
3724
- });
3725
- worker.on("error", (error) => {
3726
- appLogger.error("search_index.worker_error", {
3727
- context: this.activeBatch?.context ?? batch.context,
3728
- error
4080
+ if (message.fatal) {
4081
+ this.finishBatch(
4082
+ activeBatch,
4083
+ new Error(
4084
+ `Search index worker failed to persist ${message.stage} for ${message.agentName}`
4085
+ )
4086
+ );
4087
+ }
4088
+ return;
4089
+ }
4090
+ if (message.type !== "done") return;
4091
+ if (message.failedAgents.length > 0) {
4092
+ appLogger.warn("search_index.batch_partial", {
4093
+ batch_id: activeBatch.id,
4094
+ context: message.context,
4095
+ failed_agents: message.failedAgents
3729
4096
  });
3730
- this.invalidateWorker(worker, toError2(error));
4097
+ }
4098
+ appLogger.info(`${message.context}.done`, {
4099
+ duration_ms: Math.round(message.durationMs),
4100
+ sessions: message.sessions
3731
4101
  });
3732
- worker.on("exit", (code) => this.finishWorker(worker, code));
4102
+ this.finishBatch(activeBatch);
3733
4103
  }
3734
4104
  finishBatch(batch, error) {
3735
4105
  if (this.activeBatch !== batch) return;
@@ -3743,7 +4113,7 @@ var SearchIndexJobRunner = class {
3743
4113
  const batch = this.activeBatch;
3744
4114
  this.activeBatch = null;
3745
4115
  if (batch) this.settle(batch, error);
3746
- void worker.terminate();
4116
+ void this.retireWorker(worker);
3747
4117
  this.startNextBatch();
3748
4118
  }
3749
4119
  finishWorker(worker, code) {
@@ -3767,15 +4137,23 @@ var SearchIndexJobRunner = class {
3767
4137
  this.startNextBatch();
3768
4138
  }
3769
4139
  settle(batch, error) {
3770
- if (!this.pendingJobs.settle(batch, error) && !this.pendingMaintenanceJobs.settle(batch, error)) {
3771
- return;
3772
- }
3773
- appLogger.info("search_index.worker_settled", {
3774
- batch_id: batch.id,
3775
- context: batch.context,
3776
- result: error ? "rejected" : "resolved"
4140
+ appLogger.restoreContext(batch.logContext, () => {
4141
+ if (!this.pendingJobs.settle(batch, error) && !this.pendingMaintenanceJobs.settle(batch, error)) {
4142
+ return;
4143
+ }
4144
+ appLogger.info("search_index.worker_settled", {
4145
+ batch_id: batch.id,
4146
+ context: batch.context,
4147
+ result: error ? "rejected" : "resolved"
4148
+ });
3777
4149
  });
3778
4150
  }
4151
+ retireWorker(worker) {
4152
+ const retirement = terminateWorkerAfterLogDrain(worker);
4153
+ this.retirements.add(retirement);
4154
+ void retirement.finally(() => this.retirements.delete(retirement)).catch(() => void 0);
4155
+ return retirement;
4156
+ }
3779
4157
  workerUrl() {
3780
4158
  const workerUrl = new URL("./search-index-worker.js", import.meta.url);
3781
4159
  if (workerUrl.protocol === "file:" && !existsSync2(fileURLToPath2(workerUrl))) return null;
@@ -3862,19 +4240,21 @@ var SearchIndexMaintenanceScheduler = class {
3862
4240
  if (outcome.status === "failed") {
3863
4241
  throw new Error(`Session cache read failed for ${agentName}`);
3864
4242
  }
3865
- cached = outcome.value ?? void 0;
3866
- }
3867
- if (!cached) throw new Error(`Session cache is unavailable for ${agentName}`);
3868
- this.cachedSessionsByAgent.set(agentName, cached);
3869
- const sessionsById = new Map(
3870
- cached.sessions.map((session, sortIndex) => [
3871
- session.reference.sessionId,
3872
- { session, sortIndex }
3873
- ])
3874
- );
4243
+ if (!outcome.value) throw new Error(`Session cache is unavailable for ${agentName}`);
4244
+ cached = {
4245
+ sessionsById: new Map(
4246
+ outcome.value.sessions.map((session, sortIndex) => [
4247
+ session.reference.sessionId,
4248
+ { session, sortIndex }
4249
+ ])
4250
+ ),
4251
+ meta: outcome.value.meta
4252
+ };
4253
+ this.cachedSessionsByAgent.set(agentName, cached);
4254
+ }
3875
4255
  const changes = pending.sessionIds.flatMap(
3876
4256
  (sessionId) => {
3877
- const change = sessionsById.get(sessionId);
4257
+ const change = cached.sessionsById.get(sessionId);
3878
4258
  return change ? [change] : [];
3879
4259
  }
3880
4260
  );
@@ -4486,7 +4866,7 @@ var SearchIndexPublisher = class {
4486
4866
  kind: "full",
4487
4867
  context,
4488
4868
  agentName: agent.name,
4489
- sessions: snapshot.byAgent[agent.name] ?? [],
4869
+ sessions: this.options.agentSessions(agent.name),
4490
4870
  meta: buildAgentCacheMeta(agent),
4491
4871
  completeness: "partial",
4492
4872
  removedSessionIds: [],
@@ -4566,7 +4946,7 @@ var SessionPublicationCoordinator = class {
4566
4946
  publication.candidateChangedIds
4567
4947
  );
4568
4948
  const diffDuration = performance.now() - diffStartedAt;
4569
- const result = { durableCommitted: true, event, diffDuration };
4949
+ const result = { event, diffDuration };
4570
4950
  this.runPostCommit(publication, "worker_commit", () => publication.stagedRun?.commit());
4571
4951
  this.runPostCommit(publication, "callback", () => publication.onCommitted?.(result));
4572
4952
  this.runPostCommit(
@@ -4632,7 +5012,6 @@ function countSessionUpdates(event) {
4632
5012
  }
4633
5013
  var REFRESH_DEBOUNCE_MS = 200;
4634
5014
  var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
4635
- var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
4636
5015
  var SOURCE_FAILURE_SUMMARY_MAX_LENGTH = 160;
4637
5016
  function buildScanCompletion(completeness, sourceFailures) {
4638
5017
  if (sourceFailures.length === 0) return { completeness };
@@ -4644,6 +5023,15 @@ function buildScanCompletion(completeness, sourceFailures) {
4644
5023
  sourceFailureSummary: summary.length > SOURCE_FAILURE_SUMMARY_MAX_LENGTH ? `${summary.slice(0, SOURCE_FAILURE_SUMMARY_MAX_LENGTH - 1)}\u2026` : summary
4645
5024
  };
4646
5025
  }
5026
+ function selectCacheMeta(meta, sessionIds) {
5027
+ if (!sessionIds) return { ...meta };
5028
+ const selected = {};
5029
+ for (const sessionId of sessionIds) {
5030
+ const entry = meta[sessionId];
5031
+ if (entry) selected[sessionId] = entry;
5032
+ }
5033
+ return selected;
5034
+ }
4647
5035
  var AgentSyncEngine = class {
4648
5036
  constructor(options) {
4649
5037
  this.options = options;
@@ -4656,6 +5044,7 @@ var AgentSyncEngine = class {
4656
5044
  this.indexPublisher = new SearchIndexPublisher({
4657
5045
  jobs: this.searchIndexJobs,
4658
5046
  snapshot: () => this.sessionIndex.snapshot(),
5047
+ agentSessions: (agentName) => this.sessionIndex.agentSessions(agentName),
4659
5048
  readCachedSessions: (agentName) => this.readCachedSessionsOrWarn("search.index", agentName)
4660
5049
  });
4661
5050
  this.sessionPublication = new SessionPublicationCoordinator(
@@ -4781,11 +5170,21 @@ var AgentSyncEngine = class {
4781
5170
  reportPostCommitError(operation, agentName, error) {
4782
5171
  appLogger.error(`${operation}.post_commit_error`, { agent: agentName, error });
4783
5172
  }
4784
- commitRefreshChangeCheck(agentName, commit) {
5173
+ commitAgentState(operation, agent, state) {
5174
+ if (!state) return;
4785
5175
  try {
4786
- commit?.();
5176
+ agent.restoreSessionCacheMeta(state.meta);
5177
+ if (state.refreshedAt != null) {
5178
+ this.lastRefreshAtByAgent.set(agent.name, state.refreshedAt);
5179
+ }
4787
5180
  } catch (error) {
4788
- this.reportPostCommitError("scan.refresh", agentName, error);
5181
+ this.reportPostCommitError(operation, agent.name, error);
5182
+ return;
5183
+ }
5184
+ try {
5185
+ state.refreshTransaction?.commit();
5186
+ } catch (error) {
5187
+ this.reportPostCommitError(operation, agent.name, error);
4789
5188
  }
4790
5189
  }
4791
5190
  finishCommittedAgentScan(agentName, completion) {
@@ -4853,12 +5252,11 @@ var AgentSyncEngine = class {
4853
5252
  appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
4854
5253
  return { result: "skipped", completion: { completeness: "complete" } };
4855
5254
  }
4856
- const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
5255
+ const previousSessions = this.sessionIndex.agentSessions(agentName);
4857
5256
  const refreshBaseline = cached?.sessions ?? previousSessions;
4858
5257
  const cacheTimestamp = cached?.timestamp ?? this.lastRefreshAtByAgent.get(agentName) ?? 0;
4859
5258
  if (cached) agent.restoreSessionCacheMeta(cached.meta);
4860
5259
  const durableMeta = agent.snapshotSessionCacheMeta();
4861
- const durableLastRefreshAt = this.lastRefreshAtByAgent.get(agentName);
4862
5260
  const initialization = readAgentCacheInitialization(agentName);
4863
5261
  if (initialization.status === "failed") {
4864
5262
  appLogger.warn("scan.refresh.cache_state_unavailable", {
@@ -4868,40 +5266,50 @@ var AgentSyncEngine = class {
4868
5266
  return { result: "unchanged", completion: { completeness: "complete" } };
4869
5267
  }
4870
5268
  const isInitialized = initialization.value;
4871
- const refresh = await selectAgentRefresh(agent, {
5269
+ const refreshTransaction = await beginAgentRefresh(agent, {
4872
5270
  initialized: isInitialized,
4873
5271
  sinceTimestamp: cacheTimestamp,
4874
5272
  cachedSessions: refreshBaseline
4875
5273
  });
5274
+ const refresh = refreshTransaction.selection;
4876
5275
  const availabilityDuration = refresh.availabilityDurationMs;
4877
5276
  let strategyResult;
4878
- if (refresh.kind === "unavailable") {
4879
- strategyResult = this.refreshUnavailableAgent(agentName);
4880
- } else if (refresh.kind === "initialize") {
4881
- strategyResult = await this.initializeAgent(agent, previousSessions);
4882
- } else if (refresh.kind === "synchronize") {
4883
- strategyResult = await this.syncAgentSources(
4884
- agent,
4885
- cached ?? {
4886
- sessions: refreshBaseline,
4887
- meta: durableMeta
4888
- },
4889
- startedAt
4890
- );
4891
- } else {
4892
- strategyResult = await this.refreshChangedAgent(agent, refresh, refreshBaseline, startedAt);
5277
+ try {
5278
+ if (refresh.kind === "unavailable") {
5279
+ strategyResult = this.refreshUnavailableAgent(agentName);
5280
+ } else if (refresh.kind === "initialize") {
5281
+ strategyResult = await this.initializeAgent(agent, previousSessions);
5282
+ } else if (refresh.kind === "synchronize") {
5283
+ strategyResult = await this.syncAgentSources(
5284
+ agent,
5285
+ cached ?? {
5286
+ sessions: refreshBaseline,
5287
+ meta: durableMeta
5288
+ },
5289
+ startedAt
5290
+ );
5291
+ } else {
5292
+ strategyResult = await this.refreshChangedAgent(
5293
+ agent,
5294
+ refresh,
5295
+ refreshTransaction,
5296
+ refreshBaseline,
5297
+ startedAt
5298
+ );
5299
+ }
5300
+ } finally {
5301
+ agent.restoreSessionCacheMeta(durableMeta);
4893
5302
  }
4894
5303
  const stagedRun = strategyResult.workerRun;
4895
5304
  if (strategyResult.status === "unchanged") {
4896
5305
  stagedRun?.commit();
4897
- this.commitRefreshChangeCheck(agentName, strategyResult.commitChangeCheck);
5306
+ this.commitAgentState("scan.refresh", agent, strategyResult.pendingAgentState);
4898
5307
  return {
4899
5308
  result: "unchanged",
4900
5309
  completion: buildScanCompletion(strategyResult.completeness, strategyResult.sourceFailures)
4901
5310
  };
4902
5311
  }
4903
5312
  const nextSessions = attachMissingProjectIdentities(strategyResult.nextSessions);
4904
- const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
4905
5313
  const persistenceDiff = strategyResult.publication.kind === "changes" ? strategyResult.publication.diff : null;
4906
5314
  const publicationSessions = strategyResult.publication.kind === "full" ? strategyResult.publication.sessions : nextSessions;
4907
5315
  const explicitRemovedSessionIds = strategyResult.publication.kind === "full" ? strategyResult.publication.explicitRemovedSessionIds : [];
@@ -4923,6 +5331,7 @@ var AgentSyncEngine = class {
4923
5331
  delete_candidates: replacementDeleteCandidates
4924
5332
  });
4925
5333
  const changedSessionIds = persistenceDiff ? new Set(persistenceDiff.changedSessions.map(({ session }) => session.reference.sessionId)) : void 0;
5334
+ const pendingMeta = strategyResult.pendingAgentState?.meta ?? durableMeta;
4926
5335
  const persistStartedAt = performance.now();
4927
5336
  const persistentJob = persistenceDiff ? {
4928
5337
  kind: "changes",
@@ -4930,18 +5339,16 @@ var AgentSyncEngine = class {
4930
5339
  agentName,
4931
5340
  changes: persistenceDiff.changedSessions,
4932
5341
  removedSessionIds: persistenceDiff.removedSessionIds,
4933
- meta: buildAgentCacheMeta(agent, changedSessionIds),
4934
- ...searchIndexOptions ? { searchIndexOptions } : {}
5342
+ meta: selectCacheMeta(pendingMeta, changedSessionIds)
4935
5343
  } : {
4936
5344
  kind: "full",
4937
5345
  context: "scan.refresh",
4938
5346
  agentName,
4939
5347
  sessions: publicationSessions,
4940
- meta: buildAgentCacheMeta(agent),
5348
+ meta: selectCacheMeta(pendingMeta),
4941
5349
  completeness: strategyResult.completeness,
4942
5350
  removedSessionIds: explicitRemovedSessionIds,
4943
- saveCache: true,
4944
- ...searchIndexOptions ? { searchIndexOptions } : {}
5351
+ saveCache: true
4945
5352
  };
4946
5353
  const completion = buildScanCompletion(
4947
5354
  strategyResult.completeness,
@@ -4961,12 +5368,9 @@ var AgentSyncEngine = class {
4961
5368
  });
4962
5369
  } catch (error) {
4963
5370
  stagedRun?.discard();
4964
- agent.restoreSessionCacheMeta(durableMeta);
4965
- if (durableLastRefreshAt == null) this.lastRefreshAtByAgent.delete(agentName);
4966
- else this.lastRefreshAtByAgent.set(agentName, durableLastRefreshAt);
4967
5371
  throw error;
4968
5372
  }
4969
- this.commitRefreshChangeCheck(agentName, strategyResult.commitChangeCheck);
5373
+ this.commitAgentState("scan.refresh", agent, strategyResult.pendingAgentState);
4970
5374
  const persistDuration = performance.now() - persistStartedAt;
4971
5375
  const totalDurationMs = performance.now() - startedAt;
4972
5376
  try {
@@ -5011,7 +5415,7 @@ var AgentSyncEngine = class {
5011
5415
  * empty database.
5012
5416
  */
5013
5417
  refreshUnavailableAgent(agentName) {
5014
- const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
5418
+ const previousSessions = this.sessionIndex.agentSessions(agentName);
5015
5419
  if (previousSessions.length > 0) {
5016
5420
  appLogger.warn("scan.refresh.agent_unavailable", {
5017
5421
  agent: agentName,
@@ -5035,14 +5439,13 @@ var AgentSyncEngine = class {
5035
5439
  const workerRun = await this.runWorker(agent, previousSessions, { kind: "full-scan" }, scope);
5036
5440
  try {
5037
5441
  const { result } = workerRun;
5038
- agent.restoreSessionCacheMeta(result.meta);
5039
5442
  const sessions = attachMissingProjectIdentities(result.sessions);
5040
- this.lastRefreshAtByAgent.set(agent.name, Date.now());
5041
5443
  return {
5042
5444
  ...this.refreshStrategyBase(sessions, result.completeness, scope, {
5043
5445
  scanDuration: performance.now() - scanStartedAt,
5044
5446
  sourceFailures: result.sourceFailures ?? [],
5045
- workerRun
5447
+ workerRun,
5448
+ pendingAgentState: { meta: result.meta, refreshedAt: Date.now() }
5046
5449
  }),
5047
5450
  status: "continue",
5048
5451
  publication: {
@@ -5070,19 +5473,19 @@ var AgentSyncEngine = class {
5070
5473
  );
5071
5474
  try {
5072
5475
  const { result } = workerRun;
5073
- agent.restoreSessionCacheMeta(result.meta);
5074
5476
  const sessions = attachMissingProjectIdentities(result.sessions);
5075
5477
  const preciseChangedIds = result.changedIds ?? [];
5076
5478
  const persistenceDiff = buildSessionPersistenceDiff(baseline.sessions, sessions, {
5077
5479
  candidateChangedIds: preciseChangedIds
5078
5480
  });
5079
- this.lastRefreshAtByAgent.set(agent.name, Date.now());
5481
+ const pendingAgentState = { meta: result.meta, refreshedAt: Date.now() };
5080
5482
  if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0 && (result.sourceFailures?.length ?? 0) === 0) {
5081
5483
  this.logUnchangedRefresh(agent.name, refreshStartedAt);
5082
5484
  return {
5083
5485
  ...this.refreshStrategyBase(sessions, result.completeness, scope, {
5084
5486
  scanDuration: performance.now() - scanStartedAt,
5085
- workerRun
5487
+ workerRun,
5488
+ pendingAgentState
5086
5489
  }),
5087
5490
  status: "unchanged"
5088
5491
  };
@@ -5091,7 +5494,8 @@ var AgentSyncEngine = class {
5091
5494
  ...this.refreshStrategyBase(sessions, result.completeness, scope, {
5092
5495
  scanDuration: performance.now() - scanStartedAt,
5093
5496
  sourceFailures: result.sourceFailures ?? [],
5094
- workerRun
5497
+ workerRun,
5498
+ pendingAgentState
5095
5499
  }),
5096
5500
  status: "continue",
5097
5501
  publication: {
@@ -5105,7 +5509,7 @@ var AgentSyncEngine = class {
5105
5509
  throw error;
5106
5510
  }
5107
5511
  }
5108
- async refreshChangedAgent(agent, refresh, baseline, refreshStartedAt) {
5512
+ async refreshChangedAgent(agent, refresh, refreshTransaction, baseline, refreshStartedAt) {
5109
5513
  const scope = this.startupScanOptions();
5110
5514
  if (refresh.kind === "failed") {
5111
5515
  appLogger.warn("scan.refresh.change_check_failed", {
@@ -5133,9 +5537,12 @@ var AgentSyncEngine = class {
5133
5537
  const workerRun = await this.runWorker(agent, baseline, { kind: "recompute-derived" }, {});
5134
5538
  try {
5135
5539
  const { result } = workerRun;
5136
- agent.restoreSessionCacheMeta(result.meta);
5137
5540
  const sessions2 = attachMissingProjectIdentities(result.sessions);
5138
- this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
5541
+ const pendingAgentState = {
5542
+ meta: result.meta,
5543
+ refreshedAt: checkResult.timestamp,
5544
+ refreshTransaction
5545
+ };
5139
5546
  const persistenceDiff = buildSessionPersistenceDiff(baseline, sessions2);
5140
5547
  if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0) {
5141
5548
  this.logUnchangedRefresh(agent.name, refreshStartedAt);
@@ -5148,7 +5555,7 @@ var AgentSyncEngine = class {
5148
5555
  checkDuration,
5149
5556
  scanDuration: performance.now() - scanStartedAt2,
5150
5557
  workerRun,
5151
- commitChangeCheck: () => commitAgentRefreshCheck(refresh)
5558
+ pendingAgentState
5152
5559
  }
5153
5560
  ),
5154
5561
  status: "unchanged"
@@ -5164,7 +5571,7 @@ var AgentSyncEngine = class {
5164
5571
  scanDuration: performance.now() - scanStartedAt2,
5165
5572
  sourceFailures: result.sourceFailures ?? [],
5166
5573
  workerRun,
5167
- commitChangeCheck: () => commitAgentRefreshCheck(refresh)
5574
+ pendingAgentState
5168
5575
  }
5169
5576
  ),
5170
5577
  status: "continue",
@@ -5180,16 +5587,18 @@ var AgentSyncEngine = class {
5180
5587
  const workerRun = await this.runWorker(agent, baseline, { kind: "full-scan" }, scope);
5181
5588
  try {
5182
5589
  const { result } = workerRun;
5183
- agent.restoreSessionCacheMeta(result.meta);
5184
5590
  const sessions2 = attachMissingProjectIdentities(result.sessions);
5185
- this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
5186
5591
  return {
5187
5592
  ...this.refreshStrategyBase(sessions2, result.completeness, scope, {
5188
5593
  checkDuration,
5189
5594
  scanDuration: performance.now() - scanStartedAt,
5190
5595
  sourceFailures: result.sourceFailures ?? [],
5191
5596
  workerRun,
5192
- commitChangeCheck: () => commitAgentRefreshCheck(refresh)
5597
+ pendingAgentState: {
5598
+ meta: result.meta,
5599
+ refreshedAt: checkResult.timestamp,
5600
+ refreshTransaction
5601
+ }
5193
5602
  }),
5194
5603
  status: "continue",
5195
5604
  publication: {
@@ -5217,7 +5626,6 @@ var AgentSyncEngine = class {
5217
5626
  source.incrementalScan(baseline, preciseChangedIds, checkResult.refs, scope)
5218
5627
  )
5219
5628
  );
5220
- this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
5221
5629
  const sourceFailures = checkResult.sourceFailures ?? [];
5222
5630
  const completeness = resolveSessionSnapshotCompleteness(scope, sourceFailures);
5223
5631
  return {
@@ -5225,7 +5633,11 @@ var AgentSyncEngine = class {
5225
5633
  checkDuration,
5226
5634
  scanDuration: performance.now() - scanStartedAt,
5227
5635
  sourceFailures,
5228
- commitChangeCheck: () => commitAgentRefreshCheck(refresh)
5636
+ pendingAgentState: {
5637
+ meta: agent.snapshotSessionCacheMeta(),
5638
+ refreshedAt: checkResult.timestamp,
5639
+ refreshTransaction
5640
+ }
5229
5641
  }),
5230
5642
  status: "continue",
5231
5643
  publication: {
@@ -5301,9 +5713,8 @@ var AgentSyncEngine = class {
5301
5713
  const startedAt = performance.now();
5302
5714
  const agent = this.findAgent(agentName);
5303
5715
  if (!agent || !agent.isAvailable()) return "skipped";
5304
- const snapshot = this.sessionIndex.snapshot();
5305
5716
  const cached = this.readCachedSessionsOrWarn("scan.backfill", agentName);
5306
- const baseline = cached?.sessions ?? snapshot.byAgent[agentName] ?? [];
5717
+ const baseline = cached?.sessions ?? this.sessionIndex.agentSessions(agentName);
5307
5718
  const meta = cached?.meta ?? buildAgentCacheMeta(agent);
5308
5719
  const backfillCursor = getAgentFullSyncCursor(agentName);
5309
5720
  if (cached) agent.restoreSessionCacheMeta(cached.meta);
@@ -5337,7 +5748,6 @@ var AgentSyncEngine = class {
5337
5748
  }
5338
5749
  );
5339
5750
  const { result } = workerRun;
5340
- agent.restoreSessionCacheMeta(result.meta);
5341
5751
  const fullSessions = attachMissingProjectIdentities(result.sessions);
5342
5752
  const completion = buildScanCompletion(result.completeness, result.sourceFailures ?? []);
5343
5753
  this.statusReporter.flushProgressStatus(`backfill:${agentName}`);
@@ -5360,7 +5770,7 @@ var AgentSyncEngine = class {
5360
5770
  context: "scan.backfill",
5361
5771
  agentName,
5362
5772
  sessions: fullSessions,
5363
- meta: buildAgentCacheMeta(agent),
5773
+ meta: result.meta,
5364
5774
  completeness: result.completeness,
5365
5775
  removedSessionIds: result.explicitRemovedSessionIds,
5366
5776
  saveCache: true
@@ -5377,6 +5787,7 @@ var AgentSyncEngine = class {
5377
5787
  }
5378
5788
  }
5379
5789
  });
5790
+ this.commitAgentState("scan.backfill", agent, { meta: result.meta });
5380
5791
  try {
5381
5792
  if (completion.completeness === "complete" && !markAgentFullSyncCompleted(agentName)) {
5382
5793
  appLogger.warn("scan.backfill.completion_not_durable", { agent: agentName });
@@ -5402,7 +5813,6 @@ var AgentSyncEngine = class {
5402
5813
  }
5403
5814
  return "committed";
5404
5815
  } catch (error) {
5405
- agent.restoreSessionCacheMeta(meta);
5406
5816
  workerRun?.discard();
5407
5817
  appLogger.error("scan.backfill.error", { agent: agentName, error });
5408
5818
  console.error(`[${agentName}] Backfill failed:`, error);
@@ -5728,9 +6138,9 @@ var SessionWatcher = class {
5728
6138
  };
5729
6139
 
5730
6140
  // src/worker-runner.ts
5731
- import { Worker as Worker3 } from "worker_threads";
6141
+ import { Worker as Worker4 } from "worker_threads";
5732
6142
  var SHUTDOWN_ERROR_MESSAGE3 = "Scan refresh worker shut down";
5733
- function applySessionChanges2(previousSessions, changes, removedSessionIds) {
6143
+ function applySessionChanges(previousSessions, changes, removedSessionIds) {
5734
6144
  if (changes.length === 0 && removedSessionIds.length === 0) return previousSessions;
5735
6145
  const replacedIds = new Set(removedSessionIds);
5736
6146
  for (const { session } of changes) replacedIds.add(session.reference.sessionId);
@@ -5768,6 +6178,7 @@ var ThreadWorkerRunner = class {
5768
6178
  }
5769
6179
  workerUrl;
5770
6180
  workers = /* @__PURE__ */ new Map();
6181
+ retirements = /* @__PURE__ */ new Set();
5771
6182
  nextRequestId = 1;
5772
6183
  isShuttingDown = false;
5773
6184
  get activeCount() {
@@ -5792,6 +6203,7 @@ var ThreadWorkerRunner = class {
5792
6203
  pricingGenerationId: getPricingGeneration().id,
5793
6204
  operation: payload.operation,
5794
6205
  scanOptions: payload.scanOptions,
6206
+ logContext: appLogger.captureContext(),
5795
6207
  ...existingSlot ? {} : { previousSessions: payload.previousSessions, meta: payload.meta }
5796
6208
  };
5797
6209
  return new Promise((resolve4, reject) => {
@@ -5810,6 +6222,7 @@ var ThreadWorkerRunner = class {
5810
6222
  reject,
5811
6223
  payload,
5812
6224
  generation,
6225
+ logContext: request.logContext ?? {},
5813
6226
  onProgress: payload.onProgress,
5814
6227
  onCheckpoint: payload.onCheckpoint
5815
6228
  });
@@ -5838,10 +6251,11 @@ var ThreadWorkerRunner = class {
5838
6251
  for (const pending of slot.pending.values()) pending.reject(shutdownError);
5839
6252
  slot.pending.clear();
5840
6253
  }
5841
- await Promise.allSettled(slots.map((slot) => slot.worker.terminate()));
6254
+ for (const slot of slots) this.retireWorker(slot.worker);
6255
+ await Promise.allSettled(this.retirements);
5842
6256
  }
5843
6257
  createWorker(agentName, request) {
5844
- const worker = new Worker3(this.workerUrl, { workerData: request });
6258
+ const worker = new Worker4(this.workerUrl, { workerData: request });
5845
6259
  const slot = {
5846
6260
  agentName,
5847
6261
  worker,
@@ -5856,21 +6270,31 @@ var ThreadWorkerRunner = class {
5856
6270
  this.handleMessage(slot, message);
5857
6271
  });
5858
6272
  worker.on("error", (error) => {
5859
- this.closeWorker(agentName, slot, toError2(error));
6273
+ appLogger.restoreContext(this.slotLogContext(slot), () => {
6274
+ this.closeWorker(agentName, slot, toError2(error));
6275
+ void this.retireWorker(worker);
6276
+ });
5860
6277
  });
5861
6278
  worker.on("exit", (code) => {
5862
- if (slot.closed) return;
5863
- const error = new Error(`Scan refresh worker exited before completing (code ${code})`);
5864
- if (slot.pending.size > 0) {
5865
- appLogger.warn("scan.refresh_worker.exit_before_done", { agent: agentName, code });
5866
- }
5867
- this.closeWorker(agentName, slot, error);
6279
+ appLogger.restoreContext(this.slotLogContext(slot), () => {
6280
+ if (slot.closed) return;
6281
+ const error = new Error(`Scan refresh worker exited before completing (code ${code})`);
6282
+ if (slot.pending.size > 0) {
6283
+ appLogger.warn("scan.refresh_worker.exit_before_done", { agent: agentName, code });
6284
+ }
6285
+ this.closeWorker(agentName, slot, error);
6286
+ });
5868
6287
  });
5869
6288
  return slot;
5870
6289
  }
5871
6290
  handleMessage(slot, message) {
5872
6291
  const pending = slot.pending.get(message.requestId);
5873
6292
  if (!pending) return;
6293
+ appLogger.restoreContext(pending.logContext, () => {
6294
+ this.handlePendingMessage(slot, message, pending);
6295
+ });
6296
+ }
6297
+ handlePendingMessage(slot, message, pending) {
5874
6298
  if (message.generation !== pending.generation) {
5875
6299
  const error = new Error(
5876
6300
  `Scan refresh generation mismatch: expected ${pending.generation}, received ${message.generation}`
@@ -5907,7 +6331,7 @@ var ThreadWorkerRunner = class {
5907
6331
  const removedMetaIds = [...message.removedSessionIds, ...message.removedMetaIds];
5908
6332
  try {
5909
6333
  const result = {
5910
- sessions: applySessionChanges2(
6334
+ sessions: applySessionChanges(
5911
6335
  pending.payload.previousSessions,
5912
6336
  message.changes,
5913
6337
  message.removedSessionIds
@@ -5920,7 +6344,8 @@ var ThreadWorkerRunner = class {
5920
6344
  };
5921
6345
  slot.awaitingCommit = {
5922
6346
  requestId: message.requestId,
5923
- generation: message.generation
6347
+ generation: message.generation,
6348
+ logContext: pending.logContext
5924
6349
  };
5925
6350
  pending.resolve(this.createStagedRun(slot, message.requestId, message.generation, result));
5926
6351
  } catch (error) {
@@ -5952,7 +6377,8 @@ var ThreadWorkerRunner = class {
5952
6377
  const request = {
5953
6378
  type: "commit",
5954
6379
  requestId,
5955
- generation
6380
+ generation,
6381
+ logContext: awaiting.logContext
5956
6382
  };
5957
6383
  try {
5958
6384
  slot.worker.postMessage(request);
@@ -5976,19 +6402,30 @@ var ThreadWorkerRunner = class {
5976
6402
  slot.pending.clear();
5977
6403
  slot.awaitingCommit = null;
5978
6404
  }
6405
+ slotLogContext(slot) {
6406
+ for (const pending of slot.pending.values()) return pending.logContext;
6407
+ return slot.awaitingCommit?.logContext ?? {};
6408
+ }
5979
6409
  invalidateWorker(agentName, slot) {
5980
6410
  this.closeWorker(
5981
6411
  agentName,
5982
6412
  slot,
5983
6413
  new Error(`Scan refresh worker state discarded for ${agentName}`)
5984
6414
  );
5985
- void slot.worker.terminate().catch(() => void 0);
6415
+ void this.retireWorker(slot.worker);
6416
+ }
6417
+ retireWorker(worker) {
6418
+ const retirement = terminateWorkerAfterLogDrain(worker);
6419
+ this.retirements.add(retirement);
6420
+ void retirement.finally(() => this.retirements.delete(retirement)).catch(() => void 0);
6421
+ return retirement;
5986
6422
  }
5987
6423
  };
5988
6424
 
5989
6425
  // src/live-scan.ts
5990
6426
  var NEW_SESSION_EVENT_WINDOW_MS = 250;
5991
6427
  var LiveScanStore = class {
6428
+ queryScope;
5992
6429
  watchEnabled;
5993
6430
  scanOptions;
5994
6431
  startupScanOptions;
@@ -6003,6 +6440,10 @@ var LiveScanStore = class {
6003
6440
  constructor(options = {}) {
6004
6441
  this.watchEnabled = options.watchEnabled ?? true;
6005
6442
  this.scanOptions = options.scanOptions ?? {};
6443
+ this.queryScope = {
6444
+ ...this.scanOptions.agents?.length ? { agents: this.scanOptions.agents.map((agent) => agent.toLowerCase()) } : {},
6445
+ ...this.scanOptions.cwd ? { projectScope: createProjectScopeMatcher(this.scanOptions.cwd) } : {}
6446
+ };
6006
6447
  this.startupScanOptions = options.startupScanOptions ?? {};
6007
6448
  this.deferInitialRefresh = options.deferInitialRefresh === true;
6008
6449
  const workerRunner = options.workerRunner ?? new ThreadWorkerRunner(new URL("./scan-refresh-worker.js", import.meta.url));
@@ -6024,8 +6465,9 @@ var LiveScanStore = class {
6024
6465
  startup_to: this.startupScanOptions.to,
6025
6466
  deferred: this.deferInitialRefresh || void 0
6026
6467
  });
6468
+ const { cwd: _cwd, ...scanOptions } = this.scanOptions;
6027
6469
  const initialResult = await scanSessions({
6028
- ...this.scanOptions,
6470
+ ...scanOptions,
6029
6471
  useCache: this.scanOptions.useCache ?? true,
6030
6472
  cacheOnly: this.deferInitialRefresh,
6031
6473
  writeCache: this.deferInitialRefresh ? false : this.scanOptions.writeCache,
@@ -6035,7 +6477,7 @@ var LiveScanStore = class {
6035
6477
  this.syncEngine.initialize(initialResult, {
6036
6478
  cacheTimestamps: initialResult.cacheTimestamps,
6037
6479
  registeredAgents: createRegisteredAgents(),
6038
- allowedAgents: this.getAllowedAgents()
6480
+ queryScope: this.queryScope
6039
6481
  });
6040
6482
  const snapshot = this.getSnapshot();
6041
6483
  const indexStartedAt = performance.now();
@@ -6144,10 +6586,6 @@ var LiveScanStore = class {
6144
6586
  if (workerUrl.protocol === "file:" && !existsSync4(fileURLToPath3(workerUrl))) return null;
6145
6587
  return workerUrl;
6146
6588
  }
6147
- getAllowedAgents() {
6148
- if (!this.scanOptions.agents?.length) return null;
6149
- return new Set(this.scanOptions.agents.map((agent) => agent.toLowerCase()));
6150
- }
6151
6589
  };
6152
6590
 
6153
6591
  // src/output.ts
@@ -6287,8 +6725,23 @@ function startPricingRefresh(timeoutMs = REFRESH_TIMEOUT_MS) {
6287
6725
  // src/index.ts
6288
6726
  process.on("unhandledRejection", (reason) => {
6289
6727
  appLogger.error("process.unhandled_rejection", { error: reason });
6728
+ appLogger.flushSync();
6290
6729
  throw reason;
6291
6730
  });
6731
+ process.on("uncaughtExceptionMonitor", (error, origin) => {
6732
+ appLogger.error("process.uncaught_exception", { error, exception_origin: origin });
6733
+ appLogger.flushSync();
6734
+ });
6735
+ var closeApplicationLog = (exitCode) => closeLoggerBeforeTermination(() => appLogger.close(), exitCode);
6736
+ var shutdownOnSignal = (signal) => {
6737
+ appLogger.info("cli.shutdown", { signal, phase: "startup" });
6738
+ appLogger.flushSync();
6739
+ process.exit(0);
6740
+ };
6741
+ var dispatchSignal = (signal) => shutdownOnSignal(signal);
6742
+ process.once("SIGINT", dispatchSignal);
6743
+ process.once("SIGTERM", dispatchSignal);
6744
+ process.once("SIGHUP", dispatchSignal);
6292
6745
  var main = defineCommand({
6293
6746
  meta: {
6294
6747
  name: "codesesh",
@@ -6337,7 +6790,7 @@ var main = defineCommand({
6337
6790
  days: {
6338
6791
  type: "string",
6339
6792
  alias: "d",
6340
- description: "Only include sessions active in the last N days (0 = all time)",
6793
+ description: "Only include sessions active in the last N local calendar days (0 = all time)",
6341
6794
  default: "7"
6342
6795
  },
6343
6796
  cwd: {
@@ -6346,11 +6799,11 @@ var main = defineCommand({
6346
6799
  },
6347
6800
  from: {
6348
6801
  type: "string",
6349
- description: "Sessions active after this date, YYYY-MM-DD (overrides --days)"
6802
+ description: "Sessions active on or after this date, YYYY-MM-DD (overrides --days)"
6350
6803
  },
6351
6804
  to: {
6352
6805
  type: "string",
6353
- description: "Sessions active before this date (YYYY-MM-DD)"
6806
+ description: "Sessions active on or before this date (YYYY-MM-DD)"
6354
6807
  },
6355
6808
  session: {
6356
6809
  type: "string",
@@ -6433,7 +6886,7 @@ var main = defineCommand({
6433
6886
  log_path: appLogger.getLogPath()
6434
6887
  });
6435
6888
  if (clearCache) {
6436
- const { clearCache: clear } = await import("./discovery-K3GLISI6.js");
6889
+ const { clearCache: clear } = await import("./discovery-D23RV7FY.js");
6437
6890
  clear();
6438
6891
  appLogger.info("cache.clear");
6439
6892
  console.log("Cache cleared.");
@@ -6444,6 +6897,7 @@ var main = defineCommand({
6444
6897
  targetSession = parseSessionUri(args.session);
6445
6898
  if (!targetSession) {
6446
6899
  console.error(`Invalid session format: ${args.session}. Expected: agent://session-id`);
6900
+ await closeApplicationLog(1);
6447
6901
  process.exit(1);
6448
6902
  }
6449
6903
  }
@@ -6500,6 +6954,7 @@ var main = defineCommand({
6500
6954
  console.log(JSON.stringify(output, null, 2));
6501
6955
  }
6502
6956
  await store.shutdown();
6957
+ await closeApplicationLog(process.exitCode ?? 0);
6503
6958
  return;
6504
6959
  }
6505
6960
  const agents = createRegisteredAgents();
@@ -6518,6 +6973,7 @@ var main = defineCommand({
6518
6973
  });
6519
6974
  } catch (error) {
6520
6975
  console.error(getServerStartupErrorMessage(error, port));
6976
+ await closeApplicationLog(1);
6521
6977
  process.exit(1);
6522
6978
  }
6523
6979
  const { url } = app;
@@ -6532,16 +6988,16 @@ var main = defineCommand({
6532
6988
  appLogger.info("cli.shutdown", { signal });
6533
6989
  await pricingRefresh.cancel();
6534
6990
  await app.shutdown();
6991
+ await closeApplicationLog(0);
6535
6992
  process.exit(0);
6536
6993
  };
6537
- const shutdownOnSignal = (signal) => {
6538
- shutdown(signal).catch((error) => {
6994
+ shutdownOnSignal = (signal) => {
6995
+ shutdown(signal).catch(async (error) => {
6539
6996
  appLogger.error("cli.shutdown_failed", { signal, error });
6997
+ await closeApplicationLog(1);
6540
6998
  process.exit(1);
6541
6999
  });
6542
7000
  };
6543
- process.once("SIGINT", shutdownOnSignal);
6544
- process.once("SIGTERM", shutdownOnSignal);
6545
7001
  console.log(` ${url}`);
6546
7002
  console.log("");
6547
7003
  appLogger.info("cli.ready", {
@@ -6557,6 +7013,17 @@ var main = defineCommand({
6557
7013
  }
6558
7014
  }
6559
7015
  });
7016
+ var runCliCommand = main.run;
7017
+ if (!runCliCommand) throw new Error("CLI command is missing its runner");
7018
+ main.run = async (context) => {
7019
+ try {
7020
+ return await runCliCommand(context);
7021
+ } catch (error) {
7022
+ appLogger.error("cli.fatal", { error });
7023
+ await closeApplicationLog(1);
7024
+ throw error;
7025
+ }
7026
+ };
6560
7027
  if (process.argv.slice(2).includes("-v")) {
6561
7028
  console.log(VERSION);
6562
7029
  process.exit(0);