codesesh 1.0.4 → 1.0.6

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 (59) hide show
  1. package/README.md +5 -4
  2. package/dist/{chunk-W4PZHL2C.js → chunk-4ND6AT6W.js} +154 -92
  3. package/dist/chunk-4ND6AT6W.js.map +1 -0
  4. package/dist/chunk-4QFNZCBR.js +1271 -0
  5. package/dist/chunk-4QFNZCBR.js.map +1 -0
  6. package/dist/{chunk-MBZ4VV4C.js → chunk-A3MT6L6Z.js} +3 -3
  7. package/dist/chunk-COZSWVCH.js +89 -0
  8. package/dist/chunk-COZSWVCH.js.map +1 -0
  9. package/dist/{chunk-YCUN52HA.js → chunk-L3DDWTYV.js} +81 -115
  10. package/dist/chunk-L3DDWTYV.js.map +1 -0
  11. package/dist/{chunk-BYB77UAJ.js → chunk-XQDYKKRC.js} +139 -80
  12. package/dist/chunk-XQDYKKRC.js.map +1 -0
  13. package/dist/{chunk-3SXCTDD6.js → chunk-YZ6XCSVQ.js} +4 -3
  14. package/dist/{discovery-K3GLISI6.js → discovery-ADSQARBV.js} +11 -5
  15. package/dist/index.js +1538 -1030
  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-D2bx7uDv.js +1 -0
  26. package/dist/web/assets/InteractiveReceipt-BFpIdno8.js +1 -0
  27. package/dist/web/assets/OverviewScreen-D_KXfiJi.js +1 -0
  28. package/dist/web/assets/{PrismHighlighter-B1a2dPJO.js → PrismHighlighter-BgVkYu_S.js} +1 -1
  29. package/dist/web/assets/Projects-CN44UpuR.js +1 -0
  30. package/dist/web/assets/SearchResultsPanel-CWD01v-a.js +1 -0
  31. package/dist/web/assets/SessionDetail-D8taQ6Tl.js +53 -0
  32. package/dist/web/assets/api-B9lJtVbO.js +1 -0
  33. package/dist/web/assets/index-XpolWMY4.js +1423 -0
  34. package/dist/web/assets/index-xbEEMszv.css +1 -0
  35. package/dist/web/assets/session-indexes-fib48ZKn.js +4 -0
  36. package/dist/web/assets/translate-Dmym1ZDl.js +2 -0
  37. package/dist/web/assets/utils-Bk66u8b3.js +1 -0
  38. package/dist/web/index.html +7 -7
  39. package/package.json +3 -3
  40. package/dist/chunk-BYB77UAJ.js.map +0 -1
  41. package/dist/chunk-G4HRD3AR.js +0 -246
  42. package/dist/chunk-G4HRD3AR.js.map +0 -1
  43. package/dist/chunk-W4PZHL2C.js.map +0 -1
  44. package/dist/chunk-YCUN52HA.js.map +0 -1
  45. package/dist/web/assets/InteractiveReceipt-Dk50TBsH.js +0 -1
  46. package/dist/web/assets/OverviewScreen-5hvQVp4H.js +0 -1
  47. package/dist/web/assets/Projects-Cy_v2yep.js +0 -1
  48. package/dist/web/assets/SearchResultsPanel-DfheEwpz.js +0 -1
  49. package/dist/web/assets/SessionDetail-DRI8R3st.js +0 -53
  50. package/dist/web/assets/api-GP9UCedC.js +0 -1
  51. package/dist/web/assets/contract-xJyW5u7a.js +0 -2
  52. package/dist/web/assets/index-Bd-YkWkZ.css +0 -1
  53. package/dist/web/assets/index-DEjvzumm.js +0 -1410
  54. package/dist/web/assets/panel-ND0J63d3.js +0 -1
  55. package/dist/web/assets/session-indexes-BsgjUoc1.js +0 -4
  56. package/dist/web/assets/utils-C7BqyxxV.js +0 -1
  57. /package/dist/{chunk-MBZ4VV4C.js.map → chunk-A3MT6L6Z.js.map} +0 -0
  58. /package/dist/{chunk-3SXCTDD6.js.map → chunk-YZ6XCSVQ.js.map} +0 -0
  59. /package/dist/{discovery-K3GLISI6.js.map → discovery-ADSQARBV.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-A3MT6L6Z.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-COZSWVCH.js";
12
15
  import "./chunk-QH2HOXJH.js";
13
16
  import {
14
17
  appLogger,
15
18
  logSearchIndexSync
16
- } from "./chunk-G4HRD3AR.js";
19
+ } from "./chunk-4QFNZCBR.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-XQDYKKRC.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-YZ6XCSVQ.js";
56
61
  import {
57
62
  createRegisteredAgents,
58
63
  getAgentInfoMap
59
- } from "./chunk-YCUN52HA.js";
64
+ } from "./chunk-L3DDWTYV.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-4ND6AT6W.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,16 +529,53 @@ 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
- var identityVersions = /* @__PURE__ */ new WeakMap();
693
- function identityVersion(identity) {
694
- const existing = identityVersions.get(identity);
695
- if (existing) return existing;
696
- const version = randomUUID();
697
- identityVersions.set(identity, version);
698
- return version;
699
- }
577
+ var SNAPSHOT_TTL_MS = 6e4;
578
+ var SNAPSHOT_LIMIT = 32;
700
579
  function queryFingerprint(params) {
701
580
  const canonical = new URLSearchParams(params);
702
581
  canonical.delete("cursor");
@@ -713,7 +592,7 @@ function decodeCursor(cursor) {
713
592
  const value = JSON.parse(
714
593
  Buffer.from(cursor, "base64url").toString("utf8")
715
594
  );
716
- if (value.version !== 1 || typeof value.snapshot !== "string" || typeof value.view !== "string" || typeof value.query !== "string" || !Number.isSafeInteger(value.offset) || value.offset <= 0) {
595
+ if (value.version !== 1 || typeof value.snapshot !== "string" || !Number.isSafeInteger(value.offset) || value.offset <= 0) {
717
596
  return null;
718
597
  }
719
598
  return value;
@@ -721,26 +600,47 @@ function decodeCursor(cursor) {
721
600
  return null;
722
601
  }
723
602
  }
724
- function paginateSnapshot(items, request) {
725
- const snapshot = identityVersion(request.snapshotIdentity);
726
- const view = identityVersion(request.viewIdentity);
727
- const query = queryFingerprint(request.query);
728
- let offset = 0;
729
- if (request.cursor) {
730
- const cursor = decodeCursor(request.cursor);
731
- if (!cursor || cursor.query !== query) return { kind: "invalid_cursor" };
732
- if (cursor.snapshot !== snapshot || cursor.view !== view) {
733
- return { kind: "stale_snapshot" };
603
+ function createSnapshotPaginator() {
604
+ const snapshotsBySource = /* @__PURE__ */ new WeakMap();
605
+ return function paginateSnapshot(source, request, load) {
606
+ const snapshots = snapshotsBySource.get(source) ?? /* @__PURE__ */ new Map();
607
+ const now = Date.now();
608
+ for (const [id, snapshot2] of snapshots) {
609
+ if (snapshot2.expiresAt <= now) snapshots.delete(id);
610
+ }
611
+ const query = queryFingerprint(request.query);
612
+ let snapshot;
613
+ let snapshotId;
614
+ let offset = 0;
615
+ if (request.cursor) {
616
+ const cursor = decodeCursor(request.cursor);
617
+ if (!cursor) return { kind: "invalid_cursor" };
618
+ const retained = snapshots.get(cursor.snapshot);
619
+ if (!retained) return { kind: "stale_snapshot" };
620
+ if (retained.query !== query || cursor.offset >= retained.items.length) {
621
+ return { kind: "invalid_cursor" };
622
+ }
623
+ snapshot = retained;
624
+ snapshotId = cursor.snapshot;
625
+ offset = cursor.offset;
626
+ } else {
627
+ const loaded = load();
628
+ snapshot = { ...loaded, items: [...loaded.items], query, expiresAt: now + SNAPSHOT_TTL_MS };
629
+ snapshotId = randomUUID();
734
630
  }
735
- if (cursor.offset >= items.length) return { kind: "invalid_cursor" };
736
- offset = cursor.offset;
737
- }
738
- const end = Math.min(offset + request.limit, items.length);
739
- const nextCursor = end < items.length ? encodeCursor({ version: 1, snapshot, view, query, offset: end }) : void 0;
740
- return {
741
- kind: "page",
742
- items: items.slice(offset, end),
743
- ...nextCursor ? { nextCursor } : {}
631
+ const end = Math.min(offset + request.limit, snapshot.items.length);
632
+ const nextCursor = end < snapshot.items.length ? encodeCursor({ version: 1, snapshot: snapshotId, offset: end }) : void 0;
633
+ if (!request.cursor && nextCursor) {
634
+ if (snapshots.size >= SNAPSHOT_LIMIT) snapshots.delete(snapshots.keys().next().value);
635
+ snapshots.set(snapshotId, snapshot);
636
+ snapshotsBySource.set(source, snapshots);
637
+ }
638
+ return {
639
+ kind: "page",
640
+ items: snapshot.items.slice(offset, end),
641
+ view: snapshot.view,
642
+ ...nextCursor ? { nextCursor } : {}
643
+ };
744
644
  };
745
645
  }
746
646
 
@@ -778,26 +678,30 @@ function getSnapshotSessionTree(source, sessions) {
778
678
  const cache = getSnapshotAggregationCache(source, sessions);
779
679
  return cache.sessionTree ??= buildSessionTree(sessions);
780
680
  }
781
- function getDashboardCostFacts(source, sessions, from, to, cacheTo, analyticsRevision) {
782
- const build = () => listDashboardCostFacts({ from, to });
681
+ function getSnapshotCostFacts(source, sessions, from, to, cacheTo, analyticsRevision) {
682
+ const costFrom = from === 0 ? void 0 : from;
683
+ const build = () => listDashboardCostFacts({ from: costFrom, to });
783
684
  if (analyticsRevision === null) return build();
784
685
  return getSnapshotAggregation(
785
686
  source,
786
687
  sessions,
787
- ["dashboard-cost-facts", from, cacheTo, analyticsRevision],
688
+ ["cost-facts", costFrom, cacheTo, analyticsRevision],
788
689
  build
789
690
  );
790
691
  }
791
692
  function getDashboardStorageAggregation(source, sessions, scope, from, to, cacheTo, analyticsRevision) {
792
693
  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
- })
694
+ recentFileActivities: listFileActivity(
695
+ {
696
+ agent: scope.agent,
697
+ projectKind: scope.projectKind,
698
+ projectKey: scope.projectKey,
699
+ from,
700
+ to,
701
+ limit: 12
702
+ },
703
+ source.queryScope
704
+ )
801
705
  });
802
706
  const startedAt = performance.now();
803
707
  const log = (cache) => {
@@ -829,157 +733,318 @@ function getDashboardStorageAggregation(source, sessions, scope, from, to, cache
829
733
  );
830
734
  }
831
735
 
832
- // src/api/request-payloads.ts
833
- function isRecord(value) {
834
- return typeof value === "object" && value !== null;
736
+ // src/api/catalog-handlers.ts
737
+ var paginateProjects = createSnapshotPaginator();
738
+ function handleGetConfig(c, defaults) {
739
+ const payload = {
740
+ window: {
741
+ from: defaults.from,
742
+ to: defaults.to,
743
+ days: defaults.days
744
+ }
745
+ };
746
+ return c.json(payload);
835
747
  }
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
- });
748
+ function handleGetScanStatus(c, scanSource) {
749
+ return c.json(scanSource.getScanStatus());
844
750
  }
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;
751
+ function handleGetAgents(c, scanSource, defaults = {}) {
752
+ const scanResult = scanSource.getSnapshot();
753
+ const window = parseDateWindowRequest(c, "agents", defaults);
754
+ if (window.kind === "rejected") return window.response;
755
+ const { from, to } = window;
756
+ const agents = getSnapshotAggregation(
757
+ scanSource,
758
+ scanResult.sessions,
759
+ ["agents", from, to],
760
+ () => {
761
+ const counts = Object.fromEntries(
762
+ Object.entries(scanResult.byAgent).map(([agentName, sessions]) => [
763
+ agentName,
764
+ filterSessionsByActivityWindow(sessions, from, to).length
765
+ ])
766
+ );
767
+ return getAgentInfoMap(counts);
768
+ }
769
+ );
770
+ return c.json(agents);
771
+ }
772
+ function handleGetProjects(c, scanSource, defaults = {}) {
773
+ const scanResult = scanSource.getSnapshot();
774
+ const params = searchParams(c);
775
+ const limit = parseLimit(params.get("limit"), PROJECT_PAGE_LIMIT_POLICY);
776
+ if (limit.kind === "invalid") {
777
+ reportInvalidQueryParameter("projects", "limit", "rejected");
778
+ return c.json({ error: limit.error }, 400);
851
779
  }
852
- return normalizeSessionReference({
853
- agentName: value.agentKey.trim().toLowerCase(),
854
- sessionId: value.sessionId
780
+ const projectIdentity = parseProjectIdentityFilter(
781
+ c.req.query("projectKind"),
782
+ c.req.query("projectKey")
783
+ );
784
+ if (projectIdentity === null) {
785
+ return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
786
+ }
787
+ const window = parseDateWindowRequest(c, "projects", defaults);
788
+ if (window.kind === "rejected") return window.response;
789
+ const { from, to } = window;
790
+ const loadSnapshot = () => {
791
+ const analyticsRevision = getAnalyticsRevision();
792
+ const catalog = getSnapshotAggregation(
793
+ scanSource,
794
+ scanResult.sessions,
795
+ ["projects", from, to, analyticsRevision],
796
+ () => {
797
+ const tree = getSnapshotSessionTree(scanSource, scanResult.sessions);
798
+ const costFacts = (from == null || from === 0) && to == null ? getSnapshotCostFacts(scanSource, scanResult.sessions, from, to, to, analyticsRevision) : listDashboardCostFacts({ from, to, includeModelCosts: false });
799
+ const projects2 = attachProjectMetricsFromTree(
800
+ listCachedProjectGroups(scanResult.sessions),
801
+ tree,
802
+ from,
803
+ to,
804
+ costFacts
805
+ ).filter(
806
+ (project) => project.sessionCount > 0 || project.messages > 0 || project.tokens > 0 || project.cost > 0
807
+ );
808
+ return { projects: projects2, summary: summarizeProjects(projects2) };
809
+ }
810
+ );
811
+ const projects = projectIdentity ? catalog.projects.filter(
812
+ (project) => project.identityKind === projectIdentity.kind && project.identityKey === projectIdentity.key
813
+ ) : catalog.projects;
814
+ return {
815
+ items: projects,
816
+ view: projectIdentity ? summarizeProjects(projects) : catalog.summary
817
+ };
818
+ };
819
+ const page = paginateProjects(
820
+ scanSource,
821
+ {
822
+ cursor: params.get("cursor") ?? void 0,
823
+ limit: limit.value,
824
+ query: params
825
+ },
826
+ loadSnapshot
827
+ );
828
+ if (page.kind === "invalid_cursor") {
829
+ reportInvalidQueryParameter("projects", "cursor", "rejected");
830
+ return c.json({ error: "cursor is invalid for this request" }, 400);
831
+ }
832
+ if (page.kind === "stale_snapshot") {
833
+ return c.json({ error: "project snapshot expired; restart pagination" }, 409);
834
+ }
835
+ return c.json({
836
+ projects: page.items,
837
+ summary: page.view,
838
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {}
855
839
  });
856
840
  }
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;
841
+
842
+ // src/time-window-resolution.ts
843
+ var DAY_MS = 24 * 60 * 60 * 1e3;
844
+ var DEFAULT_DASHBOARD_DAYS = 30;
845
+ function resolveTimeWindow(request) {
846
+ return request.mode === "cli" ? resolveCliWindow(request) : resolveDashboardWindow(request);
847
+ }
848
+ function resolveCliWindow(request) {
849
+ const now = request.now ?? Date.now();
850
+ const from = parseRequiredDate(request.from, "start");
851
+ const to = parseRequiredDate(request.to, "end");
852
+ if (from != null && to != null && from > to) {
853
+ throw new Error("Invalid time window: from must not be after to");
854
+ }
855
+ if (from != null) return { from, to };
856
+ const days = parseDays(request.days);
857
+ if (days === 0) return { to, days };
858
+ if (days == null || days < 0) return { to };
859
+ const calendarFrom = addCalendarDays(startOfCalendarDay(to ?? now), -(days - 1));
860
+ return Number.isFinite(calendarFrom) ? { from: calendarFrom, to, days } : { to };
861
+ }
862
+ function resolveDashboardWindow(request) {
863
+ const now = request.now ?? Date.now();
864
+ const to = request.window.to ?? now;
865
+ const hasQueryDays = Boolean(request.days?.trim());
866
+ const parsedDays = hasQueryDays ? parseDays(request.days) : void 0;
867
+ if (request.window.hasExplicitFrom && request.window.from != null) {
868
+ return { from: request.window.from, to, days: countCalendarDays(request.window.from, to) };
869
+ }
870
+ const days = parsedDays ?? request.defaultDays;
871
+ if (days === 0) {
872
+ return { to, days: 0 };
873
+ }
874
+ if (days != null && days > 0) {
875
+ return {
876
+ from: addCalendarDays(startOfCalendarDay(to), -(days - 1)),
877
+ to,
878
+ days
879
+ };
880
+ }
881
+ if (request.window.from != null) {
882
+ return { from: request.window.from, to, days: countCalendarDays(request.window.from, to) };
864
883
  }
865
884
  return {
866
- reference,
867
- bookmarkedAt: typeof timestamp === "number" ? timestamp : Date.now()
885
+ from: addCalendarDays(startOfCalendarDay(to), -(DEFAULT_DASHBOARD_DAYS - 1)),
886
+ to,
887
+ days: DEFAULT_DASHBOARD_DAYS
868
888
  };
869
889
  }
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
- );
890
+ function parseRequiredDate(value, boundary) {
891
+ if (!value) return void 0;
892
+ const calendarBoundary = parseCalendarDayBoundary(value, boundary);
893
+ if (calendarBoundary === null) throw new Error(`Invalid date: ${value}`);
894
+ if (calendarBoundary != null) return calendarBoundary;
895
+ const timestamp = new Date(value).getTime();
896
+ if (Number.isNaN(timestamp)) throw new Error(`Invalid date: ${value}`);
897
+ return timestamp;
898
+ }
899
+ function parseDays(value) {
900
+ if (value == null) return void 0;
901
+ const normalized = value.trim();
902
+ if (!/^-?\d+$/.test(normalized)) return void 0;
903
+ const days = Number(normalized);
904
+ return Number.isSafeInteger(days) && Number.isSafeInteger(days * DAY_MS) ? days : void 0;
881
905
  }
882
906
 
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" } }
907
+ // ../core/dist/runtime/search.mjs
908
+ function executeSessionSearch(query, options, snapshot, context = {}) {
909
+ const merged = mergeSearchQueryOptions(query, options);
910
+ if (!needsIndexedSearch(merged.text, merged.options)) {
911
+ return searchRecentSessions(snapshot, merged.options, context);
912
+ }
913
+ return searchIndexedSessions(
914
+ query,
915
+ merged.text,
916
+ merged.parsed,
917
+ merged.options,
918
+ context.queryScope
934
919
  );
935
920
  }
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
- });
921
+ function needsIndexedSearch(textQuery, options) {
922
+ return Boolean(textQuery || options.file || options.fileKind || options.tools?.length);
949
923
  }
950
- var ALIAS_SEARCH_RESULT_SHARE = 0.25;
951
- function searchResultKey(result) {
952
- return `${result.reference.agentName}\0${result.reference.sessionId}`;
924
+ function matchesRecentSearchFilters(session, options, projectScope, inclusiveCost) {
925
+ if (options.projectKind || options.projectKey) {
926
+ if (!options.projectKind || !options.projectKey || !matchesProjectIdentity(session.project_identity, {
927
+ kind: options.projectKind,
928
+ key: options.projectKey
929
+ })) {
930
+ return false;
931
+ }
932
+ }
933
+ if (projectScope && !matchesProjectScope(session, projectScope)) return false;
934
+ if (options.project) {
935
+ const projectNeedle = options.project.toLowerCase();
936
+ const projectText = [
937
+ session.project_identity?.key,
938
+ session.project_identity?.displayName,
939
+ session.directory
940
+ ].filter(Boolean).join("\n").toLowerCase();
941
+ if (!projectText.includes(projectNeedle)) return false;
942
+ }
943
+ if (options.tags?.length && !options.tags.every((tag) => session.smart_tags?.includes(tag))) {
944
+ return false;
945
+ }
946
+ if (!sessionMatchesSearchCost(session, options, inclusiveCost)) return false;
947
+ return true;
953
948
  }
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
- });
949
+ function matchesSessionSearchFilters(agentName, session, options, projectScope = null, inclusiveCost = session.stats.total_cost) {
950
+ if (options.agent && agentName !== options.agent) return false;
951
+ const activity = getSessionActivityTime(session);
952
+ if (options.from != null && activity < options.from) return false;
953
+ if (options.to != null && activity > options.to) return false;
954
+ return matchesRecentSearchFilters(session, options, projectScope, inclusiveCost);
962
955
  }
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))
956
+ function filterSessionSearchCandidates(candidates, options, context = {}) {
957
+ const projectScope = options.projectScope ?? null;
958
+ const sessionSnapshot = context.sessionSnapshot ?? candidates.map((candidate) => candidate.session);
959
+ const inclusiveCosts = buildInclusiveCostLookup(sessionSnapshot, options, context.sessionTree);
960
+ const headMatches = candidates.filter(
961
+ (candidate) => matchesSessionQueryScope(candidate.session, context.queryScope) && matchesSessionSearchFilters(
962
+ candidate.reference.agentName,
963
+ candidate.session,
964
+ options,
965
+ projectScope,
966
+ inclusiveCostFor(candidate.reference.agentName, candidate.session, inclusiveCosts)
967
+ )
969
968
  );
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))
969
+ if (!options.file && !options.fileKind && !options.tools?.length) return headMatches;
970
+ const indexedMatches = filterIndexedSessionReferences(
971
+ headMatches.map((candidate) => ({
972
+ agentName: candidate.reference.agentName,
973
+ sessionId: candidate.reference.sessionId
974
+ })),
975
+ {
976
+ file: options.file,
977
+ fileKind: options.fileKind,
978
+ tools: options.tools
979
+ }
975
980
  );
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);
981
+ return headMatches.filter(
982
+ (candidate) => indexedMatches.has(getSessionReferenceKey(candidate.reference))
983
+ );
984
+ }
985
+ function searchRecentSessions(snapshot, options, context) {
986
+ const limit = Math.max(0, Math.trunc(options.limit ?? 50));
987
+ if (limit === 0) return [];
988
+ const projectScope = options.projectScope ?? null;
989
+ const inclusiveCosts = buildInclusiveCostLookup(snapshot.sessions, options, context.sessionTree);
990
+ const sessions = options.agent ? snapshot.byAgent[options.agent] ?? [] : snapshot.sessions;
991
+ const results = [];
992
+ for (const session of sessions) {
993
+ const agentName = options.agent ?? session.reference.agentName;
994
+ if (!matchesSessionQueryScope(session, context.queryScope) || !matchesSessionSearchFilters(
995
+ agentName,
996
+ session,
997
+ options,
998
+ projectScope,
999
+ inclusiveCostFor(agentName, session, inclusiveCosts)
1000
+ )) {
1001
+ continue;
1002
+ }
1003
+ results.push({
1004
+ reference: session.reference,
1005
+ session,
1006
+ snippet: `Recent session \xB7 ${session.directory}`,
1007
+ snippetHighlights: [],
1008
+ matchType: "recent"
1009
+ });
1010
+ if (results.length >= limit) break;
1011
+ }
1012
+ return results;
1013
+ }
1014
+ function buildInclusiveCostLookup(sessions, options, sessionTree) {
1015
+ if (options.costMin == null && options.costMax == null) return null;
1016
+ return (sessionTree ?? buildSessionTree(sessions)).byRouteKey;
1017
+ }
1018
+ function inclusiveCostFor(agentName, session, lookup) {
1019
+ return lookup?.get(getSessionReferenceKey({ agentName, sessionId: session.reference.sessionId }))?.inclusiveStats.cost ?? session.stats.total_cost;
1020
+ }
1021
+ function deriveFileQuery(query, parsed, options) {
1022
+ return options.file ?? (!parsed.text ? parsed.filters.file : void 0) ?? (!parsed.hasQualifiers && query ? parsed.text || query : "");
1023
+ }
1024
+ function mergeSearchResultSources(results, limit) {
1025
+ const seen = /* @__PURE__ */ new Set();
1026
+ const merged = [];
1027
+ for (const result of results) {
1028
+ const key = getSessionReferenceKey(result.reference);
1029
+ if (seen.has(key)) continue;
1030
+ seen.add(key);
1031
+ merged.push(result);
1032
+ if (merged.length >= limit) break;
1033
+ }
1034
+ return merged;
1035
+ }
1036
+ function canSkipSessionsSearch(fileQuery, textQuery) {
1037
+ return Boolean(fileQuery && !textQuery);
1038
+ }
1039
+ function searchIndexedSessions(query, textQuery, parsed, options, queryScope) {
1040
+ const fileQuery = deriveFileQuery(query, parsed, options);
1041
+ const fileResults = fileQuery ? searchFileActivitySessions(fileQuery, options, queryScope) : [];
1042
+ const sessionResults = canSkipSessionsSearch(fileQuery, textQuery) ? [] : searchSessions(query, options, queryScope);
1043
+ const textMatchReferences = textQuery && options.file ? new Set(sessionResults.map((result) => getSessionReferenceKey(result.reference))) : null;
1044
+ const matchingFileResults = textMatchReferences ? fileResults.filter(
1045
+ (result) => textMatchReferences.has(getSessionReferenceKey(result.reference))
1046
+ ) : fileResults;
1047
+ return mergeSearchResultSources([...matchingFileResults, ...sessionResults], options.limit ?? 50);
983
1048
  }
984
1049
 
985
1050
  // ../core/dist/runtime/state.mjs
@@ -1471,99 +1536,243 @@ function findAliasSearchResults(query, options, scanResult, aliases, context = {
1471
1536
  }
1472
1537
  return filterSessionSearchCandidates(results, search.options, {
1473
1538
  sessionSnapshot: scanResult.sessions,
1474
- sessionTree: context.sessionTree
1539
+ sessionTree: context.sessionTree,
1540
+ queryScope: context.queryScope
1475
1541
  }).sort((a, b) => getSessionActivityTime(b.session) - getSessionActivityTime(a.session));
1476
1542
  }
1477
1543
 
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)
1544
+ // src/api/dashboard-handler.ts
1545
+ function handleGetDashboard(c, scanSource, defaults = {}) {
1546
+ const scanResult = scanSource.getSnapshot();
1547
+ const projectIdentity = parseProjectIdentityFilter(
1548
+ c.req.query("projectKind"),
1549
+ c.req.query("projectKey")
1485
1550
  );
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);
1551
+ if (projectIdentity === null) {
1552
+ return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
1504
1553
  }
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
1554
+ const dateWindow = parseDateWindowRequest(c, "dashboard", defaults);
1555
+ if (dateWindow.kind === "rejected") return dateWindow.response;
1556
+ const { from, to, days } = resolveTimeWindow({
1557
+ mode: "dashboard",
1558
+ window: dateWindow,
1559
+ days: c.req.query("days"),
1560
+ defaultDays: defaults.days
1512
1561
  });
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)
1562
+ const scope = {
1563
+ agent: optionalQueryValue(c.req.query("agent"))?.toLowerCase(),
1564
+ projectKind: projectIdentity?.kind,
1565
+ projectKey: projectIdentity?.key
1521
1566
  };
1567
+ const compare = from == null ? void 0 : { from: addCalendarDays(from, -days), to: from - 1 };
1568
+ const fixedTo = dateWindow.to;
1569
+ const cacheTo = fixedTo ?? startOfCalendarDay(to);
1570
+ const analyticsRevision = getAnalyticsRevision();
1571
+ const costFacts = getSnapshotCostFacts(
1572
+ scanSource,
1573
+ scanResult.sessions,
1574
+ compare?.from ?? from,
1575
+ from == null && fixedTo == null ? void 0 : to,
1576
+ from == null && fixedTo == null ? void 0 : cacheTo,
1577
+ analyticsRevision
1578
+ );
1579
+ const aggregate = getSnapshotAggregation(
1580
+ scanSource,
1581
+ scanResult.sessions,
1582
+ [
1583
+ "dashboard",
1584
+ scope.agent,
1585
+ scope.projectKind,
1586
+ scope.projectKey,
1587
+ from,
1588
+ cacheTo,
1589
+ compare?.from,
1590
+ compare?.to,
1591
+ analyticsRevision
1592
+ ],
1593
+ () => {
1594
+ const agentInfo = getAgentInfoMap({});
1595
+ const agentInfoMap = new Map(agentInfo.map((agent) => [agent.name, agent]));
1596
+ return buildDashboard(scanResult.sessions, {
1597
+ byAgentNames: Object.keys(scanResult.byAgent),
1598
+ scope,
1599
+ from,
1600
+ to,
1601
+ agentInfoMap,
1602
+ compare,
1603
+ costFacts
1604
+ });
1605
+ }
1606
+ );
1607
+ const storageAggregation = getDashboardStorageAggregation(
1608
+ scanSource,
1609
+ scanResult.sessions,
1610
+ scope,
1611
+ from,
1612
+ to,
1613
+ cacheTo,
1614
+ analyticsRevision
1615
+ );
1616
+ const data = {
1617
+ ...aggregate,
1618
+ ...storageAggregation,
1619
+ window: { from, to, days, compareFrom: compare?.from, compareTo: compare?.to }
1620
+ };
1621
+ const aliases = loadAliasView();
1622
+ return c.json({
1623
+ ...data,
1624
+ recentSessions: data.recentSessions.map(
1625
+ (item) => toPublicReferencedSessionHead({
1626
+ ...item,
1627
+ session: aliases.decorate(item.session, item.reference)
1628
+ })
1629
+ ),
1630
+ recentFileActivities: data.recentFileActivities.map(
1631
+ (activity) => toPublicReferencedSessionHead(decorateFileActivity(activity, aliases))
1632
+ )
1633
+ });
1522
1634
  }
1523
- function getSessionHeadReference(session) {
1524
- return session.reference;
1635
+
1636
+ // src/api/search-result-merge.ts
1637
+ function withParentContext(results, getSessionTree, aliases) {
1638
+ if (!results.some((result) => result.session.parent_reference)) return results;
1639
+ const byRouteKey = getSessionTree().byRouteKey;
1640
+ return results.map((result) => {
1641
+ const parentReference = result.session.parent_reference;
1642
+ if (!parentReference) return result;
1643
+ const parent = byRouteKey.get(formatSessionReference(parentReference))?.session;
1644
+ if (!parent) return result;
1645
+ const reference = normalizeSessionReference(parentReference);
1646
+ return { ...result, parent: { reference, title: aliases.get(reference) ?? parent.title } };
1647
+ });
1525
1648
  }
1526
- function handleGetConfig(c, defaults) {
1527
- const payload = {
1528
- window: {
1529
- from: defaults.from,
1530
- to: defaults.to,
1531
- days: defaults.days
1532
- }
1533
- };
1534
- return c.json(payload);
1649
+ var ALIAS_SEARCH_RESULT_SHARE = 0.25;
1650
+ function searchResultKey(result) {
1651
+ return `${result.reference.agentName}\0${result.reference.sessionId}`;
1535
1652
  }
1536
- function handleGetScanStatus(c, scanSource) {
1537
- return c.json(scanSource.getScanStatus());
1653
+ function uniqueSearchResults(results) {
1654
+ const seen = /* @__PURE__ */ new Set();
1655
+ return results.filter((result) => {
1656
+ const key = searchResultKey(result);
1657
+ if (seen.has(key)) return false;
1658
+ seen.add(key);
1659
+ return true;
1660
+ });
1538
1661
  }
1539
- function handleGetAgents(c, scanSource, defaults = {}) {
1662
+ function mergeAliasSearchResults(rankedResults, aliasResults, limit) {
1663
+ if (limit <= 0) return [];
1664
+ const ranked = uniqueSearchResults(rankedResults);
1665
+ const rankedKeys = new Set(ranked.map(searchResultKey));
1666
+ const aliases = uniqueSearchResults(aliasResults).filter(
1667
+ (result) => !rankedKeys.has(searchResultKey(result))
1668
+ );
1669
+ if (ranked.length === 0) return aliases.slice(0, limit);
1670
+ if (aliases.length === 0) return ranked.slice(0, limit);
1671
+ const aliasQuota = Math.min(
1672
+ limit - 1,
1673
+ Math.max(1, Math.floor(limit * ALIAS_SEARCH_RESULT_SHARE))
1674
+ );
1675
+ const rankedQuota = limit - aliasQuota;
1676
+ return [
1677
+ ...ranked.slice(0, rankedQuota),
1678
+ ...aliases.slice(0, aliasQuota),
1679
+ ...ranked.slice(rankedQuota),
1680
+ ...aliases.slice(aliasQuota)
1681
+ ].slice(0, limit);
1682
+ }
1683
+
1684
+ // src/api/search-handlers.ts
1685
+ async function handleSearchSessions(c, scanSource, defaults = {}, resolver) {
1686
+ const query = c.req.query("q")?.trim() ?? "";
1540
1687
  const scanResult = scanSource.getSnapshot();
1541
- const window = parseDateWindowRequest(c, "agents", defaults);
1688
+ const sessionQuery = parseSessionQuery(searchParams(c), KNOWN_AGENT_NAMES, SEARCH_LIMIT_POLICY);
1689
+ if (sessionQuery.limit.kind === "invalid") {
1690
+ reportInvalidQueryParameter("search", "limit", "rejected");
1691
+ return c.json({ error: sessionQuery.limit.error }, 400);
1692
+ }
1693
+ const projectIdentity = parseProjectIdentityFilter(
1694
+ c.req.query("projectKind"),
1695
+ c.req.query("projectKey")
1696
+ );
1697
+ if (projectIdentity === null) {
1698
+ return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
1699
+ }
1700
+ const window = parseDateWindowRequest(c, "search", defaults);
1542
1701
  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
- ])
1702
+ if (sessionQuery.agent.kind === "unknown") {
1703
+ reportInvalidQueryParameter("search", "agent", "empty_result");
1704
+ return c.json({ results: [] });
1705
+ }
1706
+ const searchRequestOptions = parseSearchOptions(
1707
+ c,
1708
+ window,
1709
+ {
1710
+ agent: sessionQuery.agent.kind === "known" ? sessionQuery.agent.agentName : void 0,
1711
+ limit: sessionQuery.limit.value
1712
+ },
1713
+ projectIdentity
1714
+ );
1715
+ const cwd = getSearchProjectDirectory(query, searchRequestOptions);
1716
+ let projectScope;
1717
+ if (cwd) {
1718
+ try {
1719
+ projectScope = await resolveProjectScope(
1720
+ cwd,
1721
+ scanResult.sessions,
1722
+ resolver,
1723
+ c.req.raw.signal
1554
1724
  );
1555
- return getAgentInfoMap(counts);
1725
+ } catch (error) {
1726
+ return projectScopeResolutionFailureResponse(c, "search", error);
1556
1727
  }
1728
+ }
1729
+ const { cwd: _cwd, ...searchOptions } = searchRequestOptions;
1730
+ const resolvedSearchOptions = projectScope ? { ...searchOptions, projectScope } : searchOptions;
1731
+ const aliases = loadAliasView();
1732
+ let sessionTree;
1733
+ const getSessionTree = () => sessionTree ??= getSnapshotSessionTree(scanSource, scanResult.sessions);
1734
+ const effectiveSearchOptions = mergeSearchQueryOptions(query, resolvedSearchOptions).options;
1735
+ const searchContext = {
1736
+ queryScope: scanSource.queryScope,
1737
+ ...effectiveSearchOptions.costMin != null || effectiveSearchOptions.costMax != null ? { sessionTree: getSessionTree() } : {}
1738
+ };
1739
+ const searchResults = executeSessionSearch(
1740
+ query,
1741
+ resolvedSearchOptions,
1742
+ scanResult,
1743
+ searchContext
1557
1744
  );
1558
- return c.json(agents);
1745
+ const results = searchResults.map((result) => ({
1746
+ ...result,
1747
+ session: aliases.decorate(result.session, result.reference)
1748
+ }));
1749
+ const aliasResults = findAliasSearchResults(
1750
+ query,
1751
+ resolvedSearchOptions,
1752
+ scanResult,
1753
+ aliases,
1754
+ searchContext
1755
+ );
1756
+ const mergedResults = mergeAliasSearchResults(
1757
+ results,
1758
+ aliasResults,
1759
+ resolvedSearchOptions.limit ?? 50
1760
+ );
1761
+ return c.json({
1762
+ results: withParentContext(mergedResults, getSessionTree, aliases).map(
1763
+ toPublicReferencedSessionHead
1764
+ )
1765
+ });
1559
1766
  }
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);
1767
+ async function handleGetFileActivity(c, scanSource, defaults = {}, resolver) {
1768
+ const sessionQuery = parseSessionQuery(
1769
+ searchParams(c),
1770
+ KNOWN_AGENT_NAMES,
1771
+ FILE_ACTIVITY_LIMIT_POLICY
1772
+ );
1773
+ if (sessionQuery.limit.kind === "invalid") {
1774
+ reportInvalidQueryParameter("file-activity", "limit", "rejected");
1775
+ return c.json({ error: sessionQuery.limit.error }, 400);
1567
1776
  }
1568
1777
  const projectIdentity = parseProjectIdentityFilter(
1569
1778
  c.req.query("projectKind"),
@@ -1572,51 +1781,185 @@ function handleGetProjects(c, scanSource, defaults = {}) {
1572
1781
  if (projectIdentity === null) {
1573
1782
  return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
1574
1783
  }
1575
- const window = parseDateWindowRequest(c, "projects", defaults);
1784
+ const window = parseDateWindowRequest(c, "file-activity", defaults);
1576
1785
  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) };
1786
+ if (sessionQuery.agent.kind === "unknown") {
1787
+ reportInvalidQueryParameter("file-activity", "agent", "empty_result");
1788
+ return c.json({ activity: [] });
1789
+ }
1790
+ const cwd = optionalQueryValue(c.req.query("cwd"));
1791
+ let projectScope;
1792
+ if (cwd) {
1793
+ try {
1794
+ projectScope = await resolveProjectScope(cwd, [], resolver, c.req.raw.signal);
1795
+ } catch (error) {
1796
+ return projectScopeResolutionFailureResponse(c, "file-activity", error);
1797
+ }
1798
+ }
1799
+ const aliases = loadAliasView();
1800
+ return c.json({
1801
+ activity: listFileActivity(
1802
+ {
1803
+ agent: sessionQuery.agent.kind === "known" ? sessionQuery.agent.agentName : void 0,
1804
+ sessionId: optionalQueryValue(c.req.query("sessionId")),
1805
+ projectKind: projectIdentity?.kind,
1806
+ projectKey: projectIdentity?.key,
1807
+ project: optionalQueryValue(c.req.query("project")),
1808
+ projectScope,
1809
+ path: optionalQueryValue(c.req.query("path")),
1810
+ kind: parseFileActivityKind(optionalQueryValue(c.req.query("kind"))),
1811
+ from: window.from,
1812
+ to: window.to,
1813
+ limit: sessionQuery.limit.value
1814
+ },
1815
+ scanSource.queryScope
1816
+ ).map((activity) => toPublicReferencedSessionHead(decorateFileActivity(activity, aliases)))
1817
+ });
1818
+ }
1819
+
1820
+ // src/session-detail-loader.ts
1821
+ import { Worker as Worker2 } from "worker_threads";
1822
+ var SessionDetailBusyError = class extends Error {
1823
+ constructor() {
1824
+ super("Session detail workers are busy");
1825
+ }
1826
+ };
1827
+ var MAX_DETAIL_WORKERS = 2;
1828
+ var DETAIL_TIMEOUT_MS = 6e4;
1829
+ var ThreadSessionDetailLoader = class {
1830
+ constructor(workerUrl = new URL("./session-detail-worker.js", import.meta.url)) {
1831
+ this.workerUrl = workerUrl;
1832
+ }
1833
+ workerUrl;
1834
+ workers = /* @__PURE__ */ new Set();
1835
+ shutdownController = new AbortController();
1836
+ load = async (snapshot, reference, options = {}, signal) => {
1837
+ const requestSignal = signal ? AbortSignal.any([signal, this.shutdownController.signal]) : this.shutdownController.signal;
1838
+ requestSignal.throwIfAborted();
1839
+ const cached = materializeCachedSessionDetailResponse(snapshot, reference, options);
1840
+ if (cached) return cached;
1841
+ if (this.workers.size >= MAX_DETAIL_WORKERS) throw new SessionDetailBusyError();
1842
+ const agent = snapshot.agents.find((item) => item.name === reference.agentName);
1843
+ const head = snapshot.byAgent[reference.agentName]?.find(
1844
+ (item) => item.reference.sessionId === reference.sessionId
1845
+ );
1846
+ const meta = agent?.getSessionCacheMeta(reference.sessionId);
1847
+ const request = {
1848
+ reference,
1849
+ head,
1850
+ meta: meta ? { [reference.sessionId]: meta } : {},
1851
+ messageCursor: options.messageCursor,
1852
+ pricingGenerationId: getPricingGeneration().id,
1853
+ logContext: appLogger.captureContext()
1854
+ };
1855
+ appLogger.info("session_detail.worker.started", {
1856
+ agent: reference.agentName,
1857
+ session_id: reference.sessionId
1858
+ });
1859
+ const worker = new Worker2(this.workerUrl, { workerData: request });
1860
+ const exited = new Promise(
1861
+ (resolve4) => worker.once("exit", () => {
1862
+ this.workers.delete(worker);
1863
+ resolve4();
1864
+ })
1865
+ );
1866
+ let timeout;
1867
+ let abort = () => {
1868
+ };
1869
+ try {
1870
+ return await new Promise((resolve4, reject) => {
1871
+ abort = () => reject(requestSignal.reason);
1872
+ requestSignal.addEventListener("abort", abort, { once: true });
1873
+ this.workers.add(worker);
1874
+ timeout = setTimeout(
1875
+ () => reject(new Error("Session detail loading timed out")),
1876
+ DETAIL_TIMEOUT_MS
1877
+ );
1878
+ timeout.unref();
1879
+ worker.on("message", (message) => {
1880
+ if (appLogger.consumeWorkerMessage(message)) return;
1881
+ if (message.type === "result") resolve4(message.result);
1882
+ else if (message.type === "error") reject(new Error(message.error));
1883
+ });
1884
+ worker.once("error", reject);
1885
+ worker.once(
1886
+ "exit",
1887
+ (code) => reject(new Error(`Session detail worker exited without a result (${code})`))
1888
+ );
1889
+ });
1890
+ } finally {
1891
+ clearTimeout(timeout);
1892
+ requestSignal.removeEventListener("abort", abort);
1893
+ await terminateWorkerAfterLogDrain(worker);
1894
+ await exited;
1596
1895
  }
1896
+ };
1897
+ async shutdown() {
1898
+ this.shutdownController.abort();
1899
+ await Promise.allSettled(
1900
+ [...this.workers].map((worker) => terminateWorkerAfterLogDrain(worker))
1901
+ );
1902
+ }
1903
+ };
1904
+
1905
+ // src/api/session-detail-stream.ts
1906
+ var SESSION_DETAIL_STREAM_BATCH_CHARS = 64 * 1024;
1907
+ function createSessionDetailJsonResponse(data, messages) {
1908
+ const encoder = new TextEncoder();
1909
+ const headerJson = JSON.stringify(data);
1910
+ const headerPrefix = headerJson === "{}" ? '{"messages":[' : `${headerJson.slice(0, -1)},"messages":[`;
1911
+ const iterator = messages[Symbol.iterator]();
1912
+ let wroteHeader = false;
1913
+ let wroteMessage = false;
1914
+ return new Response(
1915
+ new ReadableStream({
1916
+ pull(controller) {
1917
+ if (!wroteHeader) {
1918
+ controller.enqueue(encoder.encode(headerPrefix));
1919
+ wroteHeader = true;
1920
+ return;
1921
+ }
1922
+ const batch = [];
1923
+ let batchLength = 0;
1924
+ let next;
1925
+ try {
1926
+ next = iterator.next();
1927
+ while (!next.done) {
1928
+ const prefix = wroteMessage ? "," : "";
1929
+ batch.push(prefix, next.value);
1930
+ batchLength += prefix.length + next.value.length;
1931
+ wroteMessage = true;
1932
+ if (batchLength >= SESSION_DETAIL_STREAM_BATCH_CHARS) break;
1933
+ next = iterator.next();
1934
+ }
1935
+ } catch (error) {
1936
+ try {
1937
+ iterator.return?.();
1938
+ } catch {
1939
+ }
1940
+ controller.error(error);
1941
+ return;
1942
+ }
1943
+ if (next.done) {
1944
+ batch.push("]}");
1945
+ controller.enqueue(encoder.encode(batch.join("")));
1946
+ controller.close();
1947
+ return;
1948
+ }
1949
+ controller.enqueue(encoder.encode(batch.join("")));
1950
+ },
1951
+ cancel() {
1952
+ iterator.return?.();
1953
+ }
1954
+ }),
1955
+ { headers: { "Content-Type": "application/json; charset=UTF-8" } }
1597
1956
  );
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
- });
1957
+ }
1958
+
1959
+ // src/api/session-handlers.ts
1960
+ var paginateSessions = createSnapshotPaginator();
1961
+ function getSessionHeadReference(session) {
1962
+ return session.reference;
1620
1963
  }
1621
1964
  async function handleGetSessions(c, scanSource, defaults = {}, resolver) {
1622
1965
  const scanResult = scanSource.getSnapshot();
@@ -1656,213 +1999,92 @@ async function handleGetSessions(c, scanSource, defaults = {}, resolver) {
1656
1999
  if (sessionQuery.agent.kind === "unknown") {
1657
2000
  reportInvalidQueryParameter("sessions", "agent", "empty_result");
1658
2001
  }
1659
- const agentFilter = sessionQuery.agent.kind === "known" ? sessionQuery.agent.agentName : sessionQuery.agent.kind;
1660
- let sessions = getSnapshotAggregation(
1661
- scanSource,
1662
- scanResult.sessions,
1663
- [
1664
- "sessions",
1665
- agentFilter,
1666
- projectIdentity?.kind,
1667
- projectIdentity?.key,
1668
- projectScope?.identity.kind,
1669
- projectScope?.identity.key,
1670
- projectScope?.path,
1671
- tag,
1672
- from,
1673
- to
1674
- ],
1675
- () => {
1676
- let filtered = sessionQuery.agent.kind === "all" ? scanResult.sessions : sessionQuery.agent.kind === "known" ? scanResult.byAgent[sessionQuery.agent.agentName] ?? [] : [];
1677
- if (projectIdentity) {
1678
- filtered = filtered.filter(
1679
- (session) => matchesProjectIdentity(session.project_identity, projectIdentity)
1680
- );
1681
- } else if (projectScope) {
1682
- filtered = filtered.filter((session) => matchesProjectScope(session, projectScope));
2002
+ const loadSnapshot = () => {
2003
+ const agentFilter = sessionQuery.agent.kind === "known" ? sessionQuery.agent.agentName : sessionQuery.agent.kind;
2004
+ let sessions2 = getSnapshotAggregation(
2005
+ scanSource,
2006
+ scanResult.sessions,
2007
+ [
2008
+ "sessions",
2009
+ agentFilter,
2010
+ projectIdentity?.kind,
2011
+ projectIdentity?.key,
2012
+ projectScope?.identity.kind,
2013
+ projectScope?.identity.key,
2014
+ projectScope?.path,
2015
+ tag,
2016
+ from,
2017
+ to
2018
+ ],
2019
+ () => {
2020
+ let filtered = sessionQuery.agent.kind === "all" ? scanResult.sessions : sessionQuery.agent.kind === "known" ? scanResult.byAgent[sessionQuery.agent.agentName] ?? [] : [];
2021
+ if (projectIdentity) {
2022
+ filtered = filtered.filter(
2023
+ (session) => matchesProjectIdentity(session.project_identity, projectIdentity)
2024
+ );
2025
+ } else if (projectScope) {
2026
+ filtered = filtered.filter(
2027
+ (session) => matchesProjectScope(session, projectScope)
2028
+ );
2029
+ }
2030
+ filtered = filterSessionsByActivityWindow(filtered, from, to);
2031
+ return tag ? filtered.filter((session) => session.smart_tags?.includes(tag)) : filtered;
1683
2032
  }
1684
- filtered = filterSessionsByActivityWindow(filtered, from, to);
1685
- return tag ? filtered.filter((session) => session.smart_tags?.includes(tag)) : filtered;
2033
+ );
2034
+ const aliases2 = loadAliasView();
2035
+ if (q) {
2036
+ sessions2 = sessions2.filter((session) => {
2037
+ const alias = aliases2.get(getSessionHeadReference(session));
2038
+ return session.title.toLowerCase().includes(q) || alias?.toLowerCase().includes(q);
2039
+ });
1686
2040
  }
1687
- );
1688
- const aliases = loadAliasView();
1689
- if (q) {
1690
- sessions = sessions.filter((session) => {
1691
- const alias = aliases.get(getSessionHeadReference(session));
1692
- return session.title.toLowerCase().includes(q) || alias?.toLowerCase().includes(q);
1693
- });
1694
- }
2041
+ return { items: sessions2, view: aliases2 };
2042
+ };
1695
2043
  if (paginationRequested) {
1696
- const page = paginateSnapshot(sessions, {
1697
- cursor: params.get("cursor") ?? void 0,
1698
- limit: sessionQuery.limit.value,
1699
- query: params,
1700
- snapshotIdentity: scanResult.sessions,
1701
- viewIdentity: aliases
1702
- });
2044
+ const page = paginateSessions(
2045
+ scanSource,
2046
+ {
2047
+ cursor: params.get("cursor") ?? void 0,
2048
+ limit: sessionQuery.limit.value,
2049
+ query: params
2050
+ },
2051
+ loadSnapshot
2052
+ );
1703
2053
  if (page.kind === "invalid_cursor") {
1704
2054
  reportInvalidQueryParameter("sessions", "cursor", "rejected");
1705
2055
  return c.json({ error: "cursor is invalid for this request" }, 400);
1706
2056
  }
1707
2057
  if (page.kind === "stale_snapshot") {
1708
- return c.json({ error: "session snapshot changed; restart pagination" }, 409);
2058
+ return c.json({ error: "session snapshot expired; restart pagination" }, 409);
1709
2059
  }
1710
2060
  return c.json({
1711
2061
  sessions: page.items.map(
1712
- (session) => toPublicSessionHead(aliases.decorate(session, getSessionHeadReference(session)))
2062
+ (session) => toPublicSessionHead(page.view.decorate(session, getSessionHeadReference(session)))
1713
2063
  ),
1714
2064
  ...page.nextCursor ? { nextCursor: page.nextCursor } : {}
1715
2065
  });
1716
2066
  }
2067
+ const { items: sessions, view: aliases } = loadSnapshot();
1717
2068
  return c.json({
1718
2069
  sessions: sessions.map(
1719
2070
  (session) => toPublicSessionHead(aliases.decorate(session, getSessionHeadReference(session)))
1720
2071
  )
1721
2072
  });
1722
2073
  }
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) {
2074
+ async function handleGetSessionData(c, scanSource, loadDetail = materializeSessionDetailResponse) {
1847
2075
  const startedAt = performance.now();
1848
2076
  const agentName = c.req.param("agent");
1849
2077
  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
- }
2078
+ if (!agentName) return c.json({ error: "Missing agent name" }, 400);
2079
+ if (!sessionId) return c.json({ error: "Missing session ID" }, 400);
1856
2080
  try {
1857
- const reference = {
1858
- agentName,
1859
- sessionId
1860
- };
2081
+ const reference = { agentName, sessionId };
1861
2082
  const messageCursor = optionalQueryValue(c.req.query("messageCursor"));
1862
- const result = materializeSessionDetailResponse(
2083
+ const result = await loadDetail(
1863
2084
  scanSource.getSnapshot(),
1864
2085
  reference,
1865
- messageCursor ? { messageCursor } : {}
2086
+ messageCursor ? { messageCursor } : {},
2087
+ c.req.raw.signal
1866
2088
  );
1867
2089
  if (result.status === "unknown-agent") {
1868
2090
  return c.json({ error: `Unknown agent: ${agentName}` }, 404);
@@ -1891,117 +2113,136 @@ async function handleGetSessionData(c, scanSource) {
1891
2113
  );
1892
2114
  }
1893
2115
  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";
2116
+ } catch (error) {
2117
+ if (error instanceof SessionDetailBusyError) {
2118
+ c.header("Retry-After", "1");
2119
+ return c.json({ error: "Session details busy; retry later" }, 503);
2120
+ }
2121
+ const message = error instanceof Error ? error.message : "Failed to load session";
1896
2122
  appLogger.error("api.session_data.error", {
1897
2123
  agent: agentName,
1898
2124
  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
- )
2125
+ duration_ms: Math.round(performance.now() - startedAt),
2126
+ error: message
2127
+ });
2128
+ return c.json({ error: "Failed to load session" }, 500);
2129
+ }
2130
+ }
2131
+
2132
+ // src/api/request-payloads.ts
2133
+ function isRecord(value) {
2134
+ return typeof value === "object" && value !== null;
2135
+ }
2136
+ function parseSessionReferencePayload(value) {
2137
+ if (!isRecord(value) || typeof value.agentName !== "string" || typeof value.sessionId !== "string" || !value.agentName.trim() || !value.sessionId) {
2138
+ return null;
2139
+ }
2140
+ return normalizeSessionReference({
2141
+ agentName: value.agentName.trim().toLowerCase(),
2142
+ sessionId: value.sessionId
2143
+ });
2144
+ }
2145
+ function parseBookmarkReference(value) {
2146
+ if (!isRecord(value)) return null;
2147
+ const reference = parseSessionReferencePayload(value.reference);
2148
+ if (reference) return reference;
2149
+ if (typeof value.agentKey !== "string" || typeof value.sessionId !== "string" || !value.agentKey.trim() || !value.sessionId) {
2150
+ return null;
2151
+ }
2152
+ return normalizeSessionReference({
2153
+ agentName: value.agentKey.trim().toLowerCase(),
2154
+ sessionId: value.sessionId
2003
2155
  });
2004
2156
  }
2157
+ function parseBookmarkImport(value) {
2158
+ if (!isRecord(value)) return null;
2159
+ const reference = parseBookmarkReference(value);
2160
+ if (!reference) return null;
2161
+ const timestamp = value.bookmarkedAt ?? value.bookmarked_at;
2162
+ if (timestamp != null && (typeof timestamp !== "number" || !Number.isFinite(timestamp))) {
2163
+ return null;
2164
+ }
2165
+ return {
2166
+ reference,
2167
+ bookmarkedAt: typeof timestamp === "number" ? timestamp : Date.now()
2168
+ };
2169
+ }
2170
+ function sanitizeClientLogData(value) {
2171
+ if (!isRecord(value)) return {};
2172
+ const sanitized = {};
2173
+ for (const [key, item] of Object.entries(value)) {
2174
+ if (CLIENT_LOG_STRING_FIELDS.has(key) && typeof item === "string") {
2175
+ if (key === "operation_id" && !UUID_PATTERN.test(item)) continue;
2176
+ sanitized[key] = item.slice(0, 300);
2177
+ continue;
2178
+ }
2179
+ if (CLIENT_LOG_NUMBER_FIELDS.has(key) && typeof item === "number" && Number.isFinite(item) && item >= 0) {
2180
+ sanitized[key] = item;
2181
+ continue;
2182
+ }
2183
+ if (CLIENT_LOG_NULLABLE_FIELDS.has(key) && item === null) sanitized[key] = null;
2184
+ }
2185
+ return sanitized;
2186
+ }
2187
+ var CLIENT_LOG_STRING_FIELDS = /* @__PURE__ */ new Set([
2188
+ "agent",
2189
+ "error_name",
2190
+ "mode",
2191
+ "operation_id",
2192
+ "phase",
2193
+ "profiler_id",
2194
+ "reason",
2195
+ "request_key",
2196
+ "session",
2197
+ "source",
2198
+ "trigger"
2199
+ ]);
2200
+ var CLIENT_LOG_NUMBER_FIELDS = /* @__PURE__ */ new Set([
2201
+ "actual_duration_ms",
2202
+ "agents",
2203
+ "base_duration_ms",
2204
+ "commit_time_ms",
2205
+ "duration_ms",
2206
+ "error_status",
2207
+ "messages",
2208
+ "query_length",
2209
+ "results",
2210
+ "sessions",
2211
+ "start_time_ms"
2212
+ ]);
2213
+ var CLIENT_LOG_NULLABLE_FIELDS = /* @__PURE__ */ new Set(["agent", "session"]);
2214
+ 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;
2215
+
2216
+ // src/api/client-log-handler.ts
2217
+ var CLIENT_LOG_EVENTS = /* @__PURE__ */ new Set([
2218
+ "app.load.done",
2219
+ "app.load.error",
2220
+ "app.load.start",
2221
+ "bookmark.add",
2222
+ "bookmark.delete",
2223
+ "react.profiler.commit",
2224
+ "route.change",
2225
+ "search.done",
2226
+ "search.error",
2227
+ "search.start",
2228
+ "session.markdown_copy.done",
2229
+ "session.markdown_copy.error",
2230
+ "session.open.cancel",
2231
+ "session.open.done",
2232
+ "session.open.error",
2233
+ "session.open.start"
2234
+ ]);
2235
+ async function handlePostClientLog(c) {
2236
+ const payload = await c.req.json().catch(() => null);
2237
+ const rawEvent = payload?.event;
2238
+ if (typeof rawEvent !== "string") {
2239
+ return c.json({ ok: false }, 400);
2240
+ }
2241
+ const event = rawEvent.trim();
2242
+ if (!CLIENT_LOG_EVENTS.has(event)) return c.json({ ok: false }, 400);
2243
+ appLogger.info(`client.${event}`, sanitizeClientLogData(payload?.data));
2244
+ return c.json({ ok: true });
2245
+ }
2005
2246
 
2006
2247
  // src/api/bookmark-handlers.ts
2007
2248
  function withStorageErrors(handler, onUnavailable) {
@@ -2041,7 +2282,7 @@ function handleGetBookmarks(c, scanSource) {
2041
2282
  storageAvailable: true
2042
2283
  });
2043
2284
  },
2044
- () => c.json({ bookmarks: [], storageAvailable: false })
2285
+ () => c.json({ error: "Bookmark storage is unavailable" }, 503)
2045
2286
  );
2046
2287
  }
2047
2288
  function isKnownAgentKey(agentKey) {
@@ -2296,7 +2537,7 @@ function createSseConnectionBudget(maxConnections) {
2296
2537
  }
2297
2538
  };
2298
2539
  }
2299
- function createSseResponse(eventSource, signal, releaseConnection) {
2540
+ function createSseResponse(eventSource, requestSignal, shutdownSignal, closeConnection) {
2300
2541
  let cancelStream = () => {
2301
2542
  };
2302
2543
  let drainStream = () => {
@@ -2309,26 +2550,32 @@ function createSseResponse(eventSource, signal, releaseConnection) {
2309
2550
  };
2310
2551
  let unsubscribeScanStatus = () => {
2311
2552
  };
2312
- let abortStream = () => {
2553
+ let disconnectStream = () => {
2554
+ };
2555
+ let shutdownStream = () => {
2313
2556
  };
2314
2557
  let heartbeat;
2315
2558
  let buffer;
2316
- const cleanup = () => {
2559
+ const cleanup = (reason) => {
2317
2560
  if (isClosed) return false;
2318
2561
  isClosed = true;
2319
2562
  buffer?.close();
2320
2563
  if (heartbeat) clearInterval(heartbeat);
2321
2564
  unsubscribeSessions();
2322
2565
  unsubscribeScanStatus();
2323
- signal.removeEventListener("abort", abortStream);
2324
- releaseConnection();
2566
+ requestSignal.removeEventListener("abort", disconnectStream);
2567
+ shutdownSignal?.removeEventListener("abort", shutdownStream);
2568
+ closeConnection(reason);
2325
2569
  return true;
2326
2570
  };
2327
- abortStream = () => {
2328
- if (cleanup()) controller.close();
2571
+ disconnectStream = () => {
2572
+ if (cleanup("client_disconnected")) controller.close();
2573
+ };
2574
+ shutdownStream = () => {
2575
+ if (cleanup("server_shutdown")) controller.close();
2329
2576
  };
2330
2577
  buffer = new SseEventBuffer(controller, () => {
2331
- if (cleanup()) controller.error(new Error("SSE client fell behind"));
2578
+ if (cleanup("client_too_slow")) controller.error(new Error("SSE client fell behind"));
2332
2579
  });
2333
2580
  drainStream = () => buffer.drain();
2334
2581
  try {
@@ -2342,12 +2589,16 @@ function createSseResponse(eventSource, signal, releaseConnection) {
2342
2589
  });
2343
2590
  heartbeat = setInterval(() => buffer?.enqueueHeartbeat(), 15e3);
2344
2591
  cancelStream = () => {
2345
- cleanup();
2592
+ cleanup("client_cancelled");
2346
2593
  };
2347
- if (signal.aborted) abortStream();
2348
- else signal.addEventListener("abort", abortStream, { once: true });
2594
+ if (shutdownSignal?.aborted) shutdownStream();
2595
+ else if (requestSignal.aborted) disconnectStream();
2596
+ else {
2597
+ requestSignal.addEventListener("abort", disconnectStream, { once: true });
2598
+ shutdownSignal?.addEventListener("abort", shutdownStream, { once: true });
2599
+ }
2349
2600
  } catch (error) {
2350
- cleanup();
2601
+ cleanup("setup_failed");
2351
2602
  throw error;
2352
2603
  }
2353
2604
  },
@@ -2391,9 +2642,12 @@ function createApiRoutes(scanSource, eventSource, options = {}) {
2391
2642
  );
2392
2643
  api.get(
2393
2644
  "/file-activity",
2394
- (c) => handleGetFileActivity(c, listDefaults, options.projectIdentityResolver)
2645
+ (c) => handleGetFileActivity(c, scanSource, listDefaults, options.projectIdentityResolver)
2646
+ );
2647
+ api.get(
2648
+ "/sessions/:agent/:id",
2649
+ (c) => handleGetSessionData(c, scanSource, options.loadSessionDetail)
2395
2650
  );
2396
- api.get("/sessions/:agent/:id", (c) => handleGetSessionData(c, scanSource));
2397
2651
  api.get("/dashboard", (c) => handleGetDashboard(c, scanSource, listDefaults));
2398
2652
  api.get("/bookmarks", (c) => handleGetBookmarks(c, scanSource));
2399
2653
  api.put("/bookmarks", (c) => handlePutBookmark(c));
@@ -2413,11 +2667,34 @@ function createApiRoutes(scanSource, eventSource, options = {}) {
2413
2667
  c.header("Retry-After", "1");
2414
2668
  return c.json({ error: "Too many active event streams" }, 429);
2415
2669
  }
2416
- const signal = options.shutdownSignal ? AbortSignal.any([c.req.raw.signal, options.shutdownSignal]) : c.req.raw.signal;
2670
+ const connectionId = randomUUID2();
2671
+ const connectedAt = performance.now();
2672
+ let isClosed = false;
2673
+ appLogger.info("api.sse.connection.opened", {
2674
+ connection_id: connectionId,
2675
+ active_connections: sseConnections.activeCount(),
2676
+ connection_limit: MAX_ACTIVE_SSE_CONNECTIONS
2677
+ });
2678
+ const closeConnection = (reason) => {
2679
+ if (isClosed) return;
2680
+ isClosed = true;
2681
+ releaseConnection();
2682
+ appLogger.info("api.sse.connection.closed", {
2683
+ connection_id: connectionId,
2684
+ close_reason: reason,
2685
+ duration_ms: Math.max(0, Math.round(performance.now() - connectedAt)),
2686
+ active_connections: sseConnections.activeCount()
2687
+ });
2688
+ };
2417
2689
  try {
2418
- return createSseResponse(eventSource, signal, releaseConnection);
2690
+ return createSseResponse(
2691
+ eventSource,
2692
+ c.req.raw.signal,
2693
+ options.shutdownSignal,
2694
+ closeConnection
2695
+ );
2419
2696
  } catch (error) {
2420
- releaseConnection();
2697
+ closeConnection("setup_failed");
2421
2698
  throw error;
2422
2699
  }
2423
2700
  });
@@ -2527,7 +2804,6 @@ function loopbackWriteGuard() {
2527
2804
  if (!decision.allowed) {
2528
2805
  appLogger.warn("http.loopback_request.rejected", {
2529
2806
  method: c.req.method,
2530
- path: url.pathname,
2531
2807
  reason: decision.reason
2532
2808
  });
2533
2809
  const error = decision.reason === "content-type" ? "Write requests require application/json" : "Cross-origin API request rejected";
@@ -2661,6 +2937,29 @@ function accessTokenAuth(expectedToken) {
2661
2937
  // src/server.ts
2662
2938
  var MAX_API_REQUEST_BYTES = 1024 * 1024;
2663
2939
  var SERVER_CLOSE_GRACE_MS = 1e3;
2940
+ 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;
2941
+ var OBSERVABLE_QUERY_KEYS = /* @__PURE__ */ new Set([
2942
+ "agent",
2943
+ "costMax",
2944
+ "costMin",
2945
+ "cursor",
2946
+ "cwd",
2947
+ "days",
2948
+ "fileActivity",
2949
+ "from",
2950
+ "kind",
2951
+ "limit",
2952
+ "messageCursor",
2953
+ "path",
2954
+ "project",
2955
+ "projectKey",
2956
+ "projectKind",
2957
+ "q",
2958
+ "sessionId",
2959
+ "tag",
2960
+ "to",
2961
+ "tool"
2962
+ ]);
2664
2963
  function findWebDistPath() {
2665
2964
  const __dirname2 = dirname(fileURLToPath(import.meta.url));
2666
2965
  const packagedPath = resolve(__dirname2, "web");
@@ -2700,6 +2999,32 @@ function getListeningPort(server, fallback) {
2700
2999
  const address = server.address();
2701
3000
  return typeof address === "object" && address !== null ? address.port : fallback;
2702
3001
  }
3002
+ function optionalOperationId(value) {
3003
+ return value && OPERATION_ID_PATTERN.test(value) ? value : void 0;
3004
+ }
3005
+ function errorStatus(error) {
3006
+ try {
3007
+ if (typeof error !== "object" || error === null) return 500;
3008
+ const status = Reflect.get(error, "status");
3009
+ if (typeof status === "number" && status >= 400 && status <= 599) return status;
3010
+ } catch {
3011
+ }
3012
+ return 500;
3013
+ }
3014
+ function matchedRoute(context) {
3015
+ try {
3016
+ return matchedRoutes(context).map((route) => route.path).filter((path) => !path.includes("*")).at(-1) ?? "<unmatched>";
3017
+ } catch {
3018
+ return "<unmatched>";
3019
+ }
3020
+ }
3021
+ function queryKeys(url) {
3022
+ try {
3023
+ return [...new Set(new URL(url).searchParams.keys())].filter((key) => OBSERVABLE_QUERY_KEYS.has(key)).toSorted();
3024
+ } catch {
3025
+ return [];
3026
+ }
3027
+ }
2703
3028
  async function closeHttpServer(server, port) {
2704
3029
  if (!server) return;
2705
3030
  let forceCloseTimer;
@@ -2731,6 +3056,7 @@ async function createServer(port, store, options = {}) {
2731
3056
  const loopbackAuthorityEnabled = !accessPolicy.remoteAccessRequired;
2732
3057
  const shutdownController = new AbortController();
2733
3058
  const projectIdentityResolver = options.projectIdentityResolver ?? createProjectIdentityResolver();
3059
+ const sessionDetailLoader = new ThreadSessionDetailLoader();
2734
3060
  let actualPort = null;
2735
3061
  appLogger.info("server.access_policy", {
2736
3062
  bind_category: accessPolicy.bindCategory,
@@ -2772,41 +3098,48 @@ async function createServer(port, store, options = {}) {
2772
3098
  xXssProtection: false
2773
3099
  })
2774
3100
  );
3101
+ app.use("*", async (c, next) => {
3102
+ const startedAt = performance.now();
3103
+ const requestId = randomUUID3();
3104
+ const actualPath = c.req.path;
3105
+ const operationId = optionalOperationId(c.req.header(CODESESH_OPERATION_ID_HEADER));
3106
+ const context = actualPath === "/api/events" ? {} : { request_id: requestId, ...operationId ? { operation_id: operationId } : {} };
3107
+ c.header(CODESESH_REQUEST_ID_HEADER, requestId);
3108
+ return appLogger.restoreContext(context, async () => {
3109
+ let thrown;
3110
+ try {
3111
+ await next();
3112
+ } catch (error) {
3113
+ thrown = error;
3114
+ throw error;
3115
+ } finally {
3116
+ const requestError = thrown ?? c.error;
3117
+ appLogger.info("http.request", {
3118
+ method: c.req.method,
3119
+ route: matchedRoute(c),
3120
+ query_keys: queryKeys(c.req.url),
3121
+ status: requestError == null ? c.res.status : errorStatus(requestError),
3122
+ duration_ms: Math.round(performance.now() - startedAt),
3123
+ ...requestError == null ? {} : { error: requestError }
3124
+ });
3125
+ }
3126
+ });
3127
+ });
2775
3128
  if (loopbackAuthorityEnabled) {
2776
3129
  app.use("*", async (c, next) => {
2777
3130
  const decision = validateLoopbackAuthority(c.env.incoming.rawHeaders, hostname, actualPort);
2778
3131
  if (!decision.allowed) {
2779
3132
  appLogger.warn("http.loopback_authority.rejected", {
2780
3133
  method: c.req.method,
2781
- path: new URL(c.req.url).pathname,
3134
+ route: "<rejected>",
2782
3135
  reason: decision.reason,
2783
- authority: decision.authority
3136
+ authority_present: Boolean(decision.authority)
2784
3137
  });
2785
3138
  return c.json({ error: "Loopback request authority rejected" }, 403);
2786
3139
  }
2787
3140
  await next();
2788
3141
  });
2789
3142
  }
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
3143
  if (transport.kind === "trusted-proxy") {
2811
3144
  app.use("/api/*", requireProxyTls());
2812
3145
  }
@@ -2825,7 +3158,8 @@ async function createServer(port, store, options = {}) {
2825
3158
  defaultSessionTo: options.defaultSessionTo,
2826
3159
  defaultSessionDays: options.defaultSessionDays,
2827
3160
  shutdownSignal: shutdownController.signal,
2828
- projectIdentityResolver
3161
+ projectIdentityResolver,
3162
+ loadSessionDetail: sessionDetailLoader.load
2829
3163
  };
2830
3164
  app.route("/api", createApiRoutes(store, store, routeOptions));
2831
3165
  const webDistPath = options.webDistPath ?? findWebDistPath();
@@ -2909,6 +3243,7 @@ async function createServer(port, store, options = {}) {
2909
3243
  appLogger.info("server.shutdown", { port: actualPort });
2910
3244
  shutdownController.abort();
2911
3245
  try {
3246
+ await sessionDetailLoader.shutdown();
2912
3247
  appLogger.info("server.shutdown.phase", { phase: "http-closing", port: actualPort });
2913
3248
  await closeHttpServer(server, actualPort);
2914
3249
  } finally {
@@ -3074,14 +3409,17 @@ var AgentOperationScheduler = class {
3074
3409
  const run = previous.then(async () => {
3075
3410
  if (this.isStopped) return "skipped";
3076
3411
  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
- }
3412
+ return appLogger.runWithContext({ operation_id: lifecycle.operationId }, async () => {
3413
+ this.logOperationStarted(lifecycle);
3414
+ try {
3415
+ const result = await operation();
3416
+ this.completeOperation(lifecycle, result);
3417
+ return result;
3418
+ } catch (error) {
3419
+ this.completeOperation(lifecycle, "failed");
3420
+ throw error;
3421
+ }
3422
+ });
3085
3423
  });
3086
3424
  const tail = run.then(
3087
3425
  () => void 0,
@@ -3139,13 +3477,21 @@ var AgentOperationScheduler = class {
3139
3477
  const generation = (this.operationGenerations.get(agentName) ?? 0) + 1;
3140
3478
  const startedAt = Date.now();
3141
3479
  this.operationGenerations.set(agentName, generation);
3142
- appLogger.info("scan.agent_operation.started", {
3143
- agent: agentName,
3144
- operation: kind,
3480
+ return {
3481
+ agentName,
3482
+ kind,
3145
3483
  generation,
3146
- started_at: startedAt
3484
+ operationId: `${agentName}:${generation}`,
3485
+ startedAt
3486
+ };
3487
+ }
3488
+ logOperationStarted(lifecycle) {
3489
+ appLogger.info("scan.agent_operation.started", {
3490
+ agent: lifecycle.agentName,
3491
+ operation: lifecycle.kind,
3492
+ generation: lifecycle.generation,
3493
+ started_at: lifecycle.startedAt
3147
3494
  });
3148
- return { agentName, kind, generation, startedAt };
3149
3495
  }
3150
3496
  completeOperation(lifecycle, result) {
3151
3497
  const completedAt = Date.now();
@@ -3256,23 +3602,27 @@ var BackfillLifecycle = class {
3256
3602
  var LiveSessionIndex = class {
3257
3603
  agents = [];
3258
3604
  agentsByName = /* @__PURE__ */ new Map();
3605
+ queryScope;
3606
+ allByAgent = {};
3259
3607
  byAgent = {};
3260
3608
  sessions = [];
3261
3609
  cacheFailures = {};
3262
3610
  scanFailures = {};
3263
3611
  signatureCaches = /* @__PURE__ */ new Map();
3264
3612
  initialize(snapshot, options = {}) {
3613
+ this.queryScope = options.queryScope;
3265
3614
  const agentMap = /* @__PURE__ */ new Map();
3266
3615
  for (const agent of snapshot.agents) agentMap.set(agent.name, agent);
3267
3616
  for (const agent of options.registeredAgents ?? []) {
3268
3617
  if (!agentMap.has(agent.name)) agentMap.set(agent.name, agent);
3269
3618
  }
3270
3619
  this.agents = [...agentMap.values()].filter(
3271
- (agent) => !options.allowedAgents || options.allowedAgents.has(agent.name.toLowerCase())
3620
+ (agent) => !this.queryScope?.agents?.length || this.queryScope.agents.includes(agent.name.toLowerCase())
3272
3621
  );
3273
3622
  this.agentsByName = new Map(this.agents.map((agent) => [agent.name, agent]));
3274
3623
  this.cacheFailures = { ...snapshot.cacheFailures };
3275
3624
  this.scanFailures = { ...snapshot.scanFailures };
3625
+ this.allByAgent = {};
3276
3626
  this.byAgent = Object.fromEntries(
3277
3627
  this.agents.flatMap((agent) => {
3278
3628
  if (this.scanFailures[agent.name] && !(agent.name in snapshot.byAgent)) return [];
@@ -3281,7 +3631,8 @@ var LiveSessionIndex = class {
3281
3631
  assertSessionIdentity(session, agent.name);
3282
3632
  assertIdentifiedSessionHead(session);
3283
3633
  }
3284
- return [[agent.name, sortSessions(sessions)]];
3634
+ this.allByAgent[agent.name] = sortSessions(sessions);
3635
+ return [[agent.name, this.visibleSessions(this.allByAgent[agent.name])]];
3285
3636
  })
3286
3637
  );
3287
3638
  this.sessions = mergeSortedSessions(Object.values(this.byAgent));
@@ -3299,11 +3650,15 @@ var LiveSessionIndex = class {
3299
3650
  findAgent(agentName) {
3300
3651
  return this.agentsByName.get(agentName);
3301
3652
  }
3653
+ agentSessions(agentName) {
3654
+ return this.allByAgent[agentName] ?? [];
3655
+ }
3302
3656
  commitAgentSessions(agentName, nextSessions, candidateChangedIds = []) {
3303
3657
  for (const session of nextSessions) {
3304
3658
  assertSessionIdentity(session, agentName);
3305
3659
  assertIdentifiedSessionHead(session);
3306
3660
  }
3661
+ const visibleSessions = this.visibleSessions(nextSessions);
3307
3662
  delete this.cacheFailures[agentName];
3308
3663
  delete this.scanFailures[agentName];
3309
3664
  const previousSessions = this.byAgent[agentName] ?? [];
@@ -3311,15 +3666,18 @@ var LiveSessionIndex = class {
3311
3666
  const signatureCache = this.signatureCache(agentName);
3312
3667
  const { changes, removedSessionIds, counts } = computeSessionDiff(
3313
3668
  previousSessions,
3314
- nextSessions,
3669
+ visibleSessions,
3315
3670
  candidateChangedIds,
3316
3671
  sessionSignature,
3317
3672
  signatureCache
3318
3673
  );
3319
3674
  for (const removedId of removedSessionIds) signatureCache.delete(removedId);
3320
- this.byAgent[agentName] = sortSessions(nextSessions);
3675
+ this.allByAgent[agentName] = sortSessions(nextSessions);
3676
+ const hasChanges = counts.new > 0 || counts.updated > 0 || counts.removed > 0;
3677
+ if (!hasChanges && agentName in this.byAgent) return null;
3678
+ this.byAgent[agentName] = this.visibleSessions(this.allByAgent[agentName]);
3321
3679
  this.sessions = mergeSortedSessions(Object.values(this.byAgent));
3322
- if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) return null;
3680
+ if (!hasChanges) return null;
3323
3681
  const changedSessionHeads = changes.map(({ session }) => ({
3324
3682
  reference: session.reference,
3325
3683
  session
@@ -3358,12 +3716,16 @@ var LiveSessionIndex = class {
3358
3716
  this.signatureCaches.set(agentName, cache);
3359
3717
  return cache;
3360
3718
  }
3719
+ visibleSessions(sessions) {
3720
+ if (!this.queryScope?.projectScope) return sessions;
3721
+ return sessions.filter((session) => matchesSessionQueryScope(session, this.queryScope));
3722
+ }
3361
3723
  };
3362
3724
 
3363
3725
  // src/search-index-job-runner.ts
3364
3726
  import { existsSync as existsSync2 } from "fs";
3365
3727
  import { fileURLToPath as fileURLToPath2 } from "url";
3366
- import { Worker as Worker2 } from "worker_threads";
3728
+ import { Worker as Worker3 } from "worker_threads";
3367
3729
 
3368
3730
  // src/errors.ts
3369
3731
  function toError2(value) {
@@ -3371,14 +3733,24 @@ function toError2(value) {
3371
3733
  }
3372
3734
 
3373
3735
  // src/pending-search-index-jobs.ts
3736
+ var LOG_CONTEXT_KEYS = ["request_id", "operation_id", "publication_id"];
3737
+ function commonLogContext(left, right) {
3738
+ const common = {};
3739
+ for (const key of LOG_CONTEXT_KEYS) {
3740
+ if (left[key] !== void 0 && left[key] === right[key]) common[key] = left[key];
3741
+ }
3742
+ return common;
3743
+ }
3374
3744
  var PendingSearchIndexJobBatch = class {
3375
- constructor(id, context, jobs, waiter) {
3745
+ constructor(id, context, jobs, waiter, logContext) {
3376
3746
  this.id = id;
3377
3747
  this.context = context;
3378
- this.merge(context, jobs, waiter);
3748
+ this.logContext = logContext;
3749
+ this.merge(context, jobs, waiter, logContext);
3379
3750
  }
3380
3751
  id;
3381
3752
  context;
3753
+ logContext;
3382
3754
  jobsByAgent = /* @__PURE__ */ new Map();
3383
3755
  waiters = [];
3384
3756
  settled = false;
@@ -3420,8 +3792,9 @@ var PendingSearchIndexJobBatch = class {
3420
3792
  }
3421
3793
  }
3422
3794
  }
3423
- merge(context, jobs, waiter) {
3795
+ merge(context, jobs, waiter, logContext) {
3424
3796
  this.context = context;
3797
+ this.logContext = commonLogContext(this.logContext, logContext);
3425
3798
  this.waiters.push(waiter);
3426
3799
  for (const job of jobs) this.mergeJob(job);
3427
3800
  }
@@ -3459,7 +3832,7 @@ var PendingSearchIndexJobs = class {
3459
3832
  get changeCount() {
3460
3833
  return this.pendingBatch?.changeCount ?? 0;
3461
3834
  }
3462
- enqueue(id, context, jobs, onStarted, onProgress) {
3835
+ enqueue(id, context, jobs, onStarted, onProgress, logContext = {}) {
3463
3836
  if (jobs.length === 0) return Promise.resolve();
3464
3837
  return new Promise((resolve4, reject) => {
3465
3838
  const waiter = {
@@ -3470,9 +3843,9 @@ var PendingSearchIndexJobs = class {
3470
3843
  agentNames: new Set(jobs.map((job) => job.agentName))
3471
3844
  };
3472
3845
  if (this.pendingBatch) {
3473
- this.pendingBatch.merge(context, jobs, waiter);
3846
+ this.pendingBatch.merge(context, jobs, waiter, logContext);
3474
3847
  } else {
3475
- this.pendingBatch = new PendingSearchIndexJobBatch(id, context, jobs, waiter);
3848
+ this.pendingBatch = new PendingSearchIndexJobBatch(id, context, jobs, waiter, logContext);
3476
3849
  this.batches.add(this.pendingBatch);
3477
3850
  }
3478
3851
  });
@@ -3560,6 +3933,7 @@ var SearchIndexJobRunner = class {
3560
3933
  pendingJobs = new PendingSearchIndexJobs();
3561
3934
  pendingMaintenanceJobs = new PendingSearchIndexJobs();
3562
3935
  isShuttingDown = false;
3936
+ retirements = /* @__PURE__ */ new Set();
3563
3937
  enqueue(context, jobs, onStarted, onProgress) {
3564
3938
  return this.enqueueIn(this.pendingJobs, context, jobs, onStarted, onProgress);
3565
3939
  }
@@ -3570,7 +3944,14 @@ var SearchIndexJobRunner = class {
3570
3944
  if (jobs.length === 0) return Promise.resolve();
3571
3945
  if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE2));
3572
3946
  const batchId = this.nextBatchId++;
3573
- const completion = queue.enqueue(batchId, context, jobs, onStarted, onProgress);
3947
+ const completion = queue.enqueue(
3948
+ batchId,
3949
+ context,
3950
+ jobs,
3951
+ onStarted,
3952
+ onProgress,
3953
+ appLogger.captureContext()
3954
+ );
3574
3955
  if (this.activeBatch) {
3575
3956
  const snapshot = this.snapshot();
3576
3957
  appLogger.debug("search_index.worker_queued", {
@@ -3606,25 +3987,33 @@ var SearchIndexJobRunner = class {
3606
3987
  if (activeBatch) this.settle(activeBatch, shutdownError);
3607
3988
  this.pendingJobs.rejectAll(shutdownError);
3608
3989
  this.pendingMaintenanceJobs.rejectAll(shutdownError);
3609
- if (worker) await worker.terminate();
3990
+ const activeRetirement = worker ? this.retireWorker(worker) : null;
3991
+ const retirements = [...this.retirements];
3992
+ try {
3993
+ if (activeRetirement) await activeRetirement;
3994
+ } finally {
3995
+ await Promise.allSettled(retirements);
3996
+ }
3610
3997
  }
3611
3998
  startNextBatch() {
3612
3999
  if (this.isShuttingDown || this.activeBatch) return;
3613
4000
  const batch = this.pendingJobs.take() ?? this.pendingMaintenanceJobs.take();
3614
4001
  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", {
4002
+ appLogger.restoreContext(batch.logContext, () => {
4003
+ appLogger.info("search_index.worker_dequeued", {
3622
4004
  batch_id: batch.id,
3623
4005
  context: batch.context,
3624
- error: toError2(error)
4006
+ pending_batches: this.pendingJobs.batchCount
4007
+ });
4008
+ batch.start((error) => {
4009
+ appLogger.error("search_index.worker_start_listener_failed", {
4010
+ batch_id: batch.id,
4011
+ context: batch.context,
4012
+ error: toError2(error)
4013
+ });
3625
4014
  });
4015
+ this.startBatch(batch);
3626
4016
  });
3627
- this.startBatch(batch);
3628
4017
  }
3629
4018
  startBatch(batch) {
3630
4019
  if (this.isShuttingDown) {
@@ -3635,6 +4024,7 @@ var SearchIndexJobRunner = class {
3635
4024
  type: "run",
3636
4025
  pricingGenerationId: getPricingGeneration().id,
3637
4026
  context: batch.context,
4027
+ logContext: batch.logContext,
3638
4028
  jobs: batch.jobs
3639
4029
  };
3640
4030
  const existingWorker = this.worker;
@@ -3661,7 +4051,7 @@ var SearchIndexJobRunner = class {
3661
4051
  });
3662
4052
  let worker;
3663
4053
  try {
3664
- worker = new Worker2(workerUrl, { workerData: request });
4054
+ worker = new Worker3(workerUrl, { workerData: request });
3665
4055
  } catch (error) {
3666
4056
  this.settle(batch, toError2(error));
3667
4057
  this.startNextBatch();
@@ -3674,62 +4064,76 @@ var SearchIndexJobRunner = class {
3674
4064
  if (this.worker !== worker) return;
3675
4065
  const activeBatch = this.activeBatch;
3676
4066
  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
4067
+ appLogger.restoreContext(activeBatch.logContext, () => {
4068
+ this.handleWorkerMessage(activeBatch, message);
4069
+ });
4070
+ });
4071
+ worker.on("error", (error) => {
4072
+ const activeBatch = this.worker === worker ? this.activeBatch : null;
4073
+ appLogger.restoreContext(activeBatch?.logContext ?? {}, () => {
4074
+ appLogger.error("search_index.worker_error", {
4075
+ context: activeBatch?.context,
4076
+ error
3700
4077
  });
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", {
4078
+ this.invalidateWorker(worker, toError2(error));
4079
+ });
4080
+ });
4081
+ worker.on("exit", (code) => {
4082
+ const activeBatch = this.worker === worker ? this.activeBatch : null;
4083
+ appLogger.restoreContext(
4084
+ activeBatch?.logContext ?? {},
4085
+ () => this.finishWorker(worker, code)
4086
+ );
4087
+ });
4088
+ }
4089
+ handleWorkerMessage(activeBatch, message) {
4090
+ if (message.type === "publication-progress") {
4091
+ const { agentName, stage } = message;
4092
+ activeBatch.progress({ agentName, stage }, (error) => {
4093
+ appLogger.error("search_index.worker_progress_listener_failed", {
3714
4094
  batch_id: activeBatch.id,
3715
- context: message.context,
3716
- failed_agents: message.failedAgents
4095
+ context: activeBatch.context,
4096
+ error: toError2(error)
3717
4097
  });
3718
- }
3719
- appLogger.info(`${message.context}.done`, {
3720
- duration_ms: Math.round(message.durationMs),
4098
+ });
4099
+ return;
4100
+ }
4101
+ if (message.type === "sync-result") {
4102
+ logSearchIndexSync(message.context, message.result);
4103
+ return;
4104
+ }
4105
+ if (message.type === "persist-failed") {
4106
+ appLogger.error("search_index.persist_failed", {
4107
+ batch_id: activeBatch.id,
4108
+ context: message.context,
4109
+ stage: message.stage,
4110
+ publication_id: message.publicationId,
4111
+ agent: message.agentName,
3721
4112
  sessions: message.sessions
3722
4113
  });
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
4114
+ if (message.fatal) {
4115
+ this.finishBatch(
4116
+ activeBatch,
4117
+ new Error(
4118
+ `Search index worker failed to persist ${message.stage} for ${message.agentName}`
4119
+ )
4120
+ );
4121
+ }
4122
+ return;
4123
+ }
4124
+ if (message.type !== "done") return;
4125
+ if (message.failedAgents.length > 0) {
4126
+ appLogger.warn("search_index.batch_partial", {
4127
+ batch_id: activeBatch.id,
4128
+ context: message.context,
4129
+ failed_agents: message.failedAgents
3729
4130
  });
3730
- this.invalidateWorker(worker, toError2(error));
4131
+ }
4132
+ appLogger.info(`${message.context}.done`, {
4133
+ duration_ms: Math.round(message.durationMs),
4134
+ sessions: message.sessions
3731
4135
  });
3732
- worker.on("exit", (code) => this.finishWorker(worker, code));
4136
+ this.finishBatch(activeBatch);
3733
4137
  }
3734
4138
  finishBatch(batch, error) {
3735
4139
  if (this.activeBatch !== batch) return;
@@ -3743,7 +4147,7 @@ var SearchIndexJobRunner = class {
3743
4147
  const batch = this.activeBatch;
3744
4148
  this.activeBatch = null;
3745
4149
  if (batch) this.settle(batch, error);
3746
- void worker.terminate();
4150
+ void this.retireWorker(worker);
3747
4151
  this.startNextBatch();
3748
4152
  }
3749
4153
  finishWorker(worker, code) {
@@ -3767,15 +4171,23 @@ var SearchIndexJobRunner = class {
3767
4171
  this.startNextBatch();
3768
4172
  }
3769
4173
  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"
4174
+ appLogger.restoreContext(batch.logContext, () => {
4175
+ if (!this.pendingJobs.settle(batch, error) && !this.pendingMaintenanceJobs.settle(batch, error)) {
4176
+ return;
4177
+ }
4178
+ appLogger.info("search_index.worker_settled", {
4179
+ batch_id: batch.id,
4180
+ context: batch.context,
4181
+ result: error ? "rejected" : "resolved"
4182
+ });
3777
4183
  });
3778
4184
  }
4185
+ retireWorker(worker) {
4186
+ const retirement = terminateWorkerAfterLogDrain(worker);
4187
+ this.retirements.add(retirement);
4188
+ void retirement.finally(() => this.retirements.delete(retirement)).catch(() => void 0);
4189
+ return retirement;
4190
+ }
3779
4191
  workerUrl() {
3780
4192
  const workerUrl = new URL("./search-index-worker.js", import.meta.url);
3781
4193
  if (workerUrl.protocol === "file:" && !existsSync2(fileURLToPath2(workerUrl))) return null;
@@ -3862,19 +4274,21 @@ var SearchIndexMaintenanceScheduler = class {
3862
4274
  if (outcome.status === "failed") {
3863
4275
  throw new Error(`Session cache read failed for ${agentName}`);
3864
4276
  }
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
- );
4277
+ if (!outcome.value) throw new Error(`Session cache is unavailable for ${agentName}`);
4278
+ cached = {
4279
+ sessionsById: new Map(
4280
+ outcome.value.sessions.map((session, sortIndex) => [
4281
+ session.reference.sessionId,
4282
+ { session, sortIndex }
4283
+ ])
4284
+ ),
4285
+ meta: outcome.value.meta
4286
+ };
4287
+ this.cachedSessionsByAgent.set(agentName, cached);
4288
+ }
3875
4289
  const changes = pending.sessionIds.flatMap(
3876
4290
  (sessionId) => {
3877
- const change = sessionsById.get(sessionId);
4291
+ const change = cached.sessionsById.get(sessionId);
3878
4292
  return change ? [change] : [];
3879
4293
  }
3880
4294
  );
@@ -4486,7 +4900,7 @@ var SearchIndexPublisher = class {
4486
4900
  kind: "full",
4487
4901
  context,
4488
4902
  agentName: agent.name,
4489
- sessions: snapshot.byAgent[agent.name] ?? [],
4903
+ sessions: this.options.agentSessions(agent.name),
4490
4904
  meta: buildAgentCacheMeta(agent),
4491
4905
  completeness: "partial",
4492
4906
  removedSessionIds: [],
@@ -4566,7 +4980,7 @@ var SessionPublicationCoordinator = class {
4566
4980
  publication.candidateChangedIds
4567
4981
  );
4568
4982
  const diffDuration = performance.now() - diffStartedAt;
4569
- const result = { durableCommitted: true, event, diffDuration };
4983
+ const result = { event, diffDuration };
4570
4984
  this.runPostCommit(publication, "worker_commit", () => publication.stagedRun?.commit());
4571
4985
  this.runPostCommit(publication, "callback", () => publication.onCommitted?.(result));
4572
4986
  this.runPostCommit(
@@ -4632,7 +5046,6 @@ function countSessionUpdates(event) {
4632
5046
  }
4633
5047
  var REFRESH_DEBOUNCE_MS = 200;
4634
5048
  var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
4635
- var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
4636
5049
  var SOURCE_FAILURE_SUMMARY_MAX_LENGTH = 160;
4637
5050
  function buildScanCompletion(completeness, sourceFailures) {
4638
5051
  if (sourceFailures.length === 0) return { completeness };
@@ -4644,6 +5057,15 @@ function buildScanCompletion(completeness, sourceFailures) {
4644
5057
  sourceFailureSummary: summary.length > SOURCE_FAILURE_SUMMARY_MAX_LENGTH ? `${summary.slice(0, SOURCE_FAILURE_SUMMARY_MAX_LENGTH - 1)}\u2026` : summary
4645
5058
  };
4646
5059
  }
5060
+ function selectCacheMeta(meta, sessionIds) {
5061
+ if (!sessionIds) return { ...meta };
5062
+ const selected = {};
5063
+ for (const sessionId of sessionIds) {
5064
+ const entry = meta[sessionId];
5065
+ if (entry) selected[sessionId] = entry;
5066
+ }
5067
+ return selected;
5068
+ }
4647
5069
  var AgentSyncEngine = class {
4648
5070
  constructor(options) {
4649
5071
  this.options = options;
@@ -4656,6 +5078,7 @@ var AgentSyncEngine = class {
4656
5078
  this.indexPublisher = new SearchIndexPublisher({
4657
5079
  jobs: this.searchIndexJobs,
4658
5080
  snapshot: () => this.sessionIndex.snapshot(),
5081
+ agentSessions: (agentName) => this.sessionIndex.agentSessions(agentName),
4659
5082
  readCachedSessions: (agentName) => this.readCachedSessionsOrWarn("search.index", agentName)
4660
5083
  });
4661
5084
  this.sessionPublication = new SessionPublicationCoordinator(
@@ -4781,11 +5204,21 @@ var AgentSyncEngine = class {
4781
5204
  reportPostCommitError(operation, agentName, error) {
4782
5205
  appLogger.error(`${operation}.post_commit_error`, { agent: agentName, error });
4783
5206
  }
4784
- commitRefreshChangeCheck(agentName, commit) {
5207
+ commitAgentState(operation, agent, state) {
5208
+ if (!state) return;
4785
5209
  try {
4786
- commit?.();
5210
+ agent.restoreSessionCacheMeta(state.meta);
5211
+ if (state.refreshedAt != null) {
5212
+ this.lastRefreshAtByAgent.set(agent.name, state.refreshedAt);
5213
+ }
4787
5214
  } catch (error) {
4788
- this.reportPostCommitError("scan.refresh", agentName, error);
5215
+ this.reportPostCommitError(operation, agent.name, error);
5216
+ return;
5217
+ }
5218
+ try {
5219
+ state.refreshTransaction?.commit();
5220
+ } catch (error) {
5221
+ this.reportPostCommitError(operation, agent.name, error);
4789
5222
  }
4790
5223
  }
4791
5224
  finishCommittedAgentScan(agentName, completion) {
@@ -4853,12 +5286,11 @@ var AgentSyncEngine = class {
4853
5286
  appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
4854
5287
  return { result: "skipped", completion: { completeness: "complete" } };
4855
5288
  }
4856
- const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
5289
+ const previousSessions = this.sessionIndex.agentSessions(agentName);
4857
5290
  const refreshBaseline = cached?.sessions ?? previousSessions;
4858
5291
  const cacheTimestamp = cached?.timestamp ?? this.lastRefreshAtByAgent.get(agentName) ?? 0;
4859
5292
  if (cached) agent.restoreSessionCacheMeta(cached.meta);
4860
5293
  const durableMeta = agent.snapshotSessionCacheMeta();
4861
- const durableLastRefreshAt = this.lastRefreshAtByAgent.get(agentName);
4862
5294
  const initialization = readAgentCacheInitialization(agentName);
4863
5295
  if (initialization.status === "failed") {
4864
5296
  appLogger.warn("scan.refresh.cache_state_unavailable", {
@@ -4868,40 +5300,50 @@ var AgentSyncEngine = class {
4868
5300
  return { result: "unchanged", completion: { completeness: "complete" } };
4869
5301
  }
4870
5302
  const isInitialized = initialization.value;
4871
- const refresh = await selectAgentRefresh(agent, {
5303
+ const refreshTransaction = await beginAgentRefresh(agent, {
4872
5304
  initialized: isInitialized,
4873
5305
  sinceTimestamp: cacheTimestamp,
4874
5306
  cachedSessions: refreshBaseline
4875
5307
  });
5308
+ const refresh = refreshTransaction.selection;
4876
5309
  const availabilityDuration = refresh.availabilityDurationMs;
4877
5310
  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);
5311
+ try {
5312
+ if (refresh.kind === "unavailable") {
5313
+ strategyResult = this.refreshUnavailableAgent(agentName);
5314
+ } else if (refresh.kind === "initialize") {
5315
+ strategyResult = await this.initializeAgent(agent, previousSessions);
5316
+ } else if (refresh.kind === "synchronize") {
5317
+ strategyResult = await this.syncAgentSources(
5318
+ agent,
5319
+ cached ?? {
5320
+ sessions: refreshBaseline,
5321
+ meta: durableMeta
5322
+ },
5323
+ startedAt
5324
+ );
5325
+ } else {
5326
+ strategyResult = await this.refreshChangedAgent(
5327
+ agent,
5328
+ refresh,
5329
+ refreshTransaction,
5330
+ refreshBaseline,
5331
+ startedAt
5332
+ );
5333
+ }
5334
+ } finally {
5335
+ agent.restoreSessionCacheMeta(durableMeta);
4893
5336
  }
4894
5337
  const stagedRun = strategyResult.workerRun;
4895
5338
  if (strategyResult.status === "unchanged") {
4896
5339
  stagedRun?.commit();
4897
- this.commitRefreshChangeCheck(agentName, strategyResult.commitChangeCheck);
5340
+ this.commitAgentState("scan.refresh", agent, strategyResult.pendingAgentState);
4898
5341
  return {
4899
5342
  result: "unchanged",
4900
5343
  completion: buildScanCompletion(strategyResult.completeness, strategyResult.sourceFailures)
4901
5344
  };
4902
5345
  }
4903
5346
  const nextSessions = attachMissingProjectIdentities(strategyResult.nextSessions);
4904
- const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
4905
5347
  const persistenceDiff = strategyResult.publication.kind === "changes" ? strategyResult.publication.diff : null;
4906
5348
  const publicationSessions = strategyResult.publication.kind === "full" ? strategyResult.publication.sessions : nextSessions;
4907
5349
  const explicitRemovedSessionIds = strategyResult.publication.kind === "full" ? strategyResult.publication.explicitRemovedSessionIds : [];
@@ -4923,6 +5365,7 @@ var AgentSyncEngine = class {
4923
5365
  delete_candidates: replacementDeleteCandidates
4924
5366
  });
4925
5367
  const changedSessionIds = persistenceDiff ? new Set(persistenceDiff.changedSessions.map(({ session }) => session.reference.sessionId)) : void 0;
5368
+ const pendingMeta = strategyResult.pendingAgentState?.meta ?? durableMeta;
4926
5369
  const persistStartedAt = performance.now();
4927
5370
  const persistentJob = persistenceDiff ? {
4928
5371
  kind: "changes",
@@ -4930,18 +5373,16 @@ var AgentSyncEngine = class {
4930
5373
  agentName,
4931
5374
  changes: persistenceDiff.changedSessions,
4932
5375
  removedSessionIds: persistenceDiff.removedSessionIds,
4933
- meta: buildAgentCacheMeta(agent, changedSessionIds),
4934
- ...searchIndexOptions ? { searchIndexOptions } : {}
5376
+ meta: selectCacheMeta(pendingMeta, changedSessionIds)
4935
5377
  } : {
4936
5378
  kind: "full",
4937
5379
  context: "scan.refresh",
4938
5380
  agentName,
4939
5381
  sessions: publicationSessions,
4940
- meta: buildAgentCacheMeta(agent),
5382
+ meta: selectCacheMeta(pendingMeta),
4941
5383
  completeness: strategyResult.completeness,
4942
5384
  removedSessionIds: explicitRemovedSessionIds,
4943
- saveCache: true,
4944
- ...searchIndexOptions ? { searchIndexOptions } : {}
5385
+ saveCache: true
4945
5386
  };
4946
5387
  const completion = buildScanCompletion(
4947
5388
  strategyResult.completeness,
@@ -4961,12 +5402,9 @@ var AgentSyncEngine = class {
4961
5402
  });
4962
5403
  } catch (error) {
4963
5404
  stagedRun?.discard();
4964
- agent.restoreSessionCacheMeta(durableMeta);
4965
- if (durableLastRefreshAt == null) this.lastRefreshAtByAgent.delete(agentName);
4966
- else this.lastRefreshAtByAgent.set(agentName, durableLastRefreshAt);
4967
5405
  throw error;
4968
5406
  }
4969
- this.commitRefreshChangeCheck(agentName, strategyResult.commitChangeCheck);
5407
+ this.commitAgentState("scan.refresh", agent, strategyResult.pendingAgentState);
4970
5408
  const persistDuration = performance.now() - persistStartedAt;
4971
5409
  const totalDurationMs = performance.now() - startedAt;
4972
5410
  try {
@@ -5011,7 +5449,7 @@ var AgentSyncEngine = class {
5011
5449
  * empty database.
5012
5450
  */
5013
5451
  refreshUnavailableAgent(agentName) {
5014
- const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
5452
+ const previousSessions = this.sessionIndex.agentSessions(agentName);
5015
5453
  if (previousSessions.length > 0) {
5016
5454
  appLogger.warn("scan.refresh.agent_unavailable", {
5017
5455
  agent: agentName,
@@ -5035,14 +5473,13 @@ var AgentSyncEngine = class {
5035
5473
  const workerRun = await this.runWorker(agent, previousSessions, { kind: "full-scan" }, scope);
5036
5474
  try {
5037
5475
  const { result } = workerRun;
5038
- agent.restoreSessionCacheMeta(result.meta);
5039
5476
  const sessions = attachMissingProjectIdentities(result.sessions);
5040
- this.lastRefreshAtByAgent.set(agent.name, Date.now());
5041
5477
  return {
5042
5478
  ...this.refreshStrategyBase(sessions, result.completeness, scope, {
5043
5479
  scanDuration: performance.now() - scanStartedAt,
5044
5480
  sourceFailures: result.sourceFailures ?? [],
5045
- workerRun
5481
+ workerRun,
5482
+ pendingAgentState: { meta: result.meta, refreshedAt: Date.now() }
5046
5483
  }),
5047
5484
  status: "continue",
5048
5485
  publication: {
@@ -5070,19 +5507,19 @@ var AgentSyncEngine = class {
5070
5507
  );
5071
5508
  try {
5072
5509
  const { result } = workerRun;
5073
- agent.restoreSessionCacheMeta(result.meta);
5074
5510
  const sessions = attachMissingProjectIdentities(result.sessions);
5075
5511
  const preciseChangedIds = result.changedIds ?? [];
5076
5512
  const persistenceDiff = buildSessionPersistenceDiff(baseline.sessions, sessions, {
5077
5513
  candidateChangedIds: preciseChangedIds
5078
5514
  });
5079
- this.lastRefreshAtByAgent.set(agent.name, Date.now());
5515
+ const pendingAgentState = { meta: result.meta, refreshedAt: Date.now() };
5080
5516
  if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0 && (result.sourceFailures?.length ?? 0) === 0) {
5081
5517
  this.logUnchangedRefresh(agent.name, refreshStartedAt);
5082
5518
  return {
5083
5519
  ...this.refreshStrategyBase(sessions, result.completeness, scope, {
5084
5520
  scanDuration: performance.now() - scanStartedAt,
5085
- workerRun
5521
+ workerRun,
5522
+ pendingAgentState
5086
5523
  }),
5087
5524
  status: "unchanged"
5088
5525
  };
@@ -5091,7 +5528,8 @@ var AgentSyncEngine = class {
5091
5528
  ...this.refreshStrategyBase(sessions, result.completeness, scope, {
5092
5529
  scanDuration: performance.now() - scanStartedAt,
5093
5530
  sourceFailures: result.sourceFailures ?? [],
5094
- workerRun
5531
+ workerRun,
5532
+ pendingAgentState
5095
5533
  }),
5096
5534
  status: "continue",
5097
5535
  publication: {
@@ -5105,7 +5543,7 @@ var AgentSyncEngine = class {
5105
5543
  throw error;
5106
5544
  }
5107
5545
  }
5108
- async refreshChangedAgent(agent, refresh, baseline, refreshStartedAt) {
5546
+ async refreshChangedAgent(agent, refresh, refreshTransaction, baseline, refreshStartedAt) {
5109
5547
  const scope = this.startupScanOptions();
5110
5548
  if (refresh.kind === "failed") {
5111
5549
  appLogger.warn("scan.refresh.change_check_failed", {
@@ -5133,9 +5571,12 @@ var AgentSyncEngine = class {
5133
5571
  const workerRun = await this.runWorker(agent, baseline, { kind: "recompute-derived" }, {});
5134
5572
  try {
5135
5573
  const { result } = workerRun;
5136
- agent.restoreSessionCacheMeta(result.meta);
5137
5574
  const sessions2 = attachMissingProjectIdentities(result.sessions);
5138
- this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
5575
+ const pendingAgentState = {
5576
+ meta: result.meta,
5577
+ refreshedAt: checkResult.timestamp,
5578
+ refreshTransaction
5579
+ };
5139
5580
  const persistenceDiff = buildSessionPersistenceDiff(baseline, sessions2);
5140
5581
  if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0) {
5141
5582
  this.logUnchangedRefresh(agent.name, refreshStartedAt);
@@ -5148,7 +5589,7 @@ var AgentSyncEngine = class {
5148
5589
  checkDuration,
5149
5590
  scanDuration: performance.now() - scanStartedAt2,
5150
5591
  workerRun,
5151
- commitChangeCheck: () => commitAgentRefreshCheck(refresh)
5592
+ pendingAgentState
5152
5593
  }
5153
5594
  ),
5154
5595
  status: "unchanged"
@@ -5164,7 +5605,7 @@ var AgentSyncEngine = class {
5164
5605
  scanDuration: performance.now() - scanStartedAt2,
5165
5606
  sourceFailures: result.sourceFailures ?? [],
5166
5607
  workerRun,
5167
- commitChangeCheck: () => commitAgentRefreshCheck(refresh)
5608
+ pendingAgentState
5168
5609
  }
5169
5610
  ),
5170
5611
  status: "continue",
@@ -5180,30 +5621,32 @@ var AgentSyncEngine = class {
5180
5621
  const workerRun = await this.runWorker(agent, baseline, { kind: "full-scan" }, scope);
5181
5622
  try {
5182
5623
  const { result } = workerRun;
5183
- agent.restoreSessionCacheMeta(result.meta);
5184
5624
  const sessions2 = attachMissingProjectIdentities(result.sessions);
5185
- this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
5625
+ const persistenceDiff = buildSessionPersistenceDiff(baseline, sessions2, {
5626
+ // Worker-reported IDs also preserve metadata-only pricing repairs.
5627
+ candidateChangedIds: result.changedIds ?? [],
5628
+ completeness: result.completeness,
5629
+ explicitRemovedSessionIds: result.explicitRemovedSessionIds
5630
+ });
5631
+ const strategy = this.refreshStrategyBase(sessions2, result.completeness, scope, {
5632
+ checkDuration,
5633
+ scanDuration: performance.now() - scanStartedAt,
5634
+ sourceFailures: result.sourceFailures ?? [],
5635
+ workerRun,
5636
+ pendingAgentState: {
5637
+ meta: result.meta,
5638
+ refreshedAt: checkResult.timestamp,
5639
+ refreshTransaction
5640
+ }
5641
+ });
5642
+ if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0 && (result.sourceFailures?.length ?? 0) === 0) {
5643
+ this.logUnchangedRefresh(agent.name, refreshStartedAt);
5644
+ return { ...strategy, status: "unchanged" };
5645
+ }
5186
5646
  return {
5187
- ...this.refreshStrategyBase(sessions2, result.completeness, scope, {
5188
- checkDuration,
5189
- scanDuration: performance.now() - scanStartedAt,
5190
- sourceFailures: result.sourceFailures ?? [],
5191
- workerRun,
5192
- commitChangeCheck: () => commitAgentRefreshCheck(refresh)
5193
- }),
5647
+ ...strategy,
5194
5648
  status: "continue",
5195
- publication: {
5196
- kind: "changes",
5197
- // Meta-only changes (e.g. a pricing capture epoch bump) leave the head
5198
- // signature intact; without the worker-reported ids they would never
5199
- // persist and checkForChanges would rescan on every startup.
5200
- diff: buildSessionPersistenceDiff(baseline, sessions2, {
5201
- candidateChangedIds: result.changedIds ?? [],
5202
- completeness: result.completeness,
5203
- explicitRemovedSessionIds: result.explicitRemovedSessionIds
5204
- }),
5205
- candidateChangedIds: []
5206
- }
5649
+ publication: { kind: "changes", diff: persistenceDiff, candidateChangedIds: [] }
5207
5650
  };
5208
5651
  } catch (error) {
5209
5652
  workerRun.discard();
@@ -5217,7 +5660,6 @@ var AgentSyncEngine = class {
5217
5660
  source.incrementalScan(baseline, preciseChangedIds, checkResult.refs, scope)
5218
5661
  )
5219
5662
  );
5220
- this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
5221
5663
  const sourceFailures = checkResult.sourceFailures ?? [];
5222
5664
  const completeness = resolveSessionSnapshotCompleteness(scope, sourceFailures);
5223
5665
  return {
@@ -5225,7 +5667,11 @@ var AgentSyncEngine = class {
5225
5667
  checkDuration,
5226
5668
  scanDuration: performance.now() - scanStartedAt,
5227
5669
  sourceFailures,
5228
- commitChangeCheck: () => commitAgentRefreshCheck(refresh)
5670
+ pendingAgentState: {
5671
+ meta: agent.snapshotSessionCacheMeta(),
5672
+ refreshedAt: checkResult.timestamp,
5673
+ refreshTransaction
5674
+ }
5229
5675
  }),
5230
5676
  status: "continue",
5231
5677
  publication: {
@@ -5301,9 +5747,8 @@ var AgentSyncEngine = class {
5301
5747
  const startedAt = performance.now();
5302
5748
  const agent = this.findAgent(agentName);
5303
5749
  if (!agent || !agent.isAvailable()) return "skipped";
5304
- const snapshot = this.sessionIndex.snapshot();
5305
5750
  const cached = this.readCachedSessionsOrWarn("scan.backfill", agentName);
5306
- const baseline = cached?.sessions ?? snapshot.byAgent[agentName] ?? [];
5751
+ const baseline = cached?.sessions ?? this.sessionIndex.agentSessions(agentName);
5307
5752
  const meta = cached?.meta ?? buildAgentCacheMeta(agent);
5308
5753
  const backfillCursor = getAgentFullSyncCursor(agentName);
5309
5754
  if (cached) agent.restoreSessionCacheMeta(cached.meta);
@@ -5337,7 +5782,6 @@ var AgentSyncEngine = class {
5337
5782
  }
5338
5783
  );
5339
5784
  const { result } = workerRun;
5340
- agent.restoreSessionCacheMeta(result.meta);
5341
5785
  const fullSessions = attachMissingProjectIdentities(result.sessions);
5342
5786
  const completion = buildScanCompletion(result.completeness, result.sourceFailures ?? []);
5343
5787
  this.statusReporter.flushProgressStatus(`backfill:${agentName}`);
@@ -5360,7 +5804,7 @@ var AgentSyncEngine = class {
5360
5804
  context: "scan.backfill",
5361
5805
  agentName,
5362
5806
  sessions: fullSessions,
5363
- meta: buildAgentCacheMeta(agent),
5807
+ meta: result.meta,
5364
5808
  completeness: result.completeness,
5365
5809
  removedSessionIds: result.explicitRemovedSessionIds,
5366
5810
  saveCache: true
@@ -5377,6 +5821,7 @@ var AgentSyncEngine = class {
5377
5821
  }
5378
5822
  }
5379
5823
  });
5824
+ this.commitAgentState("scan.backfill", agent, { meta: result.meta });
5380
5825
  try {
5381
5826
  if (completion.completeness === "complete" && !markAgentFullSyncCompleted(agentName)) {
5382
5827
  appLogger.warn("scan.backfill.completion_not_durable", { agent: agentName });
@@ -5402,7 +5847,6 @@ var AgentSyncEngine = class {
5402
5847
  }
5403
5848
  return "committed";
5404
5849
  } catch (error) {
5405
- agent.restoreSessionCacheMeta(meta);
5406
5850
  workerRun?.discard();
5407
5851
  appLogger.error("scan.backfill.error", { agent: agentName, error });
5408
5852
  console.error(`[${agentName}] Backfill failed:`, error);
@@ -5425,7 +5869,7 @@ var AgentSyncEngine = class {
5425
5869
 
5426
5870
  // src/session-watcher.ts
5427
5871
  import { existsSync as existsSync3, readdirSync, statSync, watch } from "fs";
5428
- import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
5872
+ import { basename, dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
5429
5873
  var WRITE_STABILITY_THRESHOLD_MS = 250;
5430
5874
  var WRITE_STABILITY_POLL_MS = 100;
5431
5875
  function toAbsolutePath(path) {
@@ -5531,7 +5975,11 @@ var SessionWatcher = class {
5531
5975
  const targetPath = toAbsolutePath(target.path);
5532
5976
  const scopes = scopesByRoot.get(rootPath) ?? [];
5533
5977
  if (!scopes.some((scope) => scope.agentName === agent.name && scope.targetPath === targetPath)) {
5534
- scopes.push({ agentName: agent.name, targetPath });
5978
+ scopes.push({
5979
+ agentName: agent.name,
5980
+ targetPath,
5981
+ ignoredFileNames: target.ignoredFileNames
5982
+ });
5535
5983
  }
5536
5984
  scopesByRoot.set(rootPath, scopes);
5537
5985
  }
@@ -5650,7 +6098,9 @@ var SessionWatcher = class {
5650
6098
  handleWatchEvent(watchPath, scopes, eventType, filename) {
5651
6099
  const changedPath = resolveWatchEventPath(watchPath, filename);
5652
6100
  const agentNames = new Set(
5653
- scopes.filter((scope) => isRelatedPath(changedPath, scope.targetPath)).map((scope) => scope.agentName)
6101
+ scopes.filter(
6102
+ (scope) => isRelatedPath(changedPath, scope.targetPath) && !(scope.ignoredFileNames?.includes(basename(changedPath)) && changedPath !== scope.targetPath && isSameOrChildPath(scope.targetPath, changedPath))
6103
+ ).map((scope) => scope.agentName)
5654
6104
  );
5655
6105
  if (agentNames.size === 0) {
5656
6106
  return;
@@ -5728,9 +6178,9 @@ var SessionWatcher = class {
5728
6178
  };
5729
6179
 
5730
6180
  // src/worker-runner.ts
5731
- import { Worker as Worker3 } from "worker_threads";
6181
+ import { Worker as Worker4 } from "worker_threads";
5732
6182
  var SHUTDOWN_ERROR_MESSAGE3 = "Scan refresh worker shut down";
5733
- function applySessionChanges2(previousSessions, changes, removedSessionIds) {
6183
+ function applySessionChanges(previousSessions, changes, removedSessionIds) {
5734
6184
  if (changes.length === 0 && removedSessionIds.length === 0) return previousSessions;
5735
6185
  const replacedIds = new Set(removedSessionIds);
5736
6186
  for (const { session } of changes) replacedIds.add(session.reference.sessionId);
@@ -5768,6 +6218,7 @@ var ThreadWorkerRunner = class {
5768
6218
  }
5769
6219
  workerUrl;
5770
6220
  workers = /* @__PURE__ */ new Map();
6221
+ retirements = /* @__PURE__ */ new Set();
5771
6222
  nextRequestId = 1;
5772
6223
  isShuttingDown = false;
5773
6224
  get activeCount() {
@@ -5792,6 +6243,7 @@ var ThreadWorkerRunner = class {
5792
6243
  pricingGenerationId: getPricingGeneration().id,
5793
6244
  operation: payload.operation,
5794
6245
  scanOptions: payload.scanOptions,
6246
+ logContext: appLogger.captureContext(),
5795
6247
  ...existingSlot ? {} : { previousSessions: payload.previousSessions, meta: payload.meta }
5796
6248
  };
5797
6249
  return new Promise((resolve4, reject) => {
@@ -5810,6 +6262,7 @@ var ThreadWorkerRunner = class {
5810
6262
  reject,
5811
6263
  payload,
5812
6264
  generation,
6265
+ logContext: request.logContext ?? {},
5813
6266
  onProgress: payload.onProgress,
5814
6267
  onCheckpoint: payload.onCheckpoint
5815
6268
  });
@@ -5838,10 +6291,11 @@ var ThreadWorkerRunner = class {
5838
6291
  for (const pending of slot.pending.values()) pending.reject(shutdownError);
5839
6292
  slot.pending.clear();
5840
6293
  }
5841
- await Promise.allSettled(slots.map((slot) => slot.worker.terminate()));
6294
+ for (const slot of slots) this.retireWorker(slot.worker);
6295
+ await Promise.allSettled(this.retirements);
5842
6296
  }
5843
6297
  createWorker(agentName, request) {
5844
- const worker = new Worker3(this.workerUrl, { workerData: request });
6298
+ const worker = new Worker4(this.workerUrl, { workerData: request });
5845
6299
  const slot = {
5846
6300
  agentName,
5847
6301
  worker,
@@ -5856,21 +6310,31 @@ var ThreadWorkerRunner = class {
5856
6310
  this.handleMessage(slot, message);
5857
6311
  });
5858
6312
  worker.on("error", (error) => {
5859
- this.closeWorker(agentName, slot, toError2(error));
6313
+ appLogger.restoreContext(this.slotLogContext(slot), () => {
6314
+ this.closeWorker(agentName, slot, toError2(error));
6315
+ void this.retireWorker(worker);
6316
+ });
5860
6317
  });
5861
6318
  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);
6319
+ appLogger.restoreContext(this.slotLogContext(slot), () => {
6320
+ if (slot.closed) return;
6321
+ const error = new Error(`Scan refresh worker exited before completing (code ${code})`);
6322
+ if (slot.pending.size > 0) {
6323
+ appLogger.warn("scan.refresh_worker.exit_before_done", { agent: agentName, code });
6324
+ }
6325
+ this.closeWorker(agentName, slot, error);
6326
+ });
5868
6327
  });
5869
6328
  return slot;
5870
6329
  }
5871
6330
  handleMessage(slot, message) {
5872
6331
  const pending = slot.pending.get(message.requestId);
5873
6332
  if (!pending) return;
6333
+ appLogger.restoreContext(pending.logContext, () => {
6334
+ this.handlePendingMessage(slot, message, pending);
6335
+ });
6336
+ }
6337
+ handlePendingMessage(slot, message, pending) {
5874
6338
  if (message.generation !== pending.generation) {
5875
6339
  const error = new Error(
5876
6340
  `Scan refresh generation mismatch: expected ${pending.generation}, received ${message.generation}`
@@ -5907,7 +6371,7 @@ var ThreadWorkerRunner = class {
5907
6371
  const removedMetaIds = [...message.removedSessionIds, ...message.removedMetaIds];
5908
6372
  try {
5909
6373
  const result = {
5910
- sessions: applySessionChanges2(
6374
+ sessions: applySessionChanges(
5911
6375
  pending.payload.previousSessions,
5912
6376
  message.changes,
5913
6377
  message.removedSessionIds
@@ -5920,7 +6384,8 @@ var ThreadWorkerRunner = class {
5920
6384
  };
5921
6385
  slot.awaitingCommit = {
5922
6386
  requestId: message.requestId,
5923
- generation: message.generation
6387
+ generation: message.generation,
6388
+ logContext: pending.logContext
5924
6389
  };
5925
6390
  pending.resolve(this.createStagedRun(slot, message.requestId, message.generation, result));
5926
6391
  } catch (error) {
@@ -5952,7 +6417,8 @@ var ThreadWorkerRunner = class {
5952
6417
  const request = {
5953
6418
  type: "commit",
5954
6419
  requestId,
5955
- generation
6420
+ generation,
6421
+ logContext: awaiting.logContext
5956
6422
  };
5957
6423
  try {
5958
6424
  slot.worker.postMessage(request);
@@ -5976,19 +6442,30 @@ var ThreadWorkerRunner = class {
5976
6442
  slot.pending.clear();
5977
6443
  slot.awaitingCommit = null;
5978
6444
  }
6445
+ slotLogContext(slot) {
6446
+ for (const pending of slot.pending.values()) return pending.logContext;
6447
+ return slot.awaitingCommit?.logContext ?? {};
6448
+ }
5979
6449
  invalidateWorker(agentName, slot) {
5980
6450
  this.closeWorker(
5981
6451
  agentName,
5982
6452
  slot,
5983
6453
  new Error(`Scan refresh worker state discarded for ${agentName}`)
5984
6454
  );
5985
- void slot.worker.terminate().catch(() => void 0);
6455
+ void this.retireWorker(slot.worker);
6456
+ }
6457
+ retireWorker(worker) {
6458
+ const retirement = terminateWorkerAfterLogDrain(worker);
6459
+ this.retirements.add(retirement);
6460
+ void retirement.finally(() => this.retirements.delete(retirement)).catch(() => void 0);
6461
+ return retirement;
5986
6462
  }
5987
6463
  };
5988
6464
 
5989
6465
  // src/live-scan.ts
5990
6466
  var NEW_SESSION_EVENT_WINDOW_MS = 250;
5991
6467
  var LiveScanStore = class {
6468
+ queryScope;
5992
6469
  watchEnabled;
5993
6470
  scanOptions;
5994
6471
  startupScanOptions;
@@ -6003,6 +6480,10 @@ var LiveScanStore = class {
6003
6480
  constructor(options = {}) {
6004
6481
  this.watchEnabled = options.watchEnabled ?? true;
6005
6482
  this.scanOptions = options.scanOptions ?? {};
6483
+ this.queryScope = {
6484
+ ...this.scanOptions.agents?.length ? { agents: this.scanOptions.agents.map((agent) => agent.toLowerCase()) } : {},
6485
+ ...this.scanOptions.cwd ? { projectScope: createProjectScopeMatcher(this.scanOptions.cwd) } : {}
6486
+ };
6006
6487
  this.startupScanOptions = options.startupScanOptions ?? {};
6007
6488
  this.deferInitialRefresh = options.deferInitialRefresh === true;
6008
6489
  const workerRunner = options.workerRunner ?? new ThreadWorkerRunner(new URL("./scan-refresh-worker.js", import.meta.url));
@@ -6024,8 +6505,9 @@ var LiveScanStore = class {
6024
6505
  startup_to: this.startupScanOptions.to,
6025
6506
  deferred: this.deferInitialRefresh || void 0
6026
6507
  });
6508
+ const { cwd: _cwd, ...scanOptions } = this.scanOptions;
6027
6509
  const initialResult = await scanSessions({
6028
- ...this.scanOptions,
6510
+ ...scanOptions,
6029
6511
  useCache: this.scanOptions.useCache ?? true,
6030
6512
  cacheOnly: this.deferInitialRefresh,
6031
6513
  writeCache: this.deferInitialRefresh ? false : this.scanOptions.writeCache,
@@ -6035,7 +6517,7 @@ var LiveScanStore = class {
6035
6517
  this.syncEngine.initialize(initialResult, {
6036
6518
  cacheTimestamps: initialResult.cacheTimestamps,
6037
6519
  registeredAgents: createRegisteredAgents(),
6038
- allowedAgents: this.getAllowedAgents()
6520
+ queryScope: this.queryScope
6039
6521
  });
6040
6522
  const snapshot = this.getSnapshot();
6041
6523
  const indexStartedAt = performance.now();
@@ -6144,10 +6626,6 @@ var LiveScanStore = class {
6144
6626
  if (workerUrl.protocol === "file:" && !existsSync4(fileURLToPath3(workerUrl))) return null;
6145
6627
  return workerUrl;
6146
6628
  }
6147
- getAllowedAgents() {
6148
- if (!this.scanOptions.agents?.length) return null;
6149
- return new Set(this.scanOptions.agents.map((agent) => agent.toLowerCase()));
6150
- }
6151
6629
  };
6152
6630
 
6153
6631
  // src/output.ts
@@ -6274,7 +6752,8 @@ function startPricingRefresh(timeoutMs = REFRESH_TIMEOUT_MS) {
6274
6752
  }
6275
6753
  );
6276
6754
  return {
6277
- publish() {
6755
+ async ready() {
6756
+ await completion;
6278
6757
  if (publishPendingPricing()) appLogger.info("pricing.refresh.published", {});
6279
6758
  },
6280
6759
  async cancel() {
@@ -6287,8 +6766,23 @@ function startPricingRefresh(timeoutMs = REFRESH_TIMEOUT_MS) {
6287
6766
  // src/index.ts
6288
6767
  process.on("unhandledRejection", (reason) => {
6289
6768
  appLogger.error("process.unhandled_rejection", { error: reason });
6769
+ appLogger.flushSync();
6290
6770
  throw reason;
6291
6771
  });
6772
+ process.on("uncaughtExceptionMonitor", (error, origin) => {
6773
+ appLogger.error("process.uncaught_exception", { error, exception_origin: origin });
6774
+ appLogger.flushSync();
6775
+ });
6776
+ var closeApplicationLog = (exitCode) => closeLoggerBeforeTermination(() => appLogger.close(), exitCode);
6777
+ var shutdownOnSignal = (signal) => {
6778
+ appLogger.info("cli.shutdown", { signal, phase: "startup" });
6779
+ appLogger.flushSync();
6780
+ process.exit(0);
6781
+ };
6782
+ var dispatchSignal = (signal) => shutdownOnSignal(signal);
6783
+ process.once("SIGINT", dispatchSignal);
6784
+ process.once("SIGTERM", dispatchSignal);
6785
+ process.once("SIGHUP", dispatchSignal);
6292
6786
  var main = defineCommand({
6293
6787
  meta: {
6294
6788
  name: "codesesh",
@@ -6337,7 +6831,7 @@ var main = defineCommand({
6337
6831
  days: {
6338
6832
  type: "string",
6339
6833
  alias: "d",
6340
- description: "Only include sessions active in the last N days (0 = all time)",
6834
+ description: "Only include sessions active in the last N local calendar days (0 = all time)",
6341
6835
  default: "7"
6342
6836
  },
6343
6837
  cwd: {
@@ -6346,11 +6840,11 @@ var main = defineCommand({
6346
6840
  },
6347
6841
  from: {
6348
6842
  type: "string",
6349
- description: "Sessions active after this date, YYYY-MM-DD (overrides --days)"
6843
+ description: "Sessions active on or after this date, YYYY-MM-DD (overrides --days)"
6350
6844
  },
6351
6845
  to: {
6352
6846
  type: "string",
6353
- description: "Sessions active before this date (YYYY-MM-DD)"
6847
+ description: "Sessions active on or before this date (YYYY-MM-DD)"
6354
6848
  },
6355
6849
  session: {
6356
6850
  type: "string",
@@ -6433,7 +6927,7 @@ var main = defineCommand({
6433
6927
  log_path: appLogger.getLogPath()
6434
6928
  });
6435
6929
  if (clearCache) {
6436
- const { clearCache: clear } = await import("./discovery-K3GLISI6.js");
6930
+ const { clearCache: clear } = await import("./discovery-ADSQARBV.js");
6437
6931
  clear();
6438
6932
  appLogger.info("cache.clear");
6439
6933
  console.log("Cache cleared.");
@@ -6444,6 +6938,7 @@ var main = defineCommand({
6444
6938
  targetSession = parseSessionUri(args.session);
6445
6939
  if (!targetSession) {
6446
6940
  console.error(`Invalid session format: ${args.session}. Expected: agent://session-id`);
6941
+ await closeApplicationLog(1);
6447
6942
  process.exit(1);
6448
6943
  }
6449
6944
  }
@@ -6461,6 +6956,7 @@ var main = defineCommand({
6461
6956
  { currentWorkingDirectory: process.cwd() }
6462
6957
  );
6463
6958
  const { from: listDefaultFrom, to: listDefaultTo, days: listDefaultDays } = listWindow;
6959
+ await pricingRefresh.ready();
6464
6960
  const store = new LiveScanStore({
6465
6961
  watchEnabled: !jsonOnly,
6466
6962
  scanOptions,
@@ -6500,6 +6996,7 @@ var main = defineCommand({
6500
6996
  console.log(JSON.stringify(output, null, 2));
6501
6997
  }
6502
6998
  await store.shutdown();
6999
+ await closeApplicationLog(process.exitCode ?? 0);
6503
7000
  return;
6504
7001
  }
6505
7002
  const agents = createRegisteredAgents();
@@ -6518,11 +7015,11 @@ var main = defineCommand({
6518
7015
  });
6519
7016
  } catch (error) {
6520
7017
  console.error(getServerStartupErrorMessage(error, port));
7018
+ await closeApplicationLog(1);
6521
7019
  process.exit(1);
6522
7020
  }
6523
7021
  const { url } = app;
6524
7022
  if (!jsonOnly) {
6525
- pricingRefresh.publish();
6526
7023
  store.startBackgroundRefresh();
6527
7024
  }
6528
7025
  let shuttingDown = false;
@@ -6532,16 +7029,16 @@ var main = defineCommand({
6532
7029
  appLogger.info("cli.shutdown", { signal });
6533
7030
  await pricingRefresh.cancel();
6534
7031
  await app.shutdown();
7032
+ await closeApplicationLog(0);
6535
7033
  process.exit(0);
6536
7034
  };
6537
- const shutdownOnSignal = (signal) => {
6538
- shutdown(signal).catch((error) => {
7035
+ shutdownOnSignal = (signal) => {
7036
+ shutdown(signal).catch(async (error) => {
6539
7037
  appLogger.error("cli.shutdown_failed", { signal, error });
7038
+ await closeApplicationLog(1);
6540
7039
  process.exit(1);
6541
7040
  });
6542
7041
  };
6543
- process.once("SIGINT", shutdownOnSignal);
6544
- process.once("SIGTERM", shutdownOnSignal);
6545
7042
  console.log(` ${url}`);
6546
7043
  console.log("");
6547
7044
  appLogger.info("cli.ready", {
@@ -6557,6 +7054,17 @@ var main = defineCommand({
6557
7054
  }
6558
7055
  }
6559
7056
  });
7057
+ var runCliCommand = main.run;
7058
+ if (!runCliCommand) throw new Error("CLI command is missing its runner");
7059
+ main.run = async (context) => {
7060
+ try {
7061
+ return await runCliCommand(context);
7062
+ } catch (error) {
7063
+ appLogger.error("cli.fatal", { error });
7064
+ await closeApplicationLog(1);
7065
+ throw error;
7066
+ }
7067
+ };
6560
7068
  if (process.argv.slice(2).includes("-v")) {
6561
7069
  console.log(VERSION);
6562
7070
  process.exit(0);