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.
- package/README.md +5 -4
- package/dist/{chunk-W4PZHL2C.js → chunk-4ND6AT6W.js} +154 -92
- package/dist/chunk-4ND6AT6W.js.map +1 -0
- package/dist/chunk-4QFNZCBR.js +1271 -0
- package/dist/chunk-4QFNZCBR.js.map +1 -0
- package/dist/{chunk-MBZ4VV4C.js → chunk-A3MT6L6Z.js} +3 -3
- package/dist/chunk-COZSWVCH.js +89 -0
- package/dist/chunk-COZSWVCH.js.map +1 -0
- package/dist/{chunk-YCUN52HA.js → chunk-L3DDWTYV.js} +81 -115
- package/dist/chunk-L3DDWTYV.js.map +1 -0
- package/dist/{chunk-BYB77UAJ.js → chunk-XQDYKKRC.js} +139 -80
- package/dist/chunk-XQDYKKRC.js.map +1 -0
- package/dist/{chunk-3SXCTDD6.js → chunk-YZ6XCSVQ.js} +4 -3
- package/dist/{discovery-K3GLISI6.js → discovery-ADSQARBV.js} +11 -5
- package/dist/index.js +1538 -1030
- package/dist/index.js.map +1 -1
- package/dist/project-identity-worker.js +4 -4
- package/dist/scan-refresh-worker.js +17 -7
- package/dist/scan-refresh-worker.js.map +1 -1
- package/dist/search-index-worker.js +16 -8
- package/dist/search-index-worker.js.map +1 -1
- package/dist/session-detail-worker.js +79 -0
- package/dist/session-detail-worker.js.map +1 -0
- package/dist/smart-tag-worker.js +3 -3
- package/dist/web/assets/CustomTimeWindowDialog-D2bx7uDv.js +1 -0
- package/dist/web/assets/InteractiveReceipt-BFpIdno8.js +1 -0
- package/dist/web/assets/OverviewScreen-D_KXfiJi.js +1 -0
- package/dist/web/assets/{PrismHighlighter-B1a2dPJO.js → PrismHighlighter-BgVkYu_S.js} +1 -1
- package/dist/web/assets/Projects-CN44UpuR.js +1 -0
- package/dist/web/assets/SearchResultsPanel-CWD01v-a.js +1 -0
- package/dist/web/assets/SessionDetail-D8taQ6Tl.js +53 -0
- package/dist/web/assets/api-B9lJtVbO.js +1 -0
- package/dist/web/assets/index-XpolWMY4.js +1423 -0
- package/dist/web/assets/index-xbEEMszv.css +1 -0
- package/dist/web/assets/session-indexes-fib48ZKn.js +4 -0
- package/dist/web/assets/translate-Dmym1ZDl.js +2 -0
- package/dist/web/assets/utils-Bk66u8b3.js +1 -0
- package/dist/web/index.html +7 -7
- package/package.json +3 -3
- package/dist/chunk-BYB77UAJ.js.map +0 -1
- package/dist/chunk-G4HRD3AR.js +0 -246
- package/dist/chunk-G4HRD3AR.js.map +0 -1
- package/dist/chunk-W4PZHL2C.js.map +0 -1
- package/dist/chunk-YCUN52HA.js.map +0 -1
- package/dist/web/assets/InteractiveReceipt-Dk50TBsH.js +0 -1
- package/dist/web/assets/OverviewScreen-5hvQVp4H.js +0 -1
- package/dist/web/assets/Projects-Cy_v2yep.js +0 -1
- package/dist/web/assets/SearchResultsPanel-DfheEwpz.js +0 -1
- package/dist/web/assets/SessionDetail-DRI8R3st.js +0 -53
- package/dist/web/assets/api-GP9UCedC.js +0 -1
- package/dist/web/assets/contract-xJyW5u7a.js +0 -2
- package/dist/web/assets/index-Bd-YkWkZ.css +0 -1
- package/dist/web/assets/index-DEjvzumm.js +0 -1410
- package/dist/web/assets/panel-ND0J63d3.js +0 -1
- package/dist/web/assets/session-indexes-BsgjUoc1.js +0 -4
- package/dist/web/assets/utils-C7BqyxxV.js +0 -1
- /package/dist/{chunk-MBZ4VV4C.js.map → chunk-A3MT6L6Z.js.map} +0 -0
- /package/dist/{chunk-3SXCTDD6.js.map → chunk-YZ6XCSVQ.js.map} +0 -0
- /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-
|
|
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-
|
|
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-
|
|
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-
|
|
60
|
+
} from "./chunk-YZ6XCSVQ.js";
|
|
56
61
|
import {
|
|
57
62
|
createRegisteredAgents,
|
|
58
63
|
getAgentInfoMap
|
|
59
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
693
|
-
|
|
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" ||
|
|
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
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
|
782
|
-
const
|
|
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
|
-
["
|
|
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
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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/
|
|
833
|
-
|
|
834
|
-
|
|
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
|
|
837
|
-
|
|
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
|
|
846
|
-
|
|
847
|
-
const
|
|
848
|
-
if (
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
867
|
-
|
|
885
|
+
from: addCalendarDays(startOfCalendarDay(to), -(DEFAULT_DASHBOARD_DAYS - 1)),
|
|
886
|
+
to,
|
|
887
|
+
days: DEFAULT_DASHBOARD_DAYS
|
|
868
888
|
};
|
|
869
889
|
}
|
|
870
|
-
function
|
|
871
|
-
if (!
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
//
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
-
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
|
964
|
-
|
|
965
|
-
const
|
|
966
|
-
const
|
|
967
|
-
const
|
|
968
|
-
(
|
|
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 (
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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/
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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 (
|
|
1487
|
-
return
|
|
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
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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
|
-
|
|
1524
|
-
|
|
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
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
|
1537
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1561
|
-
const
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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, "
|
|
1784
|
+
const window = parseDateWindowRequest(c, "file-activity", defaults);
|
|
1576
1785
|
if (window.kind === "rejected") return window.response;
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
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
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
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
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
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
|
-
|
|
1685
|
-
|
|
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
|
-
|
|
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 =
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
1895
|
-
|
|
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
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
);
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
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({
|
|
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,
|
|
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
|
|
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
|
-
|
|
2324
|
-
|
|
2566
|
+
requestSignal.removeEventListener("abort", disconnectStream);
|
|
2567
|
+
shutdownSignal?.removeEventListener("abort", shutdownStream);
|
|
2568
|
+
closeConnection(reason);
|
|
2325
2569
|
return true;
|
|
2326
2570
|
};
|
|
2327
|
-
|
|
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 (
|
|
2348
|
-
else
|
|
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
|
|
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(
|
|
2690
|
+
return createSseResponse(
|
|
2691
|
+
eventSource,
|
|
2692
|
+
c.req.raw.signal,
|
|
2693
|
+
options.shutdownSignal,
|
|
2694
|
+
closeConnection
|
|
2695
|
+
);
|
|
2419
2696
|
} catch (error) {
|
|
2420
|
-
|
|
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
|
-
|
|
3134
|
+
route: "<rejected>",
|
|
2782
3135
|
reason: decision.reason,
|
|
2783
|
-
|
|
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
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
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
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3480
|
+
return {
|
|
3481
|
+
agentName,
|
|
3482
|
+
kind,
|
|
3145
3483
|
generation,
|
|
3146
|
-
|
|
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) => !
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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.
|
|
3616
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
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
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
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:
|
|
3716
|
-
|
|
4095
|
+
context: activeBatch.context,
|
|
4096
|
+
error: toError2(error)
|
|
3717
4097
|
});
|
|
3718
|
-
}
|
|
3719
|
-
|
|
3720
|
-
|
|
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
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
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
|
-
|
|
4131
|
+
}
|
|
4132
|
+
appLogger.info(`${message.context}.done`, {
|
|
4133
|
+
duration_ms: Math.round(message.durationMs),
|
|
4134
|
+
sessions: message.sessions
|
|
3731
4135
|
});
|
|
3732
|
-
|
|
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
|
|
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
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
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
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
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:
|
|
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 = {
|
|
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
|
-
|
|
5207
|
+
commitAgentState(operation, agent, state) {
|
|
5208
|
+
if (!state) return;
|
|
4785
5209
|
try {
|
|
4786
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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 ??
|
|
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:
|
|
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({
|
|
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(
|
|
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
|
|
6181
|
+
import { Worker as Worker4 } from "worker_threads";
|
|
5732
6182
|
var SHUTDOWN_ERROR_MESSAGE3 = "Scan refresh worker shut down";
|
|
5733
|
-
function
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
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:
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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);
|