codesesh 0.19.0 → 1.0.1
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 +29 -5
- package/dist/{chunk-G2BTNW3C.js → chunk-JHX5OLL4.js} +10175 -7365
- package/dist/chunk-JHX5OLL4.js.map +1 -0
- package/dist/{chunk-KD3DGWZY.js → chunk-OFTTM3UD.js} +80 -16
- package/dist/chunk-OFTTM3UD.js.map +1 -0
- package/dist/{dist-ALXOBKV2.js → dist-N2PQATP2.js} +42 -4
- package/dist/index.js +1862 -457
- package/dist/index.js.map +1 -1
- package/dist/scan-refresh-worker.js +274 -95
- package/dist/scan-refresh-worker.js.map +1 -1
- package/dist/search-index-worker.js +112 -52
- package/dist/search-index-worker.js.map +1 -1
- package/dist/smart-tag-worker.js +5 -3
- package/dist/smart-tag-worker.js.map +1 -1
- package/dist/web/assets/ErrorBoundary-D11tWzfn.js +1 -0
- package/dist/web/assets/InteractiveReceipt-DYGyHeZd.js +1 -0
- package/dist/web/assets/OverviewScreen-DriPmCam.js +1 -0
- package/dist/web/assets/PrismHighlighter-BZkkX0AU.js +6 -0
- package/dist/web/assets/Projects-BPDIcUBK.js +1 -0
- package/dist/web/assets/SearchFilterBar-Bx73B5e9.js +1 -0
- package/dist/web/assets/SearchResultsPanel-A6_3TPhW.js +1 -0
- package/dist/web/assets/SessionDetail-lQz99ARH.js +54 -0
- package/dist/web/assets/{session-title-CNhapdPJ.js → SmartTagChips-ux6idUGJ.js} +1 -1
- package/dist/web/assets/agents-BkRP_ww3.js +1 -0
- package/dist/web/assets/code-metrics-UYFsCC7E.js +1 -0
- package/dist/web/assets/contract-V6QuD0UY.js +1 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
- package/dist/web/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
- package/dist/web/assets/index-B2RqUgZ5.css +1 -0
- package/dist/web/assets/index-BgWoUD_4.js +1410 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/web/assets/jsx-runtime-B-hcVAMW.js +1 -0
- package/dist/web/assets/panel-C-gc79Gz.js +1 -0
- package/dist/web/assets/session-indexes-0oT5HSS5.js +4 -0
- package/dist/web/assets/session-title-DdtlxtlR.js +1 -0
- package/dist/web/assets/space-grotesk-latin-ext-wght-normal-D9tNdqV9.woff2 +0 -0
- package/dist/web/assets/space-grotesk-latin-wght-normal-BhU9QXUp.woff2 +0 -0
- package/dist/web/assets/space-grotesk-vietnamese-wght-normal-D0rl6rjA.woff2 +0 -0
- package/dist/web/assets/utils-DgUgu15E.js +1 -0
- package/dist/web/icon/agent/grok.svg +1 -0
- package/dist/web/index.html +45 -54
- package/dist/web/theme-bootstrap.js +19 -0
- package/package.json +16 -16
- package/dist/chunk-G2BTNW3C.js.map +0 -1
- package/dist/chunk-KD3DGWZY.js.map +0 -1
- package/dist/web/assets/AgentIcon-CEj7HO1m.js +0 -1
- package/dist/web/assets/BookmarkButton-DLEnIyeE.js +0 -1
- package/dist/web/assets/Dashboard-DUG0IFcy.js +0 -39
- package/dist/web/assets/DialogTitle-Dv6DQPIb.js +0 -1
- package/dist/web/assets/InteractiveReceipt-xnRqJvfd.js +0 -1
- package/dist/web/assets/PrismHighlighter-C7OfXHq2.js +0 -6
- package/dist/web/assets/Projects-BMaZc6J5.js +0 -1
- package/dist/web/assets/SearchFilterBar-iP6pzlZo.js +0 -1
- package/dist/web/assets/SearchResultsPanel-RuiLPkbP.js +0 -1
- package/dist/web/assets/SessionDetail-De1Wl3mu.js +0 -54
- package/dist/web/assets/index-B-CV7pFu.css +0 -2
- package/dist/web/assets/index-CQI8F_8X.js +0 -1930
- package/dist/web/assets/index.dom-CCLJ9_FR.js +0 -1
- package/dist/web/assets/jsx-runtime-n5LQ9ujS.js +0 -1
- package/dist/web/assets/session-indexes-BuxBafvh.js +0 -4
- package/dist/web/assets/useTheme-CwDcVdGz.js +0 -1
- package/dist/web/assets/utils-B6KiDbIe.js +0 -1
- /package/dist/{dist-ALXOBKV2.js.map → dist-N2PQATP2.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
import {
|
|
3
3
|
appLogger,
|
|
4
4
|
logSearchIndexSync
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OFTTM3UD.js";
|
|
6
6
|
import {
|
|
7
7
|
FileSystemSessionSource,
|
|
8
8
|
SessionAliasValidationError,
|
|
9
9
|
StateStorageUnavailableError,
|
|
10
10
|
addCalendarDays,
|
|
11
11
|
attachMissingProjectIdentities,
|
|
12
|
-
|
|
12
|
+
attachProjectMetricsFromTree,
|
|
13
13
|
buildAgentCacheMeta,
|
|
14
14
|
buildDashboard,
|
|
15
|
+
buildSessionTree,
|
|
16
|
+
closeCacheStorage,
|
|
15
17
|
computeSessionDiff,
|
|
16
18
|
countCalendarDays,
|
|
17
19
|
createProjectScopeMatcher,
|
|
18
20
|
createRegisteredAgents,
|
|
21
|
+
createSessionIndex,
|
|
22
|
+
createSessionProjectionContext,
|
|
19
23
|
deleteBookmark,
|
|
20
24
|
deleteSessionAlias,
|
|
21
25
|
executeSessionSearch,
|
|
@@ -24,23 +28,25 @@ import {
|
|
|
24
28
|
formatSessionReference,
|
|
25
29
|
getAgentFullSyncCursor,
|
|
26
30
|
getAgentInfoMap,
|
|
27
|
-
|
|
31
|
+
getPricingGeneration,
|
|
28
32
|
getSessionActivityTime,
|
|
29
33
|
getSessionAgentKey,
|
|
34
|
+
getSessionRouteKey,
|
|
30
35
|
importBookmarks,
|
|
31
|
-
isAgentCacheInitialized,
|
|
32
36
|
isProjectIdentityKind,
|
|
33
37
|
listBookmarks,
|
|
34
38
|
listCachedProjectGroups,
|
|
35
39
|
listFileActivity,
|
|
40
|
+
listModelCostDistribution,
|
|
36
41
|
listSessionAliases,
|
|
42
|
+
loadCachedSessionHeads,
|
|
37
43
|
loadCachedSessions,
|
|
38
|
-
markAgentCacheInitialized,
|
|
39
44
|
markAgentFullSyncCompleted,
|
|
40
45
|
markAgentFullSyncProgress,
|
|
41
46
|
markAgentFullSyncStarted,
|
|
42
47
|
matchesProjectIdentity,
|
|
43
48
|
matchesProjectScope,
|
|
49
|
+
materializeBookmarkViews,
|
|
44
50
|
materializeSessionDetailResponse,
|
|
45
51
|
mergeSearchQueryOptions,
|
|
46
52
|
mergeSessionsUpdatedEvents,
|
|
@@ -48,9 +54,10 @@ import {
|
|
|
48
54
|
normalizeSessionReference,
|
|
49
55
|
perf,
|
|
50
56
|
publishPendingPricing,
|
|
57
|
+
readAgentCacheInitialization,
|
|
58
|
+
readAgentLastFullSyncAt,
|
|
59
|
+
readPendingSearchIndexMaintenance,
|
|
51
60
|
refreshPricingCache,
|
|
52
|
-
saveCachedSessionChanges,
|
|
53
|
-
saveCachedSessions,
|
|
54
61
|
scanSessions,
|
|
55
62
|
sessionRoutePath,
|
|
56
63
|
sessionSignature,
|
|
@@ -58,7 +65,7 @@ import {
|
|
|
58
65
|
startOfCalendarDay,
|
|
59
66
|
upsertBookmark,
|
|
60
67
|
upsertSessionAlias
|
|
61
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-JHX5OLL4.js";
|
|
62
69
|
|
|
63
70
|
// src/index.ts
|
|
64
71
|
import { defineCommand, runMain } from "citty";
|
|
@@ -67,6 +74,7 @@ import { defineCommand, runMain } from "citty";
|
|
|
67
74
|
import { Hono as Hono2 } from "hono";
|
|
68
75
|
import { bodyLimit } from "hono/body-limit";
|
|
69
76
|
import { compress } from "hono/compress";
|
|
77
|
+
import { secureHeaders } from "hono/secure-headers";
|
|
70
78
|
import { serve } from "@hono/node-server";
|
|
71
79
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
72
80
|
import { existsSync } from "fs";
|
|
@@ -149,8 +157,9 @@ var SMART_TAGS = [
|
|
|
149
157
|
"exploration",
|
|
150
158
|
"planning"
|
|
151
159
|
];
|
|
152
|
-
var
|
|
153
|
-
var
|
|
160
|
+
var SEARCH_LIMIT_POLICY = { defaultValue: 50, maxValue: 100 };
|
|
161
|
+
var FILE_ACTIVITY_LIMIT_POLICY = { defaultValue: 50, maxValue: 200 };
|
|
162
|
+
var SESSION_PAGE_LIMIT_POLICY = { defaultValue: 250, maxValue: 500 };
|
|
154
163
|
function searchParams(c) {
|
|
155
164
|
return new URL(c.req.url ?? "http://localhost/", "http://localhost/").searchParams;
|
|
156
165
|
}
|
|
@@ -164,15 +173,51 @@ function optionalQueryValue(value) {
|
|
|
164
173
|
return normalized ? normalized : void 0;
|
|
165
174
|
}
|
|
166
175
|
function parseDateParam(value, fallback) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
176
|
+
const normalized = optionalQueryValue(value);
|
|
177
|
+
if (normalized == null) return { kind: "default", value: fallback };
|
|
178
|
+
const timestamp = new Date(normalized).getTime();
|
|
179
|
+
return Number.isNaN(timestamp) ? { kind: "invalid", error: "must be a valid date" } : { kind: "valid", value: timestamp };
|
|
180
|
+
}
|
|
181
|
+
function parseDateWindow(params, defaults) {
|
|
182
|
+
const from = parseDateParam(params.get("from") ?? void 0, defaults.from);
|
|
183
|
+
if (from.kind === "invalid") return { ...from, parameter: "from" };
|
|
184
|
+
const to = parseDateParam(params.get("to") ?? void 0, defaults.to);
|
|
185
|
+
if (to.kind === "invalid") return { ...to, parameter: "to" };
|
|
186
|
+
if (from.value != null && to.value != null && from.value > to.value) {
|
|
187
|
+
return { kind: "invalid", parameter: "from", error: "must not be after to" };
|
|
188
|
+
}
|
|
189
|
+
return { kind: "valid", from: from.value, to: to.value };
|
|
170
190
|
}
|
|
171
191
|
function parseNumberParam(value) {
|
|
172
192
|
if (value == null || !value.trim()) return void 0;
|
|
173
193
|
const number = Number(value);
|
|
174
194
|
return Number.isFinite(number) ? number : void 0;
|
|
175
195
|
}
|
|
196
|
+
function parseAgentFilter(value, knownAgentNames) {
|
|
197
|
+
if (value === null) return { kind: "all" };
|
|
198
|
+
const normalized = value.trim().toLowerCase();
|
|
199
|
+
if (!normalized) return { kind: "unknown" };
|
|
200
|
+
for (const agentName of knownAgentNames) {
|
|
201
|
+
if (agentName.toLowerCase() === normalized) return { kind: "known", agentName };
|
|
202
|
+
}
|
|
203
|
+
return { kind: "unknown" };
|
|
204
|
+
}
|
|
205
|
+
function parseLimit(value, policy) {
|
|
206
|
+
if (value === null) return { kind: "default", value: policy.defaultValue };
|
|
207
|
+
const normalized = value.trim();
|
|
208
|
+
if (!/^\d+$/.test(normalized)) {
|
|
209
|
+
return { kind: "invalid", error: "limit must be a positive integer" };
|
|
210
|
+
}
|
|
211
|
+
const digits = normalized.replace(/^0+/, "");
|
|
212
|
+
if (!digits) return { kind: "invalid", error: "limit must be a positive integer" };
|
|
213
|
+
const maxDigits = String(policy.maxValue);
|
|
214
|
+
const exceedsMaximum = digits.length > maxDigits.length || digits.length === maxDigits.length && digits > maxDigits;
|
|
215
|
+
return { kind: "valid", value: exceedsMaximum ? policy.maxValue : Number(digits) };
|
|
216
|
+
}
|
|
217
|
+
function parseSessionQuery(params, knownAgentNames, limitPolicy) {
|
|
218
|
+
const agent = parseAgentFilter(params.get("agent"), knownAgentNames);
|
|
219
|
+
return limitPolicy ? { agent, limit: parseLimit(params.get("limit"), limitPolicy) } : { agent };
|
|
220
|
+
}
|
|
176
221
|
function parseSmartTags(values) {
|
|
177
222
|
const tags = values.map((value) => value.toLowerCase()).filter((value) => SMART_TAGS.includes(value));
|
|
178
223
|
return tags.length > 0 ? [...new Set(tags)] : void 0;
|
|
@@ -190,11 +235,10 @@ function parseProjectIdentityFilter(kindValue, keyValue) {
|
|
|
190
235
|
if (!kind || !key || !isProjectIdentityKind(kind)) return null;
|
|
191
236
|
return { kind, key };
|
|
192
237
|
}
|
|
193
|
-
function parseSearchOptions(c,
|
|
238
|
+
function parseSearchOptions(c, window, request, projectIdentity) {
|
|
194
239
|
const params = searchParams(c);
|
|
195
|
-
const limitValue = parseNumberParam(params.get("limit") ?? void 0);
|
|
196
240
|
return {
|
|
197
|
-
agent:
|
|
241
|
+
agent: request.agent,
|
|
198
242
|
project: optionalQueryValue(params.get("project") ?? void 0),
|
|
199
243
|
projectKind: projectIdentity?.kind,
|
|
200
244
|
projectKey: projectIdentity?.key,
|
|
@@ -207,15 +251,72 @@ function parseSearchOptions(c, defaults, projectIdentity) {
|
|
|
207
251
|
),
|
|
208
252
|
costMin: parseNumberParam(params.get("costMin") ?? void 0),
|
|
209
253
|
costMax: parseNumberParam(params.get("costMax") ?? void 0),
|
|
210
|
-
from:
|
|
211
|
-
to:
|
|
212
|
-
limit:
|
|
254
|
+
from: window.from,
|
|
255
|
+
to: window.to,
|
|
256
|
+
limit: request.limit
|
|
213
257
|
};
|
|
214
258
|
}
|
|
215
259
|
function filterSessionsByActivityWindow(sessions, from, to) {
|
|
216
260
|
return filterSessionTreeByActivityWindow(sessions, from, to);
|
|
217
261
|
}
|
|
218
262
|
|
|
263
|
+
// src/api/session-pagination.ts
|
|
264
|
+
import { createHash, randomUUID } from "crypto";
|
|
265
|
+
var identityVersions = /* @__PURE__ */ new WeakMap();
|
|
266
|
+
function identityVersion(identity) {
|
|
267
|
+
const existing = identityVersions.get(identity);
|
|
268
|
+
if (existing) return existing;
|
|
269
|
+
const version = randomUUID();
|
|
270
|
+
identityVersions.set(identity, version);
|
|
271
|
+
return version;
|
|
272
|
+
}
|
|
273
|
+
function queryFingerprint(params) {
|
|
274
|
+
const canonical = new URLSearchParams(params);
|
|
275
|
+
canonical.delete("cursor");
|
|
276
|
+
canonical.delete("limit");
|
|
277
|
+
canonical.sort();
|
|
278
|
+
return createHash("sha256").update(canonical.toString()).digest("base64url");
|
|
279
|
+
}
|
|
280
|
+
function encodeCursor(payload) {
|
|
281
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
282
|
+
}
|
|
283
|
+
function decodeCursor(cursor) {
|
|
284
|
+
if (!cursor || cursor.length > 512 || !/^[A-Za-z0-9_-]+$/.test(cursor)) return null;
|
|
285
|
+
try {
|
|
286
|
+
const value = JSON.parse(
|
|
287
|
+
Buffer.from(cursor, "base64url").toString("utf8")
|
|
288
|
+
);
|
|
289
|
+
if (value.version !== 1 || typeof value.snapshot !== "string" || typeof value.view !== "string" || typeof value.query !== "string" || !Number.isSafeInteger(value.offset) || value.offset <= 0) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return value;
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function paginateSessionSnapshot(items, request) {
|
|
298
|
+
const snapshot = identityVersion(request.snapshotIdentity);
|
|
299
|
+
const view = identityVersion(request.viewIdentity);
|
|
300
|
+
const query = queryFingerprint(request.query);
|
|
301
|
+
let offset = 0;
|
|
302
|
+
if (request.cursor) {
|
|
303
|
+
const cursor = decodeCursor(request.cursor);
|
|
304
|
+
if (!cursor || cursor.query !== query) return { kind: "invalid_cursor" };
|
|
305
|
+
if (cursor.snapshot !== snapshot || cursor.view !== view) {
|
|
306
|
+
return { kind: "stale_snapshot" };
|
|
307
|
+
}
|
|
308
|
+
if (cursor.offset >= items.length) return { kind: "invalid_cursor" };
|
|
309
|
+
offset = cursor.offset;
|
|
310
|
+
}
|
|
311
|
+
const end = Math.min(offset + request.limit, items.length);
|
|
312
|
+
const nextCursor = end < items.length ? encodeCursor({ version: 1, snapshot, view, query, offset: end }) : void 0;
|
|
313
|
+
return {
|
|
314
|
+
kind: "page",
|
|
315
|
+
items: items.slice(offset, end),
|
|
316
|
+
...nextCursor ? { nextCursor } : {}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
219
320
|
// src/api/session-aliases-view.ts
|
|
220
321
|
function aliasKey(reference) {
|
|
221
322
|
return `${reference.agentName.toLowerCase()}\0${reference.sessionId}`;
|
|
@@ -252,10 +353,14 @@ function invalidateAliasView() {
|
|
|
252
353
|
cachedView = null;
|
|
253
354
|
}
|
|
254
355
|
function decorateBookmark(bookmark, aliases) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
356
|
+
if (bookmark.availability === "available") {
|
|
357
|
+
return {
|
|
358
|
+
...bookmark,
|
|
359
|
+
session: aliases.decorate(bookmark.session, bookmark.reference)
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const displayTitle = aliases.get(bookmark.reference);
|
|
363
|
+
return displayTitle ? { ...bookmark, display_title: displayTitle } : bookmark;
|
|
259
364
|
}
|
|
260
365
|
function decorateFileActivity(activity, aliases) {
|
|
261
366
|
return {
|
|
@@ -286,15 +391,34 @@ function findAliasSearchResults(query, options, scanResult, aliases) {
|
|
|
286
391
|
reference: alias.reference,
|
|
287
392
|
session: aliases.decorate(session, alias.reference),
|
|
288
393
|
snippet: `Alias \xB7 ${session.directory}`,
|
|
394
|
+
snippetHighlights: [],
|
|
289
395
|
matchType: "title"
|
|
290
396
|
});
|
|
291
397
|
}
|
|
292
|
-
return filterSessionSearchCandidates(results, search.options).sort(
|
|
398
|
+
return filterSessionSearchCandidates(results, search.options, scanResult.sessions).sort(
|
|
293
399
|
(a, b) => getSessionActivityTime(b.session) - getSessionActivityTime(a.session)
|
|
294
400
|
);
|
|
295
401
|
}
|
|
296
402
|
|
|
297
403
|
// src/api/handlers.ts
|
|
404
|
+
var KNOWN_AGENT_NAMES = getAgentInfoMap({}).map((agent) => agent.name);
|
|
405
|
+
var KNOWN_AGENT_NAME_SET = new Set(KNOWN_AGENT_NAMES);
|
|
406
|
+
function reportInvalidQueryParameter(endpoint, parameter, validationOutcome) {
|
|
407
|
+
appLogger.warn("api.query_parameter.invalid", {
|
|
408
|
+
endpoint,
|
|
409
|
+
parameter,
|
|
410
|
+
validation_outcome: validationOutcome
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function parseDateWindowRequest(c, endpoint, defaults) {
|
|
414
|
+
const outcome = parseDateWindow(searchParams(c), defaults);
|
|
415
|
+
if (outcome.kind === "valid") return outcome;
|
|
416
|
+
reportInvalidQueryParameter(endpoint, outcome.parameter, "rejected");
|
|
417
|
+
return {
|
|
418
|
+
kind: "rejected",
|
|
419
|
+
response: c.json({ error: `${outcome.parameter} ${outcome.error}` }, 400)
|
|
420
|
+
};
|
|
421
|
+
}
|
|
298
422
|
var SNAPSHOT_AGGREGATION_CACHE_LIMIT = 64;
|
|
299
423
|
var snapshotAggregationCaches = /* @__PURE__ */ new WeakMap();
|
|
300
424
|
function getSnapshotAggregation(source, sessions, key, build) {
|
|
@@ -326,25 +450,6 @@ function withStorageErrors(handler, onUnavailable) {
|
|
|
326
450
|
function isRecord(value) {
|
|
327
451
|
return typeof value === "object" && value !== null;
|
|
328
452
|
}
|
|
329
|
-
function isSessionStats(value) {
|
|
330
|
-
if (!isRecord(value)) return false;
|
|
331
|
-
return typeof value.message_count === "number" && typeof value.total_input_tokens === "number" && typeof value.total_output_tokens === "number" && typeof value.total_cost === "number" && (value.total_tokens == null || typeof value.total_tokens === "number");
|
|
332
|
-
}
|
|
333
|
-
function parseBookmarkSession(value, reference) {
|
|
334
|
-
if (!isRecord(value)) return null;
|
|
335
|
-
if (value.id !== reference.sessionId || typeof value.slug !== "string" || typeof value.title !== "string" || typeof value.directory !== "string" || typeof value.time_created !== "number" || value.time_updated != null && typeof value.time_updated !== "number" || !isSessionStats(value.stats)) {
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
return {
|
|
339
|
-
id: reference.sessionId,
|
|
340
|
-
slug: formatSessionReference(reference),
|
|
341
|
-
title: value.title,
|
|
342
|
-
directory: value.directory,
|
|
343
|
-
time_created: value.time_created,
|
|
344
|
-
time_updated: value.time_updated ?? void 0,
|
|
345
|
-
stats: value.stats
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
453
|
function parseSessionReferencePayload(value) {
|
|
349
454
|
if (!isRecord(value) || typeof value.agentName !== "string" || typeof value.sessionId !== "string" || !value.agentName.trim() || !value.sessionId) {
|
|
350
455
|
return null;
|
|
@@ -354,29 +459,30 @@ function parseSessionReferencePayload(value) {
|
|
|
354
459
|
sessionId: value.sessionId
|
|
355
460
|
});
|
|
356
461
|
}
|
|
357
|
-
function
|
|
462
|
+
function parseBookmarkReference(value) {
|
|
358
463
|
if (!isRecord(value)) return null;
|
|
359
464
|
const reference = parseSessionReferencePayload(value.reference);
|
|
360
|
-
if (reference)
|
|
361
|
-
|
|
362
|
-
return session2 ? { reference, session: session2 } : null;
|
|
363
|
-
}
|
|
364
|
-
if (typeof value.agentKey !== "string" || typeof value.sessionId !== "string" || typeof value.fullPath !== "string" || !value.agentKey.trim() || !value.sessionId) {
|
|
465
|
+
if (reference) return reference;
|
|
466
|
+
if (typeof value.agentKey !== "string" || typeof value.sessionId !== "string" || !value.agentKey.trim() || !value.sessionId) {
|
|
365
467
|
return null;
|
|
366
468
|
}
|
|
367
|
-
|
|
469
|
+
return normalizeSessionReference({
|
|
368
470
|
agentName: value.agentKey.trim().toLowerCase(),
|
|
369
471
|
sessionId: value.sessionId
|
|
370
472
|
});
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
473
|
+
}
|
|
474
|
+
function parseBookmarkImport(value) {
|
|
475
|
+
if (!isRecord(value)) return null;
|
|
476
|
+
const reference = parseBookmarkReference(value);
|
|
477
|
+
if (!reference) return null;
|
|
478
|
+
const timestamp = value.bookmarkedAt ?? value.bookmarked_at;
|
|
479
|
+
if (timestamp != null && (typeof timestamp !== "number" || !Number.isFinite(timestamp))) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
reference,
|
|
484
|
+
bookmarkedAt: typeof timestamp === "number" ? timestamp : Date.now()
|
|
485
|
+
};
|
|
380
486
|
}
|
|
381
487
|
function sanitizeClientLogData(value) {
|
|
382
488
|
if (!isRecord(value)) return {};
|
|
@@ -391,9 +497,14 @@ function sanitizeClientLogData(value) {
|
|
|
391
497
|
);
|
|
392
498
|
}
|
|
393
499
|
function toSessionListItem(session) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
500
|
+
const {
|
|
501
|
+
model_usage: _modelUsage,
|
|
502
|
+
project_identity_resolver_revision: _resolverRevision,
|
|
503
|
+
project_identity_input_signature: _identityInputSignature,
|
|
504
|
+
smart_tags_source_updated_at: _smartTagsSourceUpdatedAt,
|
|
505
|
+
smart_tags_classifier_revision: _smartTagsClassifierRevision,
|
|
506
|
+
...item
|
|
507
|
+
} = session;
|
|
397
508
|
return item;
|
|
398
509
|
}
|
|
399
510
|
function getSessionHeadReference(session) {
|
|
@@ -406,6 +517,7 @@ var SESSION_DETAIL_STREAM_BATCH_CHARS = 64 * 1024;
|
|
|
406
517
|
function createSessionDetailJsonResponse(data, messages) {
|
|
407
518
|
const encoder = new TextEncoder();
|
|
408
519
|
const headerJson = JSON.stringify(data);
|
|
520
|
+
const headerPrefix = headerJson === "{}" ? '{"messages":[' : `${headerJson.slice(0, -1)},"messages":[`;
|
|
409
521
|
const iterator = messages[Symbol.iterator]();
|
|
410
522
|
let wroteHeader = false;
|
|
411
523
|
let wroteMessage = false;
|
|
@@ -413,20 +525,30 @@ function createSessionDetailJsonResponse(data, messages) {
|
|
|
413
525
|
new ReadableStream({
|
|
414
526
|
pull(controller) {
|
|
415
527
|
if (!wroteHeader) {
|
|
416
|
-
controller.enqueue(encoder.encode(
|
|
528
|
+
controller.enqueue(encoder.encode(headerPrefix));
|
|
417
529
|
wroteHeader = true;
|
|
418
530
|
return;
|
|
419
531
|
}
|
|
420
532
|
const batch = [];
|
|
421
533
|
let batchLength = 0;
|
|
422
|
-
let next
|
|
423
|
-
|
|
424
|
-
const prefix = wroteMessage ? "," : "";
|
|
425
|
-
batch.push(prefix, next.value);
|
|
426
|
-
batchLength += prefix.length + next.value.length;
|
|
427
|
-
wroteMessage = true;
|
|
428
|
-
if (batchLength >= SESSION_DETAIL_STREAM_BATCH_CHARS) break;
|
|
534
|
+
let next;
|
|
535
|
+
try {
|
|
429
536
|
next = iterator.next();
|
|
537
|
+
while (!next.done) {
|
|
538
|
+
const prefix = wroteMessage ? "," : "";
|
|
539
|
+
batch.push(prefix, next.value);
|
|
540
|
+
batchLength += prefix.length + next.value.length;
|
|
541
|
+
wroteMessage = true;
|
|
542
|
+
if (batchLength >= SESSION_DETAIL_STREAM_BATCH_CHARS) break;
|
|
543
|
+
next = iterator.next();
|
|
544
|
+
}
|
|
545
|
+
} catch (error) {
|
|
546
|
+
try {
|
|
547
|
+
iterator.return?.();
|
|
548
|
+
} catch {
|
|
549
|
+
}
|
|
550
|
+
controller.error(error);
|
|
551
|
+
return;
|
|
430
552
|
}
|
|
431
553
|
if (next.done) {
|
|
432
554
|
batch.push("]}");
|
|
@@ -458,8 +580,9 @@ function handleGetScanStatus(c, scanSource) {
|
|
|
458
580
|
}
|
|
459
581
|
function handleGetAgents(c, scanSource, defaults = {}) {
|
|
460
582
|
const scanResult = scanSource.getSnapshot();
|
|
461
|
-
const
|
|
462
|
-
|
|
583
|
+
const window = parseDateWindowRequest(c, "agents", defaults);
|
|
584
|
+
if (window.kind === "rejected") return window.response;
|
|
585
|
+
const { from, to } = window;
|
|
463
586
|
const agents = getSnapshotAggregation(
|
|
464
587
|
scanSource,
|
|
465
588
|
scanResult.sessions,
|
|
@@ -478,16 +601,18 @@ function handleGetAgents(c, scanSource, defaults = {}) {
|
|
|
478
601
|
}
|
|
479
602
|
function handleGetProjects(c, scanSource, defaults = {}) {
|
|
480
603
|
const scanResult = scanSource.getSnapshot();
|
|
481
|
-
const
|
|
482
|
-
|
|
604
|
+
const window = parseDateWindowRequest(c, "projects", defaults);
|
|
605
|
+
if (window.kind === "rejected") return window.response;
|
|
606
|
+
const { from, to } = window;
|
|
483
607
|
const projects = getSnapshotAggregation(
|
|
484
608
|
scanSource,
|
|
485
609
|
scanResult.sessions,
|
|
486
610
|
["projects", from, to],
|
|
487
611
|
() => {
|
|
488
|
-
const
|
|
612
|
+
const tree = buildSessionTree(scanResult.sessions);
|
|
613
|
+
const sessions = filterSessionTreeByActivityWindow(scanResult.sessions, from, to, tree);
|
|
489
614
|
return {
|
|
490
|
-
projects:
|
|
615
|
+
projects: attachProjectMetricsFromTree(listCachedProjectGroups(sessions), tree, from, to)
|
|
491
616
|
};
|
|
492
617
|
}
|
|
493
618
|
);
|
|
@@ -495,7 +620,13 @@ function handleGetProjects(c, scanSource, defaults = {}) {
|
|
|
495
620
|
}
|
|
496
621
|
function handleGetSessions(c, scanSource, defaults = {}) {
|
|
497
622
|
const scanResult = scanSource.getSnapshot();
|
|
498
|
-
const
|
|
623
|
+
const params = searchParams(c);
|
|
624
|
+
const paginationRequested = params.has("limit") || params.has("cursor");
|
|
625
|
+
const sessionQuery = parseSessionQuery(params, KNOWN_AGENT_NAMES, SESSION_PAGE_LIMIT_POLICY);
|
|
626
|
+
if (sessionQuery.limit.kind === "invalid") {
|
|
627
|
+
reportInvalidQueryParameter("sessions", "limit", "rejected");
|
|
628
|
+
return c.json({ error: sessionQuery.limit.error }, 400);
|
|
629
|
+
}
|
|
499
630
|
const q = c.req.query("q")?.toLowerCase();
|
|
500
631
|
const cwd = c.req.query("cwd");
|
|
501
632
|
const projectIdentity = parseProjectIdentityFilter(
|
|
@@ -506,26 +637,31 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
506
637
|
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
507
638
|
}
|
|
508
639
|
const tag = c.req.query("tag")?.toLowerCase();
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if (agent
|
|
513
|
-
sessions
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
sessions
|
|
519
|
-
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
640
|
+
const window = parseDateWindowRequest(c, "sessions", defaults);
|
|
641
|
+
if (window.kind === "rejected") return window.response;
|
|
642
|
+
const { from, to } = window;
|
|
643
|
+
if (sessionQuery.agent.kind === "unknown") {
|
|
644
|
+
reportInvalidQueryParameter("sessions", "agent", "empty_result");
|
|
645
|
+
}
|
|
646
|
+
const agentFilter = sessionQuery.agent.kind === "known" ? sessionQuery.agent.agentName : sessionQuery.agent.kind;
|
|
647
|
+
let sessions = getSnapshotAggregation(
|
|
648
|
+
scanSource,
|
|
649
|
+
scanResult.sessions,
|
|
650
|
+
["sessions", agentFilter, projectIdentity?.kind, projectIdentity?.key, cwd, tag, from, to],
|
|
651
|
+
() => {
|
|
652
|
+
let filtered = sessionQuery.agent.kind === "all" ? scanResult.sessions : sessionQuery.agent.kind === "known" ? scanResult.byAgent[sessionQuery.agent.agentName] ?? [] : [];
|
|
653
|
+
if (projectIdentity) {
|
|
654
|
+
filtered = filtered.filter(
|
|
655
|
+
(session) => matchesProjectIdentity(session.project_identity, projectIdentity)
|
|
656
|
+
);
|
|
657
|
+
} else if (cwd) {
|
|
658
|
+
const projectScope = createProjectScopeMatcher(cwd);
|
|
659
|
+
filtered = filtered.filter((session) => matchesProjectScope(session, projectScope));
|
|
660
|
+
}
|
|
661
|
+
filtered = filterSessionsByActivityWindow(filtered, from, to);
|
|
662
|
+
return tag ? filtered.filter((session) => session.smart_tags?.includes(tag)) : filtered;
|
|
663
|
+
}
|
|
664
|
+
);
|
|
529
665
|
const aliases = loadAliasView();
|
|
530
666
|
if (q) {
|
|
531
667
|
sessions = sessions.filter((session) => {
|
|
@@ -533,15 +669,93 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
533
669
|
return session.title.toLowerCase().includes(q) || alias?.toLowerCase().includes(q);
|
|
534
670
|
});
|
|
535
671
|
}
|
|
672
|
+
if (paginationRequested) {
|
|
673
|
+
const page = paginateSessionSnapshot(sessions, {
|
|
674
|
+
cursor: params.get("cursor") ?? void 0,
|
|
675
|
+
limit: sessionQuery.limit.value,
|
|
676
|
+
query: params,
|
|
677
|
+
snapshotIdentity: scanResult.sessions,
|
|
678
|
+
viewIdentity: aliases
|
|
679
|
+
});
|
|
680
|
+
if (page.kind === "invalid_cursor") {
|
|
681
|
+
reportInvalidQueryParameter("sessions", "cursor", "rejected");
|
|
682
|
+
return c.json({ error: "cursor is invalid for this request" }, 400);
|
|
683
|
+
}
|
|
684
|
+
if (page.kind === "stale_snapshot") {
|
|
685
|
+
return c.json({ error: "session snapshot changed; restart pagination" }, 409);
|
|
686
|
+
}
|
|
687
|
+
return c.json({
|
|
688
|
+
sessions: page.items.map(
|
|
689
|
+
(session) => toSessionListItem(aliases.decorate(session, getSessionHeadReference(session)))
|
|
690
|
+
),
|
|
691
|
+
...page.nextCursor ? { nextCursor: page.nextCursor } : {}
|
|
692
|
+
});
|
|
693
|
+
}
|
|
536
694
|
return c.json({
|
|
537
695
|
sessions: sessions.map(
|
|
538
696
|
(session) => toSessionListItem(aliases.decorate(session, getSessionHeadReference(session)))
|
|
539
697
|
)
|
|
540
698
|
});
|
|
541
699
|
}
|
|
700
|
+
function withParentContext(results, sessions, aliases) {
|
|
701
|
+
if (!results.some((result) => result.session.parent_reference)) return results;
|
|
702
|
+
const byRouteKey = new Map(
|
|
703
|
+
sessions.map((session) => [
|
|
704
|
+
getSessionRouteKey(getSessionAgentKey(session), session.id),
|
|
705
|
+
session
|
|
706
|
+
])
|
|
707
|
+
);
|
|
708
|
+
return results.map((result) => {
|
|
709
|
+
const parentReference = result.session.parent_reference;
|
|
710
|
+
if (!parentReference) return result;
|
|
711
|
+
const parent = byRouteKey.get(formatSessionReference(parentReference));
|
|
712
|
+
if (!parent) return result;
|
|
713
|
+
const reference = normalizeSessionReference(parentReference);
|
|
714
|
+
return { ...result, parent: { reference, title: aliases.get(reference) ?? parent.title } };
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
var ALIAS_SEARCH_RESULT_SHARE = 0.25;
|
|
718
|
+
function searchResultKey(result) {
|
|
719
|
+
return `${result.reference.agentName}\0${result.reference.sessionId}`;
|
|
720
|
+
}
|
|
721
|
+
function uniqueSearchResults(results) {
|
|
722
|
+
const seen = /* @__PURE__ */ new Set();
|
|
723
|
+
return results.filter((result) => {
|
|
724
|
+
const key = searchResultKey(result);
|
|
725
|
+
if (seen.has(key)) return false;
|
|
726
|
+
seen.add(key);
|
|
727
|
+
return true;
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
function mergeAliasSearchResults(rankedResults, aliasResults, limit) {
|
|
731
|
+
if (limit <= 0) return [];
|
|
732
|
+
const ranked = uniqueSearchResults(rankedResults);
|
|
733
|
+
const rankedKeys = new Set(ranked.map(searchResultKey));
|
|
734
|
+
const aliases = uniqueSearchResults(aliasResults).filter(
|
|
735
|
+
(result) => !rankedKeys.has(searchResultKey(result))
|
|
736
|
+
);
|
|
737
|
+
if (ranked.length === 0) return aliases.slice(0, limit);
|
|
738
|
+
if (aliases.length === 0) return ranked.slice(0, limit);
|
|
739
|
+
const aliasQuota = Math.min(
|
|
740
|
+
limit - 1,
|
|
741
|
+
Math.max(1, Math.floor(limit * ALIAS_SEARCH_RESULT_SHARE))
|
|
742
|
+
);
|
|
743
|
+
const rankedQuota = limit - aliasQuota;
|
|
744
|
+
return [
|
|
745
|
+
...ranked.slice(0, rankedQuota),
|
|
746
|
+
...aliases.slice(0, aliasQuota),
|
|
747
|
+
...ranked.slice(rankedQuota),
|
|
748
|
+
...aliases.slice(aliasQuota)
|
|
749
|
+
].slice(0, limit);
|
|
750
|
+
}
|
|
542
751
|
function handleSearchSessions(c, scanSource, defaults = {}) {
|
|
543
752
|
const query = c.req.query("q")?.trim() ?? "";
|
|
544
753
|
const scanResult = scanSource.getSnapshot();
|
|
754
|
+
const sessionQuery = parseSessionQuery(searchParams(c), KNOWN_AGENT_NAMES, SEARCH_LIMIT_POLICY);
|
|
755
|
+
if (sessionQuery.limit.kind === "invalid") {
|
|
756
|
+
reportInvalidQueryParameter("search", "limit", "rejected");
|
|
757
|
+
return c.json({ error: sessionQuery.limit.error }, 400);
|
|
758
|
+
}
|
|
545
759
|
const projectIdentity = parseProjectIdentityFilter(
|
|
546
760
|
c.req.query("projectKind"),
|
|
547
761
|
c.req.query("projectKey")
|
|
@@ -549,22 +763,42 @@ function handleSearchSessions(c, scanSource, defaults = {}) {
|
|
|
549
763
|
if (projectIdentity === null) {
|
|
550
764
|
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
551
765
|
}
|
|
552
|
-
const
|
|
766
|
+
const window = parseDateWindowRequest(c, "search", defaults);
|
|
767
|
+
if (window.kind === "rejected") return window.response;
|
|
768
|
+
if (sessionQuery.agent.kind === "unknown") {
|
|
769
|
+
reportInvalidQueryParameter("search", "agent", "empty_result");
|
|
770
|
+
return c.json({ results: [] });
|
|
771
|
+
}
|
|
772
|
+
const searchOptions = parseSearchOptions(
|
|
773
|
+
c,
|
|
774
|
+
window,
|
|
775
|
+
{
|
|
776
|
+
agent: sessionQuery.agent.kind === "known" ? sessionQuery.agent.agentName : void 0,
|
|
777
|
+
limit: sessionQuery.limit.value
|
|
778
|
+
},
|
|
779
|
+
projectIdentity
|
|
780
|
+
);
|
|
553
781
|
const aliases = loadAliasView();
|
|
554
782
|
const results = executeSessionSearch(query, searchOptions, scanResult).map((result) => ({
|
|
555
783
|
...result,
|
|
556
784
|
session: aliases.decorate(result.session, result.reference)
|
|
557
785
|
}));
|
|
558
786
|
const aliasResults = findAliasSearchResults(query, searchOptions, scanResult, aliases);
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
return c.json({ results: [...deduped.values()].slice(0, searchOptions.limit ?? 50) });
|
|
787
|
+
const mergedResults = mergeAliasSearchResults(results, aliasResults, searchOptions.limit ?? 50);
|
|
788
|
+
return c.json({
|
|
789
|
+
results: withParentContext(mergedResults, scanResult.sessions, aliases)
|
|
790
|
+
});
|
|
564
791
|
}
|
|
565
792
|
function handleGetFileActivity(c, defaults = {}) {
|
|
566
|
-
const
|
|
567
|
-
|
|
793
|
+
const sessionQuery = parseSessionQuery(
|
|
794
|
+
searchParams(c),
|
|
795
|
+
KNOWN_AGENT_NAMES,
|
|
796
|
+
FILE_ACTIVITY_LIMIT_POLICY
|
|
797
|
+
);
|
|
798
|
+
if (sessionQuery.limit.kind === "invalid") {
|
|
799
|
+
reportInvalidQueryParameter("file-activity", "limit", "rejected");
|
|
800
|
+
return c.json({ error: sessionQuery.limit.error }, 400);
|
|
801
|
+
}
|
|
568
802
|
const projectIdentity = parseProjectIdentityFilter(
|
|
569
803
|
c.req.query("projectKind"),
|
|
570
804
|
c.req.query("projectKey")
|
|
@@ -572,10 +806,16 @@ function handleGetFileActivity(c, defaults = {}) {
|
|
|
572
806
|
if (projectIdentity === null) {
|
|
573
807
|
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
574
808
|
}
|
|
809
|
+
const window = parseDateWindowRequest(c, "file-activity", defaults);
|
|
810
|
+
if (window.kind === "rejected") return window.response;
|
|
811
|
+
if (sessionQuery.agent.kind === "unknown") {
|
|
812
|
+
reportInvalidQueryParameter("file-activity", "agent", "empty_result");
|
|
813
|
+
return c.json({ activity: [] });
|
|
814
|
+
}
|
|
575
815
|
const aliases = loadAliasView();
|
|
576
816
|
return c.json({
|
|
577
817
|
activity: listFileActivity({
|
|
578
|
-
agent:
|
|
818
|
+
agent: sessionQuery.agent.kind === "known" ? sessionQuery.agent.agentName : void 0,
|
|
579
819
|
sessionId: optionalQueryValue(c.req.query("sessionId")),
|
|
580
820
|
projectKind: projectIdentity?.kind,
|
|
581
821
|
projectKey: projectIdentity?.key,
|
|
@@ -583,9 +823,9 @@ function handleGetFileActivity(c, defaults = {}) {
|
|
|
583
823
|
cwd: optionalQueryValue(c.req.query("cwd")),
|
|
584
824
|
path: optionalQueryValue(c.req.query("path")),
|
|
585
825
|
kind: parseFileActivityKind(optionalQueryValue(c.req.query("kind"))),
|
|
586
|
-
from:
|
|
587
|
-
to:
|
|
588
|
-
limit
|
|
826
|
+
from: window.from,
|
|
827
|
+
to: window.to,
|
|
828
|
+
limit: sessionQuery.limit.value
|
|
589
829
|
}).map((activity) => decorateFileActivity(activity, aliases))
|
|
590
830
|
});
|
|
591
831
|
}
|
|
@@ -600,10 +840,12 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
600
840
|
return c.json({ error: "Missing session ID" }, 400);
|
|
601
841
|
}
|
|
602
842
|
try {
|
|
603
|
-
const
|
|
843
|
+
const reference = {
|
|
604
844
|
agentName,
|
|
605
845
|
sessionId
|
|
606
|
-
}
|
|
846
|
+
};
|
|
847
|
+
const messageCursor = optionalQueryValue(c.req.query("messageCursor"));
|
|
848
|
+
const result = messageCursor ? materializeSessionDetailResponse(scanSource.getSnapshot(), reference, { messageCursor }) : materializeSessionDetailResponse(scanSource.getSnapshot(), reference);
|
|
607
849
|
if (result.status === "unknown-agent") {
|
|
608
850
|
return c.json({ error: `Unknown agent: ${agentName}` }, 404);
|
|
609
851
|
}
|
|
@@ -619,6 +861,8 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
619
861
|
agent: agentName,
|
|
620
862
|
session_id: sessionId,
|
|
621
863
|
messages: result.status === "found-json" ? result.messageCount : result.data.messages.length,
|
|
864
|
+
sent_messages: result.status === "found-json" ? result.sentMessageCount : result.data.messages.length,
|
|
865
|
+
message_update: result.data.message_update ?? "reset",
|
|
622
866
|
duration_ms: Math.round(performance.now() - startedAt)
|
|
623
867
|
});
|
|
624
868
|
const aliases = loadAliasView();
|
|
@@ -637,7 +881,7 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
637
881
|
duration_ms: Math.round(performance.now() - startedAt),
|
|
638
882
|
error: message
|
|
639
883
|
});
|
|
640
|
-
return c.json({ error:
|
|
884
|
+
return c.json({ error: "Failed to load session" }, 500);
|
|
641
885
|
}
|
|
642
886
|
}
|
|
643
887
|
async function handlePostClientLog(c) {
|
|
@@ -650,12 +894,27 @@ async function handlePostClientLog(c) {
|
|
|
650
894
|
appLogger.info(`client.${event}`, sanitizeClientLogData(payload?.data));
|
|
651
895
|
return c.json({ ok: true });
|
|
652
896
|
}
|
|
653
|
-
function
|
|
897
|
+
function materializeStoredBookmarks(scanSource, bookmarks, aliases) {
|
|
898
|
+
const snapshot = scanSource.getSnapshot();
|
|
899
|
+
const sessionIndex = getSnapshotAggregation(
|
|
900
|
+
scanSource,
|
|
901
|
+
snapshot.sessions,
|
|
902
|
+
["bookmark-session-index"],
|
|
903
|
+
() => createSessionIndex(snapshot.sessions)
|
|
904
|
+
);
|
|
905
|
+
return materializeBookmarkViews(bookmarks, {
|
|
906
|
+
liveSessionsByReference: sessionIndex.byRouteKey,
|
|
907
|
+
knownAgentNames: KNOWN_AGENT_NAME_SET,
|
|
908
|
+
resolveCachedSessions: loadCachedSessionHeads
|
|
909
|
+
}).map((bookmark) => decorateBookmark(bookmark, aliases));
|
|
910
|
+
}
|
|
911
|
+
function handleGetBookmarks(c, scanSource) {
|
|
654
912
|
return withStorageErrors(
|
|
655
913
|
() => {
|
|
656
914
|
const aliases = loadAliasView();
|
|
915
|
+
const bookmarks = materializeStoredBookmarks(scanSource, listBookmarks(), aliases);
|
|
657
916
|
return c.json({
|
|
658
|
-
bookmarks
|
|
917
|
+
bookmarks,
|
|
659
918
|
storageAvailable: true
|
|
660
919
|
});
|
|
661
920
|
},
|
|
@@ -663,7 +922,7 @@ function handleGetBookmarks(c) {
|
|
|
663
922
|
);
|
|
664
923
|
}
|
|
665
924
|
async function handlePutBookmark(c) {
|
|
666
|
-
const payload =
|
|
925
|
+
const payload = parseBookmarkReference(await c.req.json().catch(() => null));
|
|
667
926
|
if (!payload) {
|
|
668
927
|
return c.json({ error: "Invalid bookmark payload" }, 400);
|
|
669
928
|
}
|
|
@@ -672,17 +931,21 @@ async function handlePutBookmark(c) {
|
|
|
672
931
|
() => c.json({ error: "Bookmark storage is unavailable" }, 503)
|
|
673
932
|
);
|
|
674
933
|
}
|
|
675
|
-
async function handleImportBookmarks(c) {
|
|
934
|
+
async function handleImportBookmarks(c, scanSource) {
|
|
676
935
|
const payload = await c.req.json().catch(() => null);
|
|
677
936
|
if (!Array.isArray(payload)) {
|
|
678
937
|
return c.json({ error: "Invalid bookmark payload" }, 400);
|
|
679
938
|
}
|
|
680
|
-
const bookmarks = payload.map((entry) =>
|
|
939
|
+
const bookmarks = payload.map((entry) => parseBookmarkImport(entry)).filter((entry) => entry !== null);
|
|
681
940
|
if (bookmarks.length !== payload.length) {
|
|
682
941
|
return c.json({ error: "Invalid bookmark payload" }, 400);
|
|
683
942
|
}
|
|
684
943
|
return withStorageErrors(
|
|
685
|
-
() =>
|
|
944
|
+
() => {
|
|
945
|
+
const imported = importBookmarks(bookmarks);
|
|
946
|
+
const views = materializeStoredBookmarks(scanSource, imported, loadAliasView());
|
|
947
|
+
return c.json({ bookmarks: views, storageAvailable: true });
|
|
948
|
+
},
|
|
686
949
|
() => c.json({ error: "Bookmark storage is unavailable" }, 503)
|
|
687
950
|
);
|
|
688
951
|
}
|
|
@@ -748,6 +1011,8 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
748
1011
|
if (projectIdentity === null) {
|
|
749
1012
|
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
750
1013
|
}
|
|
1014
|
+
const dateWindow = parseDateWindowRequest(c, "dashboard", defaults);
|
|
1015
|
+
if (dateWindow.kind === "rejected") return dateWindow.response;
|
|
751
1016
|
const { from, to, days } = resolveTimeWindow({
|
|
752
1017
|
mode: "dashboard",
|
|
753
1018
|
query: {
|
|
@@ -762,7 +1027,8 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
762
1027
|
projectKind: projectIdentity?.kind,
|
|
763
1028
|
projectKey: projectIdentity?.key
|
|
764
1029
|
};
|
|
765
|
-
const
|
|
1030
|
+
const compare = from == null ? void 0 : { from: addCalendarDays(from, -(days ?? countCalendarDays(from, to))), to: from - 1 };
|
|
1031
|
+
const fixedTo = dateWindow.to;
|
|
766
1032
|
const aggregate = getSnapshotAggregation(
|
|
767
1033
|
scanSource,
|
|
768
1034
|
scanResult.sessions,
|
|
@@ -772,7 +1038,9 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
772
1038
|
scope.projectKind,
|
|
773
1039
|
scope.projectKey,
|
|
774
1040
|
from,
|
|
775
|
-
fixedTo ?? startOfCalendarDay(to)
|
|
1041
|
+
fixedTo ?? startOfCalendarDay(to),
|
|
1042
|
+
compare?.from,
|
|
1043
|
+
compare?.to
|
|
776
1044
|
],
|
|
777
1045
|
() => {
|
|
778
1046
|
const agentInfo = getAgentInfoMap({});
|
|
@@ -782,7 +1050,8 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
782
1050
|
scope,
|
|
783
1051
|
from,
|
|
784
1052
|
to,
|
|
785
|
-
agentInfoMap
|
|
1053
|
+
agentInfoMap,
|
|
1054
|
+
compare
|
|
786
1055
|
});
|
|
787
1056
|
}
|
|
788
1057
|
);
|
|
@@ -796,7 +1065,14 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
796
1065
|
to,
|
|
797
1066
|
limit: 12
|
|
798
1067
|
}),
|
|
799
|
-
|
|
1068
|
+
modelCost: listModelCostDistribution({
|
|
1069
|
+
agent: scope.agent,
|
|
1070
|
+
projectKind: scope.projectKind,
|
|
1071
|
+
projectKey: scope.projectKey,
|
|
1072
|
+
from,
|
|
1073
|
+
to
|
|
1074
|
+
}),
|
|
1075
|
+
window: { from, to, days, compareFrom: compare?.from, compareTo: compare?.to }
|
|
800
1076
|
};
|
|
801
1077
|
const aliases = loadAliasView();
|
|
802
1078
|
return c.json({
|
|
@@ -811,52 +1087,174 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
811
1087
|
});
|
|
812
1088
|
}
|
|
813
1089
|
|
|
1090
|
+
// src/api/sse-event-buffer.ts
|
|
1091
|
+
var MAX_PENDING_CRITICAL_SSE_FRAMES = 64;
|
|
1092
|
+
function scanStatusMilestone(status) {
|
|
1093
|
+
const agentStates = Object.entries(status.agentStatuses).sort(([left], [right]) => left.localeCompare(right)).map(([agentName, agentStatus]) => [agentName, agentStatus.status, agentStatus.error ?? null]);
|
|
1094
|
+
return JSON.stringify([
|
|
1095
|
+
status.active,
|
|
1096
|
+
status.phase,
|
|
1097
|
+
status.totalAgents,
|
|
1098
|
+
status.pendingAgents,
|
|
1099
|
+
status.scanningAgents,
|
|
1100
|
+
status.completedAgents,
|
|
1101
|
+
agentStates,
|
|
1102
|
+
status.backfill.active,
|
|
1103
|
+
status.backfill.currentAgent ?? null,
|
|
1104
|
+
status.backfill.pendingAgents,
|
|
1105
|
+
status.backfill.completedAgents,
|
|
1106
|
+
status.backfill.failedAgents,
|
|
1107
|
+
status.backfill.progress?.phase ?? null,
|
|
1108
|
+
status.searchIndexMaintenance?.active ?? false,
|
|
1109
|
+
status.searchIndexMaintenance?.currentAgent ?? null,
|
|
1110
|
+
status.searchIndexMaintenance?.pendingAgents ?? [],
|
|
1111
|
+
status.searchIndexMaintenance?.completedAgents ?? [],
|
|
1112
|
+
status.searchIndexMaintenance?.failedAgents ?? []
|
|
1113
|
+
]);
|
|
1114
|
+
}
|
|
1115
|
+
var SseEventBuffer = class {
|
|
1116
|
+
constructor(controller, onOverflow) {
|
|
1117
|
+
this.controller = controller;
|
|
1118
|
+
this.onOverflow = onOverflow;
|
|
1119
|
+
}
|
|
1120
|
+
controller;
|
|
1121
|
+
onOverflow;
|
|
1122
|
+
encoder = new TextEncoder();
|
|
1123
|
+
frames = [];
|
|
1124
|
+
replaceableStatus = null;
|
|
1125
|
+
lastStatusMilestone = null;
|
|
1126
|
+
criticalFrameCount = 0;
|
|
1127
|
+
isClosed = false;
|
|
1128
|
+
enqueue(event, data) {
|
|
1129
|
+
if (!this.freezeReplaceableStatus()) return;
|
|
1130
|
+
this.appendCritical(this.frame(event, data));
|
|
1131
|
+
}
|
|
1132
|
+
enqueueScanStatus(status) {
|
|
1133
|
+
if (this.isClosed) return;
|
|
1134
|
+
const milestone = scanStatusMilestone(status);
|
|
1135
|
+
const bytes = this.frame(status.type, status);
|
|
1136
|
+
if (this.lastStatusMilestone !== milestone) {
|
|
1137
|
+
if (!this.freezeReplaceableStatus()) return;
|
|
1138
|
+
this.lastStatusMilestone = milestone;
|
|
1139
|
+
this.appendCritical(bytes);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (this.replaceableStatus) {
|
|
1143
|
+
this.replaceableStatus.bytes = bytes;
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const frame = { bytes, critical: false };
|
|
1147
|
+
this.frames.push(frame);
|
|
1148
|
+
this.replaceableStatus = frame;
|
|
1149
|
+
this.drain();
|
|
1150
|
+
}
|
|
1151
|
+
enqueueHeartbeat() {
|
|
1152
|
+
if (this.isClosed || this.frames.length > 0 || this.controller.desiredSize == null || this.controller.desiredSize <= 0) {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
this.controller.enqueue(this.encoder.encode(": keepalive\n\n"));
|
|
1156
|
+
}
|
|
1157
|
+
drain() {
|
|
1158
|
+
while (!this.isClosed && this.frames.length > 0 && this.controller.desiredSize != null && this.controller.desiredSize > 0) {
|
|
1159
|
+
const frame = this.frames.shift();
|
|
1160
|
+
if (frame === this.replaceableStatus) this.replaceableStatus = null;
|
|
1161
|
+
if (frame.critical) this.criticalFrameCount -= 1;
|
|
1162
|
+
this.controller.enqueue(frame.bytes);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
close() {
|
|
1166
|
+
this.isClosed = true;
|
|
1167
|
+
this.frames.length = 0;
|
|
1168
|
+
this.replaceableStatus = null;
|
|
1169
|
+
this.criticalFrameCount = 0;
|
|
1170
|
+
}
|
|
1171
|
+
freezeReplaceableStatus() {
|
|
1172
|
+
if (!this.replaceableStatus) return !this.isClosed;
|
|
1173
|
+
if (this.criticalFrameCount >= MAX_PENDING_CRITICAL_SSE_FRAMES) {
|
|
1174
|
+
this.overflow();
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
this.replaceableStatus.critical = true;
|
|
1178
|
+
this.replaceableStatus = null;
|
|
1179
|
+
this.criticalFrameCount += 1;
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
appendCritical(bytes) {
|
|
1183
|
+
if (this.isClosed) return;
|
|
1184
|
+
if (this.criticalFrameCount >= MAX_PENDING_CRITICAL_SSE_FRAMES) {
|
|
1185
|
+
this.overflow();
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
this.frames.push({ bytes, critical: true });
|
|
1189
|
+
this.criticalFrameCount += 1;
|
|
1190
|
+
this.drain();
|
|
1191
|
+
}
|
|
1192
|
+
overflow() {
|
|
1193
|
+
if (this.isClosed) return;
|
|
1194
|
+
this.close();
|
|
1195
|
+
this.onOverflow();
|
|
1196
|
+
}
|
|
1197
|
+
frame(event, data) {
|
|
1198
|
+
return this.encoder.encode(`event: ${event}
|
|
1199
|
+
data: ${JSON.stringify(data)}
|
|
1200
|
+
|
|
1201
|
+
`);
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
|
|
814
1205
|
// src/api/routes.ts
|
|
815
1206
|
function createSseResponse(eventSource, signal) {
|
|
816
|
-
const encoder = new TextEncoder();
|
|
817
1207
|
let cancelStream = () => {
|
|
818
1208
|
};
|
|
1209
|
+
let drainStream = () => {
|
|
1210
|
+
};
|
|
819
1211
|
return new Response(
|
|
820
1212
|
new ReadableStream({
|
|
821
1213
|
start(controller) {
|
|
822
1214
|
let isClosed = false;
|
|
823
|
-
|
|
824
|
-
if (isClosed) return;
|
|
825
|
-
controller.enqueue(encoder.encode(`event: ${event}
|
|
826
|
-
`));
|
|
827
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
828
|
-
|
|
829
|
-
`));
|
|
1215
|
+
let unsubscribeSessions = () => {
|
|
830
1216
|
};
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
write(event.type, event);
|
|
838
|
-
});
|
|
839
|
-
const heartbeat = setInterval(() => {
|
|
840
|
-
if (!isClosed) controller.enqueue(encoder.encode(": keepalive\n\n"));
|
|
841
|
-
}, 15e3);
|
|
1217
|
+
let unsubscribeScanStatus = () => {
|
|
1218
|
+
};
|
|
1219
|
+
let abortStream = () => {
|
|
1220
|
+
};
|
|
1221
|
+
let heartbeat;
|
|
1222
|
+
let buffer;
|
|
842
1223
|
const cleanup = () => {
|
|
843
1224
|
if (isClosed) return false;
|
|
844
1225
|
isClosed = true;
|
|
845
|
-
|
|
1226
|
+
buffer.close();
|
|
1227
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
846
1228
|
unsubscribeSessions();
|
|
847
1229
|
unsubscribeScanStatus();
|
|
848
1230
|
signal.removeEventListener("abort", abortStream);
|
|
849
1231
|
return true;
|
|
850
1232
|
};
|
|
851
|
-
|
|
1233
|
+
abortStream = () => {
|
|
852
1234
|
if (cleanup()) controller.close();
|
|
853
1235
|
};
|
|
1236
|
+
buffer = new SseEventBuffer(controller, () => {
|
|
1237
|
+
if (cleanup()) controller.error(new Error("SSE client fell behind"));
|
|
1238
|
+
});
|
|
1239
|
+
drainStream = () => buffer.drain();
|
|
1240
|
+
buffer.enqueue("connected", { timestamp: Date.now() });
|
|
1241
|
+
buffer.enqueueScanStatus(eventSource.getScanStatus());
|
|
1242
|
+
unsubscribeSessions = eventSource.subscribe((event) => {
|
|
1243
|
+
buffer.enqueue(event.type, event);
|
|
1244
|
+
});
|
|
1245
|
+
unsubscribeScanStatus = eventSource.subscribeScanStatus((event) => {
|
|
1246
|
+
buffer.enqueueScanStatus(event);
|
|
1247
|
+
});
|
|
1248
|
+
heartbeat = setInterval(() => buffer.enqueueHeartbeat(), 15e3);
|
|
854
1249
|
cancelStream = () => {
|
|
855
1250
|
cleanup();
|
|
856
1251
|
};
|
|
857
1252
|
if (signal.aborted) abortStream();
|
|
858
1253
|
else signal.addEventListener("abort", abortStream, { once: true });
|
|
859
1254
|
},
|
|
1255
|
+
pull() {
|
|
1256
|
+
drainStream();
|
|
1257
|
+
},
|
|
860
1258
|
cancel() {
|
|
861
1259
|
cancelStream();
|
|
862
1260
|
}
|
|
@@ -888,23 +1286,140 @@ function createApiRoutes(scanSource, eventSource, options = {}) {
|
|
|
888
1286
|
api.get("/file-activity", (c) => handleGetFileActivity(c, listDefaults));
|
|
889
1287
|
api.get("/sessions/:agent/:id", (c) => handleGetSessionData(c, scanSource));
|
|
890
1288
|
api.get("/dashboard", (c) => handleGetDashboard(c, scanSource, listDefaults));
|
|
891
|
-
api.get("/bookmarks", (c) => handleGetBookmarks(c));
|
|
1289
|
+
api.get("/bookmarks", (c) => handleGetBookmarks(c, scanSource));
|
|
892
1290
|
api.put("/bookmarks", (c) => handlePutBookmark(c));
|
|
893
|
-
api.post("/bookmarks/import", (c) => handleImportBookmarks(c));
|
|
1291
|
+
api.post("/bookmarks/import", (c) => handleImportBookmarks(c, scanSource));
|
|
894
1292
|
api.delete("/bookmarks/:agent/:id", (c) => handleDeleteBookmark(c));
|
|
895
1293
|
api.put("/session-aliases/:agent/:id", (c) => handlePutSessionAlias(c));
|
|
896
1294
|
api.delete("/session-aliases/:agent/:id", (c) => handleDeleteSessionAlias(c));
|
|
897
1295
|
api.post("/logs", (c) => handlePostClientLog(c));
|
|
898
1296
|
if (eventSource) {
|
|
899
|
-
api.get("/events", (c) =>
|
|
1297
|
+
api.get("/events", (c) => {
|
|
1298
|
+
const signal = options.shutdownSignal ? AbortSignal.any([c.req.raw.signal, options.shutdownSignal]) : c.req.raw.signal;
|
|
1299
|
+
return createSseResponse(eventSource, signal);
|
|
1300
|
+
});
|
|
900
1301
|
}
|
|
901
1302
|
return api;
|
|
902
1303
|
}
|
|
903
1304
|
|
|
1305
|
+
// src/loopback-authority.ts
|
|
1306
|
+
import { isIP } from "net";
|
|
1307
|
+
var LOOPBACK_AUTHORITY_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
1308
|
+
function canonicalizeHostname(hostname) {
|
|
1309
|
+
const unwrapped = hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
1310
|
+
const authorityHost = isIP(unwrapped) === 6 ? `[${unwrapped}]` : unwrapped;
|
|
1311
|
+
try {
|
|
1312
|
+
return new URL(`http://${authorityHost}`).hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
1313
|
+
} catch {
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function parseAuthority(value) {
|
|
1318
|
+
if (!value || value !== value.trim()) return null;
|
|
1319
|
+
const bracketed = /^\[([^\]]+)\]:(\d{1,5})$/.exec(value);
|
|
1320
|
+
const plain = /^([^:[\]]+):(\d{1,5})$/.exec(value);
|
|
1321
|
+
const match = bracketed ?? plain;
|
|
1322
|
+
if (!match) return null;
|
|
1323
|
+
const port = Number(match[2]);
|
|
1324
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
|
|
1325
|
+
const rawHostname = match[1];
|
|
1326
|
+
const authorityHost = bracketed ? `[${rawHostname}]` : rawHostname;
|
|
1327
|
+
try {
|
|
1328
|
+
const parsed = new URL(`http://${authorityHost}:${port}`);
|
|
1329
|
+
if (parsed.username || parsed.password) return null;
|
|
1330
|
+
const hostname = parsed.hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
1331
|
+
return {
|
|
1332
|
+
hostname,
|
|
1333
|
+
port,
|
|
1334
|
+
normalized: isIP(hostname) === 6 ? `[${hostname}]:${port}` : `${hostname}:${port}`
|
|
1335
|
+
};
|
|
1336
|
+
} catch {
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
function hostHeaders(rawHeaders) {
|
|
1341
|
+
const values = [];
|
|
1342
|
+
for (let index = 0; index < rawHeaders.length; index += 2) {
|
|
1343
|
+
if (rawHeaders[index]?.toLowerCase() === "host") values.push(rawHeaders[index + 1] ?? "");
|
|
1344
|
+
}
|
|
1345
|
+
return values;
|
|
1346
|
+
}
|
|
1347
|
+
function validateLoopbackAuthority(rawHeaders, listenerHostname, listenerPort) {
|
|
1348
|
+
if (listenerPort === null) return { allowed: false, reason: "listener-not-ready" };
|
|
1349
|
+
const hosts = hostHeaders(rawHeaders);
|
|
1350
|
+
if (hosts.length === 0) return { allowed: false, reason: "missing-host" };
|
|
1351
|
+
if (hosts.length !== 1) return { allowed: false, reason: "multiple-hosts" };
|
|
1352
|
+
const authority = parseAuthority(hosts[0]);
|
|
1353
|
+
if (!authority) return { allowed: false, reason: "invalid-host" };
|
|
1354
|
+
if (authority.port !== listenerPort) {
|
|
1355
|
+
return { allowed: false, reason: "port-mismatch", authority: authority.normalized };
|
|
1356
|
+
}
|
|
1357
|
+
const listener = canonicalizeHostname(listenerHostname);
|
|
1358
|
+
if (!LOOPBACK_AUTHORITY_HOSTS.has(authority.hostname) && authority.hostname !== listener) {
|
|
1359
|
+
return {
|
|
1360
|
+
allowed: false,
|
|
1361
|
+
reason: "hostname-not-allowed",
|
|
1362
|
+
authority: authority.normalized
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
return { allowed: true, authority: authority.normalized };
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/loopback-write-guard.ts
|
|
1369
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
1370
|
+
var JSON_BODY_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
1371
|
+
var ALLOWED_FETCH_SITES = /* @__PURE__ */ new Set(["same-origin", "none"]);
|
|
1372
|
+
function mediaType(contentType) {
|
|
1373
|
+
return contentType?.split(";", 1)[0]?.trim().toLowerCase() || null;
|
|
1374
|
+
}
|
|
1375
|
+
function isSameOrigin(origin, requestOrigin) {
|
|
1376
|
+
try {
|
|
1377
|
+
return new URL(origin).origin === requestOrigin;
|
|
1378
|
+
} catch {
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function evaluateLoopbackWriteRequest(request) {
|
|
1383
|
+
const method = request.method.toUpperCase();
|
|
1384
|
+
if (SAFE_METHODS.has(method)) return { allowed: true };
|
|
1385
|
+
if (JSON_BODY_METHODS.has(method) && mediaType(request.contentType) !== "application/json") {
|
|
1386
|
+
return { allowed: false, reason: "content-type", status: 415 };
|
|
1387
|
+
}
|
|
1388
|
+
if (request.fetchSite && !ALLOWED_FETCH_SITES.has(request.fetchSite)) {
|
|
1389
|
+
return { allowed: false, reason: "fetch-site", status: 403 };
|
|
1390
|
+
}
|
|
1391
|
+
if (request.origin && !isSameOrigin(request.origin, request.requestOrigin)) {
|
|
1392
|
+
return { allowed: false, reason: "origin", status: 403 };
|
|
1393
|
+
}
|
|
1394
|
+
return { allowed: true };
|
|
1395
|
+
}
|
|
1396
|
+
function loopbackWriteGuard() {
|
|
1397
|
+
return async (c, next) => {
|
|
1398
|
+
const url = new URL(c.req.url);
|
|
1399
|
+
const decision = evaluateLoopbackWriteRequest({
|
|
1400
|
+
method: c.req.method,
|
|
1401
|
+
contentType: c.req.header("Content-Type"),
|
|
1402
|
+
fetchSite: c.req.header("Sec-Fetch-Site"),
|
|
1403
|
+
origin: c.req.header("Origin"),
|
|
1404
|
+
requestOrigin: url.origin
|
|
1405
|
+
});
|
|
1406
|
+
if (!decision.allowed) {
|
|
1407
|
+
appLogger.warn("http.loopback_write.rejected", {
|
|
1408
|
+
method: c.req.method,
|
|
1409
|
+
path: url.pathname,
|
|
1410
|
+
reason: decision.reason
|
|
1411
|
+
});
|
|
1412
|
+
const error = decision.reason === "content-type" ? "Write requests require application/json" : "Cross-origin write request rejected";
|
|
1413
|
+
return c.json({ error }, decision.status);
|
|
1414
|
+
}
|
|
1415
|
+
await next();
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
|
|
904
1419
|
// src/remote-access.ts
|
|
905
1420
|
import { randomBytes, timingSafeEqual } from "crypto";
|
|
906
1421
|
import { readFileSync } from "fs";
|
|
907
|
-
import { isIP } from "net";
|
|
1422
|
+
import { isIP as isIP2 } from "net";
|
|
908
1423
|
var REMOTE_ACCESS_QUERY_PARAM = "access_token";
|
|
909
1424
|
var RemoteTransportError = class extends Error {
|
|
910
1425
|
};
|
|
@@ -935,6 +1450,13 @@ function resolveRemoteTransport(request) {
|
|
|
935
1450
|
if (request.trustProxy) return { kind: "trusted-proxy" };
|
|
936
1451
|
return isLoopbackHostname(request.hostname) ? { kind: "loopback" } : { kind: "plaintext" };
|
|
937
1452
|
}
|
|
1453
|
+
function resolveRemoteAccessPolicy(hostname, transport) {
|
|
1454
|
+
const bindCategory = isLoopbackHostname(hostname) ? "loopback" : "network";
|
|
1455
|
+
return {
|
|
1456
|
+
bindCategory,
|
|
1457
|
+
authenticationRequired: bindCategory === "network" || transport.kind !== "loopback"
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
938
1460
|
var FORWARDED_PROTO_HEADER = "X-Forwarded-Proto";
|
|
939
1461
|
function requireProxyTls() {
|
|
940
1462
|
return async (c, next) => {
|
|
@@ -951,7 +1473,7 @@ function createRemoteAccessToken() {
|
|
|
951
1473
|
function isLoopbackHostname(hostname) {
|
|
952
1474
|
const normalized = hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
953
1475
|
if (normalized === "localhost" || normalized === "::1") return true;
|
|
954
|
-
return
|
|
1476
|
+
return isIP2(normalized) === 4 && normalized.startsWith("127.");
|
|
955
1477
|
}
|
|
956
1478
|
function tokenMatches(actual, expected) {
|
|
957
1479
|
if (!actual) return false;
|
|
@@ -981,6 +1503,7 @@ function remoteAccessAuth(expectedToken) {
|
|
|
981
1503
|
|
|
982
1504
|
// src/server.ts
|
|
983
1505
|
var MAX_API_REQUEST_BYTES = 1024 * 1024;
|
|
1506
|
+
var SERVER_CLOSE_GRACE_MS = 1e3;
|
|
984
1507
|
function findWebDistPath() {
|
|
985
1508
|
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
986
1509
|
const packagedPath = resolve(__dirname2, "web");
|
|
@@ -1020,17 +1543,91 @@ function getListeningPort(server, fallback) {
|
|
|
1020
1543
|
const address = server.address();
|
|
1021
1544
|
return typeof address === "object" && address !== null ? address.port : fallback;
|
|
1022
1545
|
}
|
|
1546
|
+
async function closeHttpServer(server, port) {
|
|
1547
|
+
if (!server) return;
|
|
1548
|
+
let forceCloseTimer;
|
|
1549
|
+
const gracefulClose = new Promise((resolve4) => server.close(() => resolve4()));
|
|
1550
|
+
const forceClose = new Promise((resolve4) => {
|
|
1551
|
+
forceCloseTimer = setTimeout(() => {
|
|
1552
|
+
appLogger.warn("server.shutdown.force_close", {
|
|
1553
|
+
port,
|
|
1554
|
+
grace_ms: SERVER_CLOSE_GRACE_MS
|
|
1555
|
+
});
|
|
1556
|
+
if ("closeAllConnections" in server) server.closeAllConnections();
|
|
1557
|
+
resolve4();
|
|
1558
|
+
}, SERVER_CLOSE_GRACE_MS);
|
|
1559
|
+
forceCloseTimer.unref();
|
|
1560
|
+
});
|
|
1561
|
+
try {
|
|
1562
|
+
await Promise.race([gracefulClose, forceClose]);
|
|
1563
|
+
} finally {
|
|
1564
|
+
if (forceCloseTimer) clearTimeout(forceCloseTimer);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1023
1567
|
async function createServer(port, store, options = {}) {
|
|
1024
1568
|
const app = new Hono2();
|
|
1025
1569
|
const hostname = options.hostname ?? "127.0.0.1";
|
|
1026
|
-
const
|
|
1027
|
-
const
|
|
1028
|
-
const remoteAccessToken =
|
|
1029
|
-
|
|
1570
|
+
const transport = options.transport ?? resolveRemoteTransport({ hostname });
|
|
1571
|
+
const accessPolicy = resolveRemoteAccessPolicy(hostname, transport);
|
|
1572
|
+
const remoteAccessToken = accessPolicy.authenticationRequired ? options.remoteAccessToken ?? (options.remoteAccess ? createRemoteAccessToken() : null) : null;
|
|
1573
|
+
const loopbackAuthorityEnabled = !accessPolicy.authenticationRequired;
|
|
1574
|
+
const shutdownController = new AbortController();
|
|
1575
|
+
let actualPort = null;
|
|
1576
|
+
appLogger.info("server.access_policy", {
|
|
1577
|
+
bind_category: accessPolicy.bindCategory,
|
|
1578
|
+
transport: transport.kind,
|
|
1579
|
+
authentication: remoteAccessToken ? "token" : "none",
|
|
1580
|
+
loopback_authority: loopbackAuthorityEnabled ? "enabled" : "disabled"
|
|
1581
|
+
});
|
|
1582
|
+
if (accessPolicy.authenticationRequired && !remoteAccessToken) {
|
|
1030
1583
|
throw new Error(
|
|
1031
1584
|
`Refusing to expose CodeSesh on ${hostname} without authentication. Add --remote-access to continue.`
|
|
1032
1585
|
);
|
|
1033
1586
|
}
|
|
1587
|
+
app.use(
|
|
1588
|
+
"*",
|
|
1589
|
+
secureHeaders({
|
|
1590
|
+
contentSecurityPolicy: {
|
|
1591
|
+
defaultSrc: ["'self'"],
|
|
1592
|
+
baseUri: ["'none'"],
|
|
1593
|
+
connectSrc: ["'self'"],
|
|
1594
|
+
fontSrc: ["'self'", "data:"],
|
|
1595
|
+
formAction: ["'self'"],
|
|
1596
|
+
frameAncestors: ["'none'"],
|
|
1597
|
+
imgSrc: ["'self'", "data:"],
|
|
1598
|
+
objectSrc: ["'none'"],
|
|
1599
|
+
scriptSrc: ["'self'"],
|
|
1600
|
+
scriptSrcAttr: ["'none'"],
|
|
1601
|
+
styleSrc: ["'self'", "'unsafe-inline'"]
|
|
1602
|
+
},
|
|
1603
|
+
crossOriginOpenerPolicy: false,
|
|
1604
|
+
crossOriginResourcePolicy: false,
|
|
1605
|
+
originAgentCluster: false,
|
|
1606
|
+
referrerPolicy: "no-referrer",
|
|
1607
|
+
strictTransportSecurity: false,
|
|
1608
|
+
xContentTypeOptions: "nosniff",
|
|
1609
|
+
xDnsPrefetchControl: false,
|
|
1610
|
+
xDownloadOptions: false,
|
|
1611
|
+
xFrameOptions: "DENY",
|
|
1612
|
+
xPermittedCrossDomainPolicies: false,
|
|
1613
|
+
xXssProtection: false
|
|
1614
|
+
})
|
|
1615
|
+
);
|
|
1616
|
+
if (loopbackAuthorityEnabled) {
|
|
1617
|
+
app.use("*", async (c, next) => {
|
|
1618
|
+
const decision = validateLoopbackAuthority(c.env.incoming.rawHeaders, hostname, actualPort);
|
|
1619
|
+
if (!decision.allowed) {
|
|
1620
|
+
appLogger.warn("http.loopback_authority.rejected", {
|
|
1621
|
+
method: c.req.method,
|
|
1622
|
+
path: new URL(c.req.url).pathname,
|
|
1623
|
+
reason: decision.reason,
|
|
1624
|
+
authority: decision.authority
|
|
1625
|
+
});
|
|
1626
|
+
return c.json({ error: "Loopback request authority rejected" }, 403);
|
|
1627
|
+
}
|
|
1628
|
+
await next();
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1034
1631
|
app.use("*", async (c, next) => {
|
|
1035
1632
|
const startedAt = performance.now();
|
|
1036
1633
|
let thrown;
|
|
@@ -1057,6 +1654,7 @@ async function createServer(port, store, options = {}) {
|
|
|
1057
1654
|
if (remoteAccessToken) {
|
|
1058
1655
|
app.use("/api/*", remoteAccessAuth(remoteAccessToken));
|
|
1059
1656
|
}
|
|
1657
|
+
app.use("/api/*", loopbackWriteGuard());
|
|
1060
1658
|
app.use(
|
|
1061
1659
|
"/api/*",
|
|
1062
1660
|
bodyLimit({
|
|
@@ -1068,7 +1666,8 @@ async function createServer(port, store, options = {}) {
|
|
|
1068
1666
|
const routeOptions = {
|
|
1069
1667
|
defaultSessionFrom: options.defaultSessionFrom,
|
|
1070
1668
|
defaultSessionTo: options.defaultSessionTo,
|
|
1071
|
-
defaultSessionDays: options.defaultSessionDays
|
|
1669
|
+
defaultSessionDays: options.defaultSessionDays,
|
|
1670
|
+
shutdownSignal: shutdownController.signal
|
|
1072
1671
|
};
|
|
1073
1672
|
app.route("/api", createApiRoutes(store, store, routeOptions));
|
|
1074
1673
|
const webDistPath = options.webDistPath ?? findWebDistPath();
|
|
@@ -1078,7 +1677,6 @@ async function createServer(port, store, options = {}) {
|
|
|
1078
1677
|
}
|
|
1079
1678
|
const attempts = Math.max(1, options.portFallbackAttempts ?? 1);
|
|
1080
1679
|
let server = null;
|
|
1081
|
-
let actualPort = port;
|
|
1082
1680
|
for (let offset = 0; offset < attempts; offset += 1) {
|
|
1083
1681
|
const candidatePort = port + offset;
|
|
1084
1682
|
server = serve({
|
|
@@ -1110,7 +1708,7 @@ async function createServer(port, store, options = {}) {
|
|
|
1110
1708
|
}
|
|
1111
1709
|
}
|
|
1112
1710
|
const scheme = transport.kind === "tls" ? "https" : "http";
|
|
1113
|
-
const baseUrl =
|
|
1711
|
+
const baseUrl = accessPolicy.bindCategory === "loopback" ? `${scheme}://localhost:${actualPort}` : `${scheme}://${hostname}:${actualPort}`;
|
|
1114
1712
|
const url = remoteAccessToken ? `${baseUrl}/?${REMOTE_ACCESS_QUERY_PARAM}=${encodeURIComponent(remoteAccessToken)}` : baseUrl;
|
|
1115
1713
|
appLogger.info("server.listen", {
|
|
1116
1714
|
port: actualPort,
|
|
@@ -1119,7 +1717,7 @@ async function createServer(port, store, options = {}) {
|
|
|
1119
1717
|
remote_access: Boolean(remoteAccessToken),
|
|
1120
1718
|
transport: transport.kind
|
|
1121
1719
|
});
|
|
1122
|
-
if (
|
|
1720
|
+
if (accessPolicy.authenticationRequired) {
|
|
1123
1721
|
appLogger.warn("server.listen.remote_access", {
|
|
1124
1722
|
hostname,
|
|
1125
1723
|
port: actualPort,
|
|
@@ -1133,6 +1731,14 @@ async function createServer(port, store, options = {}) {
|
|
|
1133
1731
|
console.warn(
|
|
1134
1732
|
`\u26A0 \u4F20\u8F93\u672A\u52A0\u5BC6\u3002\u8BBF\u95EE\u4EE4\u724C\u4E0E\u5B8C\u6574\u4F1A\u8BDD\u5185\u5BB9\u4EE5\u660E\u6587\u7ECF\u8FC7\u7F51\u7EDC\uFF0CURL \u4E2D\u7684\u4EE4\u724C\u8FD8\u53EF\u80FD\u5199\u5165\u4EE3\u7406\u8BBF\u95EE\u65E5\u5FD7\u3002
|
|
1135
1733
|
\u4F7F\u7528 --tls-cert/--tls-key \u76F4\u63A5\u542F\u7528 TLS\uFF0C\u6216\u5728\u53D7\u4FE1\u53CD\u5411\u4EE3\u7406\u540E\u4F7F\u7528 --trust-proxy\u3002
|
|
1734
|
+
`
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
if (transport.kind === "trusted-proxy") {
|
|
1738
|
+
appLogger.warn("server.listen.proxy_logging", { hostname, port: actualPort });
|
|
1739
|
+
console.warn(
|
|
1740
|
+
`\u26A0 URL \u4E2D\u7684\u8BBF\u95EE\u4EE4\u724C\u4F1A\u7528\u4E8E SSE \u91CD\u8FDE\uFF0C\u5E76\u53EF\u80FD\u5199\u5165\u53D7\u4FE1\u53CD\u5411\u4EE3\u7406\u7684\u8BBF\u95EE\u65E5\u5FD7\u3002
|
|
1741
|
+
\u8BF7\u5728\u4EE3\u7406\u914D\u7F6E\u4E2D\u5173\u95ED\u67E5\u8BE2\u53C2\u6570\u65E5\u5FD7\uFF0C\u6216\u5BF9 access_token \u53C2\u6570\u8131\u654F\u3002
|
|
1136
1742
|
`
|
|
1137
1743
|
);
|
|
1138
1744
|
}
|
|
@@ -1141,14 +1747,15 @@ async function createServer(port, store, options = {}) {
|
|
|
1141
1747
|
url,
|
|
1142
1748
|
shutdown: async () => {
|
|
1143
1749
|
appLogger.info("server.shutdown", { port: actualPort });
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
server.
|
|
1150
|
-
|
|
1151
|
-
|
|
1750
|
+
shutdownController.abort();
|
|
1751
|
+
try {
|
|
1752
|
+
appLogger.info("server.shutdown.phase", { phase: "http-closing", port: actualPort });
|
|
1753
|
+
await closeHttpServer(server, actualPort);
|
|
1754
|
+
} finally {
|
|
1755
|
+
appLogger.info("server.shutdown.phase", { phase: "store-closing", port: actualPort });
|
|
1756
|
+
await store.shutdown();
|
|
1757
|
+
}
|
|
1758
|
+
appLogger.info("server.shutdown.phase", { phase: "complete", port: actualPort });
|
|
1152
1759
|
}
|
|
1153
1760
|
};
|
|
1154
1761
|
}
|
|
@@ -1307,12 +1914,88 @@ var AgentOperationScheduler = class {
|
|
|
1307
1914
|
}
|
|
1308
1915
|
};
|
|
1309
1916
|
|
|
1917
|
+
// src/backfill-lifecycle.ts
|
|
1918
|
+
var BackfillLifecycle = class {
|
|
1919
|
+
attempts = /* @__PURE__ */ new Map();
|
|
1920
|
+
nextAttemptId = 1;
|
|
1921
|
+
enqueue(agentName) {
|
|
1922
|
+
const current = this.attempts.get(agentName);
|
|
1923
|
+
if (current?.status === "queued" || current?.status === "running") return null;
|
|
1924
|
+
const attempt = {
|
|
1925
|
+
agentName,
|
|
1926
|
+
attemptId: this.nextAttemptId++,
|
|
1927
|
+
status: "queued"
|
|
1928
|
+
};
|
|
1929
|
+
this.attempts.set(agentName, attempt);
|
|
1930
|
+
return attempt;
|
|
1931
|
+
}
|
|
1932
|
+
startNext() {
|
|
1933
|
+
if ([...this.attempts.values()].some((attempt) => attempt.status === "running")) return null;
|
|
1934
|
+
const queued = [...this.attempts.values()].filter((attempt) => attempt.status === "queued").sort((left, right) => left.attemptId - right.attemptId)[0];
|
|
1935
|
+
if (!queued) return null;
|
|
1936
|
+
const running = { ...queued, status: "running" };
|
|
1937
|
+
this.attempts.set(running.agentName, running);
|
|
1938
|
+
return running;
|
|
1939
|
+
}
|
|
1940
|
+
updateProgress(attempt, progress) {
|
|
1941
|
+
const current = this.matchingRunningAttempt(attempt);
|
|
1942
|
+
if (!current) return false;
|
|
1943
|
+
this.attempts.set(attempt.agentName, {
|
|
1944
|
+
...current,
|
|
1945
|
+
progress: { ...current.progress, ...progress }
|
|
1946
|
+
});
|
|
1947
|
+
return true;
|
|
1948
|
+
}
|
|
1949
|
+
complete(attempt, status) {
|
|
1950
|
+
if (!this.matchingRunningAttempt(attempt)) return false;
|
|
1951
|
+
this.attempts.set(attempt.agentName, { ...attempt, status });
|
|
1952
|
+
return true;
|
|
1953
|
+
}
|
|
1954
|
+
cancelAll() {
|
|
1955
|
+
for (const [agentName, attempt] of this.attempts) {
|
|
1956
|
+
if (attempt.status === "queued" || attempt.status === "running") {
|
|
1957
|
+
this.attempts.set(agentName, { ...attempt, status: "cancelled" });
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
runningAttempt() {
|
|
1962
|
+
const running = [...this.attempts.values()].find((attempt) => attempt.status === "running");
|
|
1963
|
+
return running ?? null;
|
|
1964
|
+
}
|
|
1965
|
+
stateFor(agentName) {
|
|
1966
|
+
const state = this.attempts.get(agentName);
|
|
1967
|
+
if (!state) return void 0;
|
|
1968
|
+
return state.status === "running" && state.progress ? { ...state, progress: { ...state.progress } } : { ...state };
|
|
1969
|
+
}
|
|
1970
|
+
status() {
|
|
1971
|
+
const states = [...this.attempts.values()];
|
|
1972
|
+
const queued = states.filter((attempt) => attempt.status === "queued").sort((left, right) => left.attemptId - right.attemptId);
|
|
1973
|
+
const running = states.find((attempt) => attempt.status === "running");
|
|
1974
|
+
const status = {
|
|
1975
|
+
active: running != null || queued.length > 0,
|
|
1976
|
+
pendingAgents: queued.map((attempt) => attempt.agentName),
|
|
1977
|
+
completedAgents: states.filter((attempt) => attempt.status === "committed").map((attempt) => attempt.agentName),
|
|
1978
|
+
failedAgents: states.filter((attempt) => attempt.status === "failed").map((attempt) => attempt.agentName)
|
|
1979
|
+
};
|
|
1980
|
+
if (running) {
|
|
1981
|
+
status.currentAgent = running.agentName;
|
|
1982
|
+
if (running.progress) status.progress = { ...running.progress };
|
|
1983
|
+
}
|
|
1984
|
+
return status;
|
|
1985
|
+
}
|
|
1986
|
+
matchingRunningAttempt(attempt) {
|
|
1987
|
+
const current = this.attempts.get(attempt.agentName);
|
|
1988
|
+
return current?.status === "running" && current.attemptId === attempt.attemptId ? current : null;
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1310
1992
|
// src/live-session-index.ts
|
|
1311
1993
|
var LiveSessionIndex = class {
|
|
1312
1994
|
agents = [];
|
|
1313
1995
|
agentsByName = /* @__PURE__ */ new Map();
|
|
1314
1996
|
byAgent = {};
|
|
1315
1997
|
sessions = [];
|
|
1998
|
+
scanFailures = {};
|
|
1316
1999
|
signatureCaches = /* @__PURE__ */ new Map();
|
|
1317
2000
|
initialize(snapshot, options = {}) {
|
|
1318
2001
|
const agentMap = /* @__PURE__ */ new Map();
|
|
@@ -1324,8 +2007,12 @@ var LiveSessionIndex = class {
|
|
|
1324
2007
|
(agent) => !options.allowedAgents || options.allowedAgents.has(agent.name.toLowerCase())
|
|
1325
2008
|
);
|
|
1326
2009
|
this.agentsByName = new Map(this.agents.map((agent) => [agent.name, agent]));
|
|
2010
|
+
this.scanFailures = { ...snapshot.scanFailures };
|
|
1327
2011
|
this.byAgent = Object.fromEntries(
|
|
1328
|
-
this.agents.
|
|
2012
|
+
this.agents.flatMap((agent) => {
|
|
2013
|
+
if (this.scanFailures[agent.name] && !(agent.name in snapshot.byAgent)) return [];
|
|
2014
|
+
return [[agent.name, sortSessions(snapshot.byAgent[agent.name] ?? [])]];
|
|
2015
|
+
})
|
|
1329
2016
|
);
|
|
1330
2017
|
this.sessions = mergeSortedSessions(Object.values(this.byAgent));
|
|
1331
2018
|
this.signatureCaches.clear();
|
|
@@ -1334,14 +2021,17 @@ var LiveSessionIndex = class {
|
|
|
1334
2021
|
return {
|
|
1335
2022
|
agents: this.agents,
|
|
1336
2023
|
byAgent: this.byAgent,
|
|
1337
|
-
sessions: this.sessions
|
|
2024
|
+
sessions: this.sessions,
|
|
2025
|
+
...Object.keys(this.scanFailures).length > 0 ? { scanFailures: this.scanFailures } : {}
|
|
1338
2026
|
};
|
|
1339
2027
|
}
|
|
1340
2028
|
findAgent(agentName) {
|
|
1341
2029
|
return this.agentsByName.get(agentName);
|
|
1342
2030
|
}
|
|
1343
2031
|
commitAgentSessions(agentName, nextSessions, candidateChangedIds = []) {
|
|
2032
|
+
delete this.scanFailures[agentName];
|
|
1344
2033
|
const previousSessions = this.byAgent[agentName] ?? [];
|
|
2034
|
+
const previousGlobalSessions = this.sessions;
|
|
1345
2035
|
const signatureCache = this.signatureCache(agentName);
|
|
1346
2036
|
const { changes, removedSessionIds, counts } = computeSessionDiff(
|
|
1347
2037
|
previousSessions,
|
|
@@ -1354,19 +2044,34 @@ var LiveSessionIndex = class {
|
|
|
1354
2044
|
this.byAgent[agentName] = sortSessions(nextSessions);
|
|
1355
2045
|
this.sessions = mergeSortedSessions(Object.values(this.byAgent));
|
|
1356
2046
|
if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) return null;
|
|
2047
|
+
const changedSessionHeads = changes.map(({ session }) => ({
|
|
2048
|
+
reference: { agentName, sessionId: session.id },
|
|
2049
|
+
session
|
|
2050
|
+
}));
|
|
2051
|
+
const previousSessionIds = new Set(previousSessions.map((session) => session.id));
|
|
2052
|
+
const newSessionRefs = changedSessionHeads.flatMap(
|
|
2053
|
+
({ reference }) => previousSessionIds.has(reference.sessionId) ? [] : [reference]
|
|
2054
|
+
);
|
|
2055
|
+
const removedSessionRefs = removedSessionIds.map((sessionId) => ({ agentName, sessionId }));
|
|
2056
|
+
const projectionContext = createSessionProjectionContext(
|
|
2057
|
+
previousGlobalSessions,
|
|
2058
|
+
this.sessions,
|
|
2059
|
+
changedSessionHeads,
|
|
2060
|
+
removedSessionRefs
|
|
2061
|
+
);
|
|
1357
2062
|
return {
|
|
1358
2063
|
type: "sessions-updated",
|
|
1359
2064
|
changedAgents: [agentName],
|
|
1360
2065
|
newSessions: counts.new,
|
|
2066
|
+
newSessionRefs,
|
|
1361
2067
|
updatedSessions: counts.updated,
|
|
1362
2068
|
removedSessions: counts.removed,
|
|
1363
2069
|
totalSessions: this.sessions.length,
|
|
1364
2070
|
timestamp: Date.now(),
|
|
1365
|
-
changedSessionHeads
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
|
|
2071
|
+
changedSessionHeads,
|
|
2072
|
+
projectionRelatedSessionHeads: projectionContext.relatedSessionHeads,
|
|
2073
|
+
projectionSessionOrder: projectionContext.sessionOrder,
|
|
2074
|
+
removedSessionRefs
|
|
1370
2075
|
};
|
|
1371
2076
|
}
|
|
1372
2077
|
signatureCache(agentName) {
|
|
@@ -1378,11 +2083,79 @@ var LiveSessionIndex = class {
|
|
|
1378
2083
|
}
|
|
1379
2084
|
};
|
|
1380
2085
|
|
|
2086
|
+
// src/latest-value-throttle.ts
|
|
2087
|
+
var LatestValueThrottle = class {
|
|
2088
|
+
constructor(intervalMs, emit) {
|
|
2089
|
+
this.intervalMs = intervalMs;
|
|
2090
|
+
this.emit = emit;
|
|
2091
|
+
}
|
|
2092
|
+
intervalMs;
|
|
2093
|
+
emit;
|
|
2094
|
+
timer = null;
|
|
2095
|
+
phase = null;
|
|
2096
|
+
pending;
|
|
2097
|
+
hasPending = false;
|
|
2098
|
+
push(value, phase) {
|
|
2099
|
+
if (this.phase !== phase) {
|
|
2100
|
+
this.flush();
|
|
2101
|
+
this.phase = phase;
|
|
2102
|
+
this.emit(value);
|
|
2103
|
+
this.schedule();
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
if (this.timer) {
|
|
2107
|
+
this.pending = value;
|
|
2108
|
+
this.hasPending = true;
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
this.emit(value);
|
|
2112
|
+
this.schedule();
|
|
2113
|
+
}
|
|
2114
|
+
flush() {
|
|
2115
|
+
this.clearTimer();
|
|
2116
|
+
if (!this.hasPending) return;
|
|
2117
|
+
const pending = this.pending;
|
|
2118
|
+
this.pending = void 0;
|
|
2119
|
+
this.hasPending = false;
|
|
2120
|
+
this.emit(pending);
|
|
2121
|
+
}
|
|
2122
|
+
cancel() {
|
|
2123
|
+
this.dropPending();
|
|
2124
|
+
this.phase = null;
|
|
2125
|
+
}
|
|
2126
|
+
schedule() {
|
|
2127
|
+
this.timer = setTimeout(() => {
|
|
2128
|
+
this.timer = null;
|
|
2129
|
+
if (!this.hasPending) return;
|
|
2130
|
+
const pending = this.pending;
|
|
2131
|
+
this.pending = void 0;
|
|
2132
|
+
this.hasPending = false;
|
|
2133
|
+
this.emit(pending);
|
|
2134
|
+
this.schedule();
|
|
2135
|
+
}, this.intervalMs);
|
|
2136
|
+
}
|
|
2137
|
+
clearTimer() {
|
|
2138
|
+
if (!this.timer) return;
|
|
2139
|
+
clearTimeout(this.timer);
|
|
2140
|
+
this.timer = null;
|
|
2141
|
+
}
|
|
2142
|
+
dropPending() {
|
|
2143
|
+
this.clearTimer();
|
|
2144
|
+
this.pending = void 0;
|
|
2145
|
+
this.hasPending = false;
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
|
|
1381
2149
|
// src/search-index-job-runner.ts
|
|
1382
2150
|
import { existsSync as existsSync2 } from "fs";
|
|
1383
2151
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1384
2152
|
import { Worker } from "worker_threads";
|
|
1385
2153
|
|
|
2154
|
+
// src/errors.ts
|
|
2155
|
+
function toError(value) {
|
|
2156
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
2157
|
+
}
|
|
2158
|
+
|
|
1386
2159
|
// src/pending-search-index-jobs.ts
|
|
1387
2160
|
var PendingSearchIndexJobBatch = class {
|
|
1388
2161
|
constructor(id, context, jobs, waiter) {
|
|
@@ -1395,6 +2168,7 @@ var PendingSearchIndexJobBatch = class {
|
|
|
1395
2168
|
jobsByAgent = /* @__PURE__ */ new Map();
|
|
1396
2169
|
waiters = [];
|
|
1397
2170
|
settled = false;
|
|
2171
|
+
started = false;
|
|
1398
2172
|
get jobs() {
|
|
1399
2173
|
const jobs = [];
|
|
1400
2174
|
for (const pending of this.jobsByAgent.values()) {
|
|
@@ -1411,6 +2185,17 @@ var PendingSearchIndexJobBatch = class {
|
|
|
1411
2185
|
}
|
|
1412
2186
|
return count;
|
|
1413
2187
|
}
|
|
2188
|
+
start(onError) {
|
|
2189
|
+
if (this.started) return;
|
|
2190
|
+
this.started = true;
|
|
2191
|
+
for (const waiter of this.waiters) {
|
|
2192
|
+
try {
|
|
2193
|
+
waiter.onStarted?.();
|
|
2194
|
+
} catch (error) {
|
|
2195
|
+
onError(error);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
1414
2199
|
merge(context, jobs, waiter) {
|
|
1415
2200
|
this.context = context;
|
|
1416
2201
|
this.waiters.push(waiter);
|
|
@@ -1440,6 +2225,7 @@ var PendingSearchIndexJobBatch = class {
|
|
|
1440
2225
|
};
|
|
1441
2226
|
var PendingSearchIndexJobs = class {
|
|
1442
2227
|
pendingBatch = null;
|
|
2228
|
+
batches = /* @__PURE__ */ new WeakSet();
|
|
1443
2229
|
get batchCount() {
|
|
1444
2230
|
return this.pendingBatch ? 1 : 0;
|
|
1445
2231
|
}
|
|
@@ -1449,14 +2235,15 @@ var PendingSearchIndexJobs = class {
|
|
|
1449
2235
|
get changeCount() {
|
|
1450
2236
|
return this.pendingBatch?.changeCount ?? 0;
|
|
1451
2237
|
}
|
|
1452
|
-
enqueue(id, context, jobs) {
|
|
2238
|
+
enqueue(id, context, jobs, onStarted) {
|
|
1453
2239
|
if (jobs.length === 0) return Promise.resolve();
|
|
1454
2240
|
return new Promise((resolve4, reject) => {
|
|
1455
|
-
const waiter = { resolve: resolve4, reject };
|
|
2241
|
+
const waiter = { resolve: resolve4, reject, onStarted };
|
|
1456
2242
|
if (this.pendingBatch) {
|
|
1457
2243
|
this.pendingBatch.merge(context, jobs, waiter);
|
|
1458
2244
|
} else {
|
|
1459
2245
|
this.pendingBatch = new PendingSearchIndexJobBatch(id, context, jobs, waiter);
|
|
2246
|
+
this.batches.add(this.pendingBatch);
|
|
1460
2247
|
}
|
|
1461
2248
|
});
|
|
1462
2249
|
}
|
|
@@ -1466,7 +2253,9 @@ var PendingSearchIndexJobs = class {
|
|
|
1466
2253
|
return batch;
|
|
1467
2254
|
}
|
|
1468
2255
|
settle(batch, error) {
|
|
1469
|
-
|
|
2256
|
+
if (!(batch instanceof PendingSearchIndexJobBatch) || !this.batches.has(batch)) return false;
|
|
2257
|
+
this.batches.delete(batch);
|
|
2258
|
+
return batch.settle(error);
|
|
1470
2259
|
}
|
|
1471
2260
|
rejectAll(error) {
|
|
1472
2261
|
const batch = this.take();
|
|
@@ -1475,8 +2264,10 @@ var PendingSearchIndexJobs = class {
|
|
|
1475
2264
|
};
|
|
1476
2265
|
function createPendingChanges(job) {
|
|
1477
2266
|
return {
|
|
2267
|
+
kind: job.kind,
|
|
1478
2268
|
context: job.context,
|
|
1479
2269
|
agentName: job.agentName,
|
|
2270
|
+
...job.kind === "changes" && job.publicationId ? { publicationId: job.publicationId } : {},
|
|
1480
2271
|
changesBySessionId: /* @__PURE__ */ new Map(),
|
|
1481
2272
|
removedSessionIds: /* @__PURE__ */ new Set(),
|
|
1482
2273
|
meta: {},
|
|
@@ -1484,7 +2275,11 @@ function createPendingChanges(job) {
|
|
|
1484
2275
|
};
|
|
1485
2276
|
}
|
|
1486
2277
|
function mergeChanges(pending, job) {
|
|
2278
|
+
pending.kind = job.kind;
|
|
1487
2279
|
pending.context = job.context;
|
|
2280
|
+
if (job.kind === "changes" && job.publicationId) {
|
|
2281
|
+
pending.publicationId = job.publicationId;
|
|
2282
|
+
}
|
|
1488
2283
|
pending.searchIndexOptions = mergeSearchIndexOptions(
|
|
1489
2284
|
pending.searchIndexOptions,
|
|
1490
2285
|
job.searchIndexOptions
|
|
@@ -1511,8 +2306,7 @@ function mergeSearchIndexOptions(current, incoming) {
|
|
|
1511
2306
|
return { ...current, ...incoming };
|
|
1512
2307
|
}
|
|
1513
2308
|
function changesJobFromPending(pending) {
|
|
1514
|
-
|
|
1515
|
-
kind: "changes",
|
|
2309
|
+
const common = {
|
|
1516
2310
|
context: pending.context,
|
|
1517
2311
|
agentName: pending.agentName,
|
|
1518
2312
|
changes: [...pending.changesBySessionId.values()],
|
|
@@ -1520,6 +2314,11 @@ function changesJobFromPending(pending) {
|
|
|
1520
2314
|
meta: pending.meta,
|
|
1521
2315
|
...pending.searchIndexOptions ? { searchIndexOptions: pending.searchIndexOptions } : {}
|
|
1522
2316
|
};
|
|
2317
|
+
return pending.kind === "changes" ? {
|
|
2318
|
+
kind: "changes",
|
|
2319
|
+
...common,
|
|
2320
|
+
...pending.publicationId ? { publicationId: pending.publicationId } : {}
|
|
2321
|
+
} : { kind: "maintenance", ...common };
|
|
1523
2322
|
}
|
|
1524
2323
|
|
|
1525
2324
|
// src/search-index-job-runner.ts
|
|
@@ -1529,13 +2328,20 @@ var SearchIndexJobRunner = class {
|
|
|
1529
2328
|
activeBatch = null;
|
|
1530
2329
|
nextBatchId = 1;
|
|
1531
2330
|
pendingJobs = new PendingSearchIndexJobs();
|
|
2331
|
+
pendingMaintenanceJobs = new PendingSearchIndexJobs();
|
|
1532
2332
|
isShuttingDown = false;
|
|
1533
|
-
enqueue(context, jobs) {
|
|
2333
|
+
enqueue(context, jobs, onStarted) {
|
|
2334
|
+
return this.enqueueIn(this.pendingJobs, context, jobs, onStarted);
|
|
2335
|
+
}
|
|
2336
|
+
enqueueMaintenance(context, jobs) {
|
|
2337
|
+
return this.enqueueIn(this.pendingMaintenanceJobs, context, jobs);
|
|
2338
|
+
}
|
|
2339
|
+
enqueueIn(queue, context, jobs, onStarted) {
|
|
1534
2340
|
if (jobs.length === 0) return Promise.resolve();
|
|
1535
2341
|
if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE));
|
|
1536
2342
|
const batchId = this.nextBatchId++;
|
|
1537
|
-
const completion =
|
|
1538
|
-
if (this.
|
|
2343
|
+
const completion = queue.enqueue(batchId, context, jobs, onStarted);
|
|
2344
|
+
if (this.activeBatch) {
|
|
1539
2345
|
const snapshot = this.snapshot();
|
|
1540
2346
|
appLogger.debug("search_index.worker_queued", {
|
|
1541
2347
|
batch_id: batchId,
|
|
@@ -1555,7 +2361,9 @@ var SearchIndexJobRunner = class {
|
|
|
1555
2361
|
activeBatchId: this.activeBatch?.id,
|
|
1556
2362
|
pendingBatches: this.pendingJobs.batchCount,
|
|
1557
2363
|
pendingJobs: this.pendingJobs.jobCount,
|
|
1558
|
-
pendingChanges: this.pendingJobs.changeCount
|
|
2364
|
+
pendingChanges: this.pendingJobs.changeCount,
|
|
2365
|
+
pendingMaintenanceBatches: this.pendingMaintenanceJobs.batchCount,
|
|
2366
|
+
pendingMaintenanceJobs: this.pendingMaintenanceJobs.jobCount
|
|
1559
2367
|
};
|
|
1560
2368
|
}
|
|
1561
2369
|
async shutdown() {
|
|
@@ -1567,17 +2375,25 @@ var SearchIndexJobRunner = class {
|
|
|
1567
2375
|
const shutdownError = new Error(SHUTDOWN_ERROR_MESSAGE);
|
|
1568
2376
|
if (activeBatch) this.settle(activeBatch, shutdownError);
|
|
1569
2377
|
this.pendingJobs.rejectAll(shutdownError);
|
|
2378
|
+
this.pendingMaintenanceJobs.rejectAll(shutdownError);
|
|
1570
2379
|
if (worker) await worker.terminate();
|
|
1571
2380
|
}
|
|
1572
2381
|
startNextBatch() {
|
|
1573
|
-
if (this.isShuttingDown || this.
|
|
1574
|
-
const batch = this.pendingJobs.take();
|
|
2382
|
+
if (this.isShuttingDown || this.activeBatch) return;
|
|
2383
|
+
const batch = this.pendingJobs.take() ?? this.pendingMaintenanceJobs.take();
|
|
1575
2384
|
if (!batch) return;
|
|
1576
2385
|
appLogger.info("search_index.worker_dequeued", {
|
|
1577
2386
|
batch_id: batch.id,
|
|
1578
2387
|
context: batch.context,
|
|
1579
2388
|
pending_batches: this.pendingJobs.batchCount
|
|
1580
2389
|
});
|
|
2390
|
+
batch.start((error) => {
|
|
2391
|
+
appLogger.error("search_index.worker_start_listener_failed", {
|
|
2392
|
+
batch_id: batch.id,
|
|
2393
|
+
context: batch.context,
|
|
2394
|
+
error: toError(error)
|
|
2395
|
+
});
|
|
2396
|
+
});
|
|
1581
2397
|
this.startBatch(batch);
|
|
1582
2398
|
}
|
|
1583
2399
|
startBatch(batch) {
|
|
@@ -1585,10 +2401,27 @@ var SearchIndexJobRunner = class {
|
|
|
1585
2401
|
this.settle(batch, new Error(SHUTDOWN_ERROR_MESSAGE));
|
|
1586
2402
|
return;
|
|
1587
2403
|
}
|
|
2404
|
+
const request = {
|
|
2405
|
+
type: "run",
|
|
2406
|
+
pricingGenerationId: getPricingGeneration().id,
|
|
2407
|
+
context: batch.context,
|
|
2408
|
+
jobs: batch.jobs
|
|
2409
|
+
};
|
|
2410
|
+
const existingWorker = this.worker;
|
|
2411
|
+
if (existingWorker) {
|
|
2412
|
+
this.activeBatch = batch;
|
|
2413
|
+
try {
|
|
2414
|
+
existingWorker.postMessage(request);
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
this.invalidateWorker(existingWorker, toError(error));
|
|
2417
|
+
}
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
1588
2420
|
const workerUrl = this.workerUrl();
|
|
1589
2421
|
if (!workerUrl) {
|
|
1590
2422
|
appLogger.warn("search_index.worker_missing", { context: batch.context });
|
|
1591
|
-
this.settle(batch);
|
|
2423
|
+
this.settle(batch, new Error("Search index worker is unavailable"));
|
|
2424
|
+
this.startNextBatch();
|
|
1592
2425
|
return;
|
|
1593
2426
|
}
|
|
1594
2427
|
appLogger.info("search_index.worker_started", {
|
|
@@ -1596,33 +2429,37 @@ var SearchIndexJobRunner = class {
|
|
|
1596
2429
|
context: batch.context,
|
|
1597
2430
|
jobs: batch.jobs.length
|
|
1598
2431
|
});
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
});
|
|
2432
|
+
let worker;
|
|
2433
|
+
try {
|
|
2434
|
+
worker = new Worker(workerUrl, { workerData: request });
|
|
2435
|
+
} catch (error) {
|
|
2436
|
+
this.settle(batch, toError(error));
|
|
2437
|
+
this.startNextBatch();
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
1608
2440
|
worker.unref();
|
|
1609
2441
|
this.worker = worker;
|
|
1610
2442
|
this.activeBatch = batch;
|
|
1611
2443
|
worker.on("message", (message) => {
|
|
2444
|
+
if (appLogger.consumeWorkerMessage(message)) return;
|
|
2445
|
+
if (this.worker !== worker) return;
|
|
2446
|
+
const activeBatch = this.activeBatch;
|
|
2447
|
+
if (!activeBatch) return;
|
|
1612
2448
|
if (message.type === "sync-result") {
|
|
1613
2449
|
logSearchIndexSync(message.context, message.result);
|
|
1614
2450
|
return;
|
|
1615
2451
|
}
|
|
1616
2452
|
if (message.type === "persist-failed") {
|
|
1617
2453
|
appLogger.error("search_index.persist_failed", {
|
|
1618
|
-
batch_id:
|
|
2454
|
+
batch_id: activeBatch.id,
|
|
1619
2455
|
context: message.context,
|
|
1620
2456
|
stage: message.stage,
|
|
2457
|
+
publication_id: message.publicationId,
|
|
1621
2458
|
agent: message.agentName,
|
|
1622
2459
|
sessions: message.sessions
|
|
1623
2460
|
});
|
|
1624
|
-
this.
|
|
1625
|
-
|
|
2461
|
+
this.finishBatch(
|
|
2462
|
+
activeBatch,
|
|
1626
2463
|
new Error(
|
|
1627
2464
|
`Search index worker failed to persist ${message.stage} for ${message.agentName}`
|
|
1628
2465
|
)
|
|
@@ -1634,30 +2471,56 @@ var SearchIndexJobRunner = class {
|
|
|
1634
2471
|
duration_ms: Math.round(message.durationMs),
|
|
1635
2472
|
sessions: message.sessions
|
|
1636
2473
|
});
|
|
1637
|
-
this.
|
|
2474
|
+
this.finishBatch(activeBatch);
|
|
1638
2475
|
});
|
|
1639
2476
|
worker.on("error", (error) => {
|
|
1640
|
-
appLogger.error("search_index.worker_error", {
|
|
1641
|
-
|
|
2477
|
+
appLogger.error("search_index.worker_error", {
|
|
2478
|
+
context: this.activeBatch?.context ?? batch.context,
|
|
2479
|
+
error
|
|
2480
|
+
});
|
|
2481
|
+
this.invalidateWorker(worker, toError(error));
|
|
1642
2482
|
});
|
|
1643
|
-
worker.on("exit", (code) => this.finishWorker(worker,
|
|
2483
|
+
worker.on("exit", (code) => this.finishWorker(worker, code));
|
|
2484
|
+
}
|
|
2485
|
+
finishBatch(batch, error) {
|
|
2486
|
+
if (this.activeBatch !== batch) return;
|
|
2487
|
+
this.activeBatch = null;
|
|
2488
|
+
this.settle(batch, error);
|
|
2489
|
+
this.startNextBatch();
|
|
2490
|
+
}
|
|
2491
|
+
invalidateWorker(worker, error) {
|
|
2492
|
+
if (this.worker !== worker) return;
|
|
2493
|
+
this.worker = null;
|
|
2494
|
+
const batch = this.activeBatch;
|
|
2495
|
+
this.activeBatch = null;
|
|
2496
|
+
if (batch) this.settle(batch, error);
|
|
2497
|
+
void worker.terminate();
|
|
2498
|
+
this.startNextBatch();
|
|
1644
2499
|
}
|
|
1645
|
-
finishWorker(worker,
|
|
2500
|
+
finishWorker(worker, code) {
|
|
2501
|
+
const batch = this.worker === worker ? this.activeBatch : null;
|
|
1646
2502
|
appLogger.info("search_index.worker_exited", {
|
|
1647
|
-
batch_id: batch
|
|
1648
|
-
context: batch
|
|
2503
|
+
batch_id: batch?.id,
|
|
2504
|
+
context: batch?.context,
|
|
1649
2505
|
code,
|
|
1650
2506
|
shutting_down: this.isShuttingDown || void 0
|
|
1651
2507
|
});
|
|
1652
|
-
if (this.worker
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
if (
|
|
1656
|
-
|
|
2508
|
+
if (this.worker !== worker) return;
|
|
2509
|
+
this.worker = null;
|
|
2510
|
+
this.activeBatch = null;
|
|
2511
|
+
if (batch) {
|
|
2512
|
+
const error = code === 0 ? new Error("Search index worker exited before completing its batch") : new Error(`Search index worker exited with code ${code}`);
|
|
2513
|
+
if (code !== 0) {
|
|
2514
|
+
appLogger.warn("search_index.worker_exit", { context: batch.context, code });
|
|
2515
|
+
}
|
|
2516
|
+
this.settle(batch, error);
|
|
2517
|
+
}
|
|
1657
2518
|
this.startNextBatch();
|
|
1658
2519
|
}
|
|
1659
2520
|
settle(batch, error) {
|
|
1660
|
-
if (!this.pendingJobs.settle(batch, error))
|
|
2521
|
+
if (!this.pendingJobs.settle(batch, error) && !this.pendingMaintenanceJobs.settle(batch, error)) {
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
1661
2524
|
appLogger.info("search_index.worker_settled", {
|
|
1662
2525
|
batch_id: batch.id,
|
|
1663
2526
|
context: batch.context,
|
|
@@ -1671,6 +2534,143 @@ var SearchIndexJobRunner = class {
|
|
|
1671
2534
|
}
|
|
1672
2535
|
};
|
|
1673
2536
|
|
|
2537
|
+
// src/search-index-maintenance-scheduler.ts
|
|
2538
|
+
var MAINTENANCE_BATCH_SIZE = 4;
|
|
2539
|
+
var SearchIndexMaintenanceScheduler = class {
|
|
2540
|
+
constructor(runner, onStatus) {
|
|
2541
|
+
this.runner = runner;
|
|
2542
|
+
this.onStatus = onStatus;
|
|
2543
|
+
}
|
|
2544
|
+
runner;
|
|
2545
|
+
onStatus;
|
|
2546
|
+
pendingAgents = /* @__PURE__ */ new Set();
|
|
2547
|
+
completedAgents = /* @__PURE__ */ new Set();
|
|
2548
|
+
failedAgents = /* @__PURE__ */ new Set();
|
|
2549
|
+
cachedSessionsByAgent = /* @__PURE__ */ new Map();
|
|
2550
|
+
currentAgent;
|
|
2551
|
+
remaining;
|
|
2552
|
+
pumpPromise = null;
|
|
2553
|
+
stopped = false;
|
|
2554
|
+
enqueue(agentName) {
|
|
2555
|
+
if (this.stopped) return;
|
|
2556
|
+
this.pendingAgents.add(agentName);
|
|
2557
|
+
this.completedAgents.delete(agentName);
|
|
2558
|
+
this.failedAgents.delete(agentName);
|
|
2559
|
+
this.cachedSessionsByAgent.delete(agentName);
|
|
2560
|
+
this.startPump();
|
|
2561
|
+
}
|
|
2562
|
+
stop() {
|
|
2563
|
+
this.stopped = true;
|
|
2564
|
+
this.pendingAgents.clear();
|
|
2565
|
+
this.cachedSessionsByAgent.clear();
|
|
2566
|
+
this.publishStatus();
|
|
2567
|
+
}
|
|
2568
|
+
async waitForIdle() {
|
|
2569
|
+
while (this.pumpPromise) await this.pumpPromise;
|
|
2570
|
+
}
|
|
2571
|
+
startPump() {
|
|
2572
|
+
if (this.pumpPromise || this.stopped) return;
|
|
2573
|
+
this.pumpPromise = this.pump().finally(() => {
|
|
2574
|
+
this.pumpPromise = null;
|
|
2575
|
+
if (this.pendingAgents.size > 0 && !this.stopped) this.startPump();
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
async pump() {
|
|
2579
|
+
while (!this.stopped && this.pendingAgents.size > 0) {
|
|
2580
|
+
const agentName = this.pendingAgents.values().next().value;
|
|
2581
|
+
this.pendingAgents.delete(agentName);
|
|
2582
|
+
this.currentAgent = agentName;
|
|
2583
|
+
try {
|
|
2584
|
+
await this.runBatch(agentName);
|
|
2585
|
+
} catch (error) {
|
|
2586
|
+
this.cachedSessionsByAgent.delete(agentName);
|
|
2587
|
+
if (!this.stopped) {
|
|
2588
|
+
this.failedAgents.add(agentName);
|
|
2589
|
+
appLogger.error("search_index.maintenance_failed", {
|
|
2590
|
+
agent: agentName,
|
|
2591
|
+
error: toError(error)
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
} finally {
|
|
2595
|
+
this.currentAgent = void 0;
|
|
2596
|
+
this.remaining = void 0;
|
|
2597
|
+
this.publishStatus();
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
async runBatch(agentName) {
|
|
2602
|
+
const pending = readPendingSearchIndexMaintenance(agentName, MAINTENANCE_BATCH_SIZE);
|
|
2603
|
+
if (!pending) throw new Error("Search index storage is unavailable");
|
|
2604
|
+
if (pending.total === 0) {
|
|
2605
|
+
this.completedAgents.add(agentName);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
this.remaining = pending.total;
|
|
2609
|
+
this.publishStatus();
|
|
2610
|
+
const cached = this.cachedSessionsByAgent.get(agentName) ?? loadCachedSessions(agentName);
|
|
2611
|
+
if (!cached) throw new Error(`Session cache is unavailable for ${agentName}`);
|
|
2612
|
+
this.cachedSessionsByAgent.set(agentName, cached);
|
|
2613
|
+
const sessionsById = new Map(
|
|
2614
|
+
cached.sessions.map((session, sortIndex) => [session.id, { session, sortIndex }])
|
|
2615
|
+
);
|
|
2616
|
+
const changes = pending.sessionIds.flatMap((sessionId) => {
|
|
2617
|
+
const change = sessionsById.get(sessionId);
|
|
2618
|
+
return change ? [change] : [];
|
|
2619
|
+
});
|
|
2620
|
+
if (changes.length === 0) {
|
|
2621
|
+
throw new Error(`No cached sessions matched pending maintenance for ${agentName}`);
|
|
2622
|
+
}
|
|
2623
|
+
const meta = Object.fromEntries(
|
|
2624
|
+
changes.flatMap(({ session }) => {
|
|
2625
|
+
const sessionMeta = cached.meta[session.id];
|
|
2626
|
+
return sessionMeta ? [[session.id, sessionMeta]] : [];
|
|
2627
|
+
})
|
|
2628
|
+
);
|
|
2629
|
+
const job = {
|
|
2630
|
+
kind: "maintenance",
|
|
2631
|
+
context: "search.maintenance",
|
|
2632
|
+
agentName,
|
|
2633
|
+
changes,
|
|
2634
|
+
removedSessionIds: [],
|
|
2635
|
+
meta,
|
|
2636
|
+
searchIndexOptions: { isBulk: false }
|
|
2637
|
+
};
|
|
2638
|
+
appLogger.info("search_index.maintenance_batch_started", {
|
|
2639
|
+
agent: agentName,
|
|
2640
|
+
batch_sessions: changes.length,
|
|
2641
|
+
remaining: pending.total
|
|
2642
|
+
});
|
|
2643
|
+
await this.runner.enqueueMaintenance("search.maintenance", [job]);
|
|
2644
|
+
const next = readPendingSearchIndexMaintenance(agentName, MAINTENANCE_BATCH_SIZE);
|
|
2645
|
+
if (!next) throw new Error("Search index storage became unavailable");
|
|
2646
|
+
if (next.total >= pending.total) {
|
|
2647
|
+
throw new Error(`Search index maintenance made no progress for ${agentName}`);
|
|
2648
|
+
}
|
|
2649
|
+
appLogger.info("search_index.maintenance_batch_completed", {
|
|
2650
|
+
agent: agentName,
|
|
2651
|
+
indexed: pending.total - next.total,
|
|
2652
|
+
remaining: next.total
|
|
2653
|
+
});
|
|
2654
|
+
if (next.total > 0) {
|
|
2655
|
+
this.pendingAgents.add(agentName);
|
|
2656
|
+
this.remaining = next.total;
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
this.cachedSessionsByAgent.delete(agentName);
|
|
2660
|
+
this.completedAgents.add(agentName);
|
|
2661
|
+
}
|
|
2662
|
+
publishStatus() {
|
|
2663
|
+
this.onStatus({
|
|
2664
|
+
active: this.currentAgent != null || this.pendingAgents.size > 0,
|
|
2665
|
+
pendingAgents: [...this.pendingAgents],
|
|
2666
|
+
...this.currentAgent ? { currentAgent: this.currentAgent } : {},
|
|
2667
|
+
...this.remaining != null ? { remaining: this.remaining } : {},
|
|
2668
|
+
completedAgents: [...this.completedAgents],
|
|
2669
|
+
failedAgents: [...this.failedAgents]
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
};
|
|
2673
|
+
|
|
1674
2674
|
// src/scan-status-model.ts
|
|
1675
2675
|
var ScanStatusModel = class {
|
|
1676
2676
|
status = {
|
|
@@ -1682,7 +2682,13 @@ var ScanStatusModel = class {
|
|
|
1682
2682
|
agentStatuses: {},
|
|
1683
2683
|
totalAgents: 0,
|
|
1684
2684
|
updatedAt: Date.now(),
|
|
1685
|
-
backfill: { active: false, pendingAgents: [], completedAgents: [], failedAgents: [] }
|
|
2685
|
+
backfill: { active: false, pendingAgents: [], completedAgents: [], failedAgents: [] },
|
|
2686
|
+
searchIndexMaintenance: {
|
|
2687
|
+
active: false,
|
|
2688
|
+
pendingAgents: [],
|
|
2689
|
+
completedAgents: [],
|
|
2690
|
+
failedAgents: []
|
|
2691
|
+
}
|
|
1686
2692
|
};
|
|
1687
2693
|
snapshot() {
|
|
1688
2694
|
const backfill = {
|
|
@@ -1708,7 +2714,13 @@ var ScanStatusModel = class {
|
|
|
1708
2714
|
{ ...status }
|
|
1709
2715
|
])
|
|
1710
2716
|
),
|
|
1711
|
-
backfill
|
|
2717
|
+
backfill,
|
|
2718
|
+
searchIndexMaintenance: {
|
|
2719
|
+
...this.status.searchIndexMaintenance,
|
|
2720
|
+
pendingAgents: [...this.status.searchIndexMaintenance.pendingAgents],
|
|
2721
|
+
completedAgents: [...this.status.searchIndexMaintenance.completedAgents],
|
|
2722
|
+
failedAgents: [...this.status.searchIndexMaintenance.failedAgents]
|
|
2723
|
+
}
|
|
1712
2724
|
};
|
|
1713
2725
|
}
|
|
1714
2726
|
startBatch(agentNames, phase, sessionCounts) {
|
|
@@ -1778,7 +2790,7 @@ var ScanStatusModel = class {
|
|
|
1778
2790
|
}
|
|
1779
2791
|
updateAgent(agentName, progress) {
|
|
1780
2792
|
const status = this.status.agentStatuses[agentName];
|
|
1781
|
-
if (!status || status.status === "pending" || status.status === "complete" || status.status === "
|
|
2793
|
+
if (!status || status.status === "pending" || status.status === "complete" || (status.status === "publish-queued" || status.status === "publishing") && progress.phase !== "finalizing") {
|
|
1782
2794
|
return null;
|
|
1783
2795
|
}
|
|
1784
2796
|
const now = Date.now();
|
|
@@ -1801,7 +2813,7 @@ var ScanStatusModel = class {
|
|
|
1801
2813
|
updatedAt: now
|
|
1802
2814
|
});
|
|
1803
2815
|
}
|
|
1804
|
-
|
|
2816
|
+
queueAgentPublication(agentName) {
|
|
1805
2817
|
const status = this.status.agentStatuses[agentName];
|
|
1806
2818
|
if (!this.status.active || !status || status.status !== "scanning" && status.status !== "finalizing") {
|
|
1807
2819
|
return null;
|
|
@@ -1809,7 +2821,24 @@ var ScanStatusModel = class {
|
|
|
1809
2821
|
const now = Date.now();
|
|
1810
2822
|
const agentStatuses = {
|
|
1811
2823
|
...this.status.agentStatuses,
|
|
1812
|
-
[agentName]: { ...status, status: "
|
|
2824
|
+
[agentName]: { ...status, status: "publish-queued", updatedAt: now }
|
|
2825
|
+
};
|
|
2826
|
+
return this.set({
|
|
2827
|
+
...this.status,
|
|
2828
|
+
phase: this.activePhase(this.status.pendingAgents, this.status.scanningAgents, agentStatuses),
|
|
2829
|
+
agentStatuses,
|
|
2830
|
+
updatedAt: now
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
publishAgent(agentName) {
|
|
2834
|
+
const status = this.status.agentStatuses[agentName];
|
|
2835
|
+
if (!this.status.active || !status || status.status !== "scanning" && status.status !== "finalizing" && status.status !== "publish-queued") {
|
|
2836
|
+
return null;
|
|
2837
|
+
}
|
|
2838
|
+
const now = Date.now();
|
|
2839
|
+
const agentStatuses = {
|
|
2840
|
+
...this.status.agentStatuses,
|
|
2841
|
+
[agentName]: { ...status, status: "publishing", updatedAt: now }
|
|
1813
2842
|
};
|
|
1814
2843
|
return this.set({
|
|
1815
2844
|
...this.status,
|
|
@@ -1851,6 +2880,38 @@ var ScanStatusModel = class {
|
|
|
1851
2880
|
completedAt: isActive ? void 0 : now
|
|
1852
2881
|
});
|
|
1853
2882
|
}
|
|
2883
|
+
failAgent(agentName, error, sessionCount) {
|
|
2884
|
+
const pendingAgents = this.status.pendingAgents.filter((agent) => agent !== agentName);
|
|
2885
|
+
const scanningAgents = this.status.scanningAgents.filter((agent) => agent !== agentName);
|
|
2886
|
+
const completedAgents = this.status.completedAgents.filter((agent) => agent !== agentName);
|
|
2887
|
+
const isActive = pendingAgents.length > 0 || scanningAgents.length > 0;
|
|
2888
|
+
const now = Date.now();
|
|
2889
|
+
const previousStatus = this.status.agentStatuses[agentName];
|
|
2890
|
+
return this.set({
|
|
2891
|
+
...this.status,
|
|
2892
|
+
active: isActive,
|
|
2893
|
+
phase: isActive ? this.activePhase(pendingAgents, scanningAgents, this.status.agentStatuses) : "idle",
|
|
2894
|
+
pendingAgents,
|
|
2895
|
+
scanningAgents,
|
|
2896
|
+
completedAgents,
|
|
2897
|
+
agentStatuses: {
|
|
2898
|
+
...this.status.agentStatuses,
|
|
2899
|
+
[agentName]: {
|
|
2900
|
+
agentName,
|
|
2901
|
+
status: "failed",
|
|
2902
|
+
error,
|
|
2903
|
+
total: previousStatus?.total,
|
|
2904
|
+
processed: previousStatus?.processed,
|
|
2905
|
+
sessions: sessionCount ?? previousStatus?.sessions ?? 0,
|
|
2906
|
+
startedAt: previousStatus?.startedAt,
|
|
2907
|
+
updatedAt: now,
|
|
2908
|
+
completedAt: now
|
|
2909
|
+
}
|
|
2910
|
+
},
|
|
2911
|
+
updatedAt: now,
|
|
2912
|
+
completedAt: isActive ? void 0 : now
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
1854
2915
|
finishBatch() {
|
|
1855
2916
|
const now = Date.now();
|
|
1856
2917
|
return this.set({
|
|
@@ -1862,26 +2923,49 @@ var ScanStatusModel = class {
|
|
|
1862
2923
|
agentStatuses: Object.fromEntries(
|
|
1863
2924
|
Object.entries(this.status.agentStatuses).map(([agentName, status]) => [
|
|
1864
2925
|
agentName,
|
|
1865
|
-
|
|
2926
|
+
status.status === "failed" ? { ...status, completedAt: status.completedAt ?? now, updatedAt: now } : {
|
|
2927
|
+
...status,
|
|
2928
|
+
status: "complete",
|
|
2929
|
+
completedAt: status.completedAt ?? now,
|
|
2930
|
+
updatedAt: now
|
|
2931
|
+
}
|
|
1866
2932
|
])
|
|
1867
2933
|
),
|
|
1868
2934
|
updatedAt: now,
|
|
1869
2935
|
completedAt: now
|
|
1870
2936
|
});
|
|
1871
2937
|
}
|
|
1872
|
-
updateBackfill(
|
|
2938
|
+
updateBackfill(backfill) {
|
|
2939
|
+
return this.set({
|
|
2940
|
+
...this.status,
|
|
2941
|
+
backfill: {
|
|
2942
|
+
...backfill,
|
|
2943
|
+
pendingAgents: [...backfill.pendingAgents],
|
|
2944
|
+
completedAgents: [...backfill.completedAgents],
|
|
2945
|
+
failedAgents: [...backfill.failedAgents],
|
|
2946
|
+
progress: backfill.progress ? { ...backfill.progress } : void 0
|
|
2947
|
+
},
|
|
2948
|
+
updatedAt: Date.now()
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
updateSearchIndexMaintenance(searchIndexMaintenance) {
|
|
1873
2952
|
return this.set({
|
|
1874
2953
|
...this.status,
|
|
1875
|
-
|
|
2954
|
+
searchIndexMaintenance: {
|
|
2955
|
+
...searchIndexMaintenance,
|
|
2956
|
+
pendingAgents: [...searchIndexMaintenance.pendingAgents],
|
|
2957
|
+
completedAgents: [...searchIndexMaintenance.completedAgents],
|
|
2958
|
+
failedAgents: [...searchIndexMaintenance.failedAgents]
|
|
2959
|
+
},
|
|
1876
2960
|
updatedAt: Date.now()
|
|
1877
2961
|
});
|
|
1878
2962
|
}
|
|
1879
2963
|
activePhase(pendingAgents, scanningAgents, agentStatuses) {
|
|
1880
2964
|
if (pendingAgents.length === 0 && scanningAgents.length > 0 && scanningAgents.every((agentName) => {
|
|
1881
2965
|
const status = agentStatuses[agentName]?.status;
|
|
1882
|
-
return status === "finalizing" || status === "
|
|
2966
|
+
return status === "finalizing" || status === "publish-queued" || status === "publishing";
|
|
1883
2967
|
})) {
|
|
1884
|
-
return "
|
|
2968
|
+
return "publishing";
|
|
1885
2969
|
}
|
|
1886
2970
|
return this.status.phase === "initializing" ? "initializing" : "scanning";
|
|
1887
2971
|
}
|
|
@@ -1897,14 +2981,26 @@ var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
|
|
|
1897
2981
|
var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
|
|
1898
2982
|
var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1899
2983
|
var CACHE_TRUNCATION_COVERAGE = 0.5;
|
|
1900
|
-
|
|
2984
|
+
var STATUS_PROGRESS_INTERVAL_MS = 100;
|
|
2985
|
+
function buildPersistenceDiff(previousSessions, nextSessions, candidateChangedIds = [], completeness = "complete", explicitRemovedSessionIds = []) {
|
|
1901
2986
|
const { changes, removedSessionIds } = computeSessionDiff(
|
|
1902
2987
|
previousSessions,
|
|
1903
2988
|
nextSessions,
|
|
1904
2989
|
candidateChangedIds,
|
|
1905
2990
|
sessionSignature
|
|
1906
2991
|
);
|
|
1907
|
-
|
|
2992
|
+
if (completeness === "complete") {
|
|
2993
|
+
return { changedSessions: changes, removedSessionIds };
|
|
2994
|
+
}
|
|
2995
|
+
const explicitRemovals = new Set(explicitRemovedSessionIds);
|
|
2996
|
+
return {
|
|
2997
|
+
changedSessions: changes,
|
|
2998
|
+
removedSessionIds: removedSessionIds.filter((sessionId) => explicitRemovals.has(sessionId))
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
function sourceFailureError(failures) {
|
|
3002
|
+
const noun = failures.length === 1 ? "source" : "sources";
|
|
3003
|
+
return new Error(`${failures.length} session ${noun} failed; last-known-good data retained`);
|
|
1908
3004
|
}
|
|
1909
3005
|
function restoreAgentCacheMeta(agent, cached) {
|
|
1910
3006
|
agent.setSessionMetaMap(new Map(Object.entries(cached.meta)));
|
|
@@ -1913,21 +3009,24 @@ var AgentSyncEngine = class {
|
|
|
1913
3009
|
constructor(options) {
|
|
1914
3010
|
this.options = options;
|
|
1915
3011
|
this.scheduler = new AgentOperationScheduler((agentName) => this.performRefresh(agentName));
|
|
3012
|
+
this.searchIndexJobs = new SearchIndexJobRunner();
|
|
3013
|
+
this.searchIndexMaintenance = new SearchIndexMaintenanceScheduler(
|
|
3014
|
+
this.searchIndexJobs,
|
|
3015
|
+
(status) => this.publishStatus(this.scanStatus.updateSearchIndexMaintenance(status))
|
|
3016
|
+
);
|
|
1916
3017
|
}
|
|
1917
3018
|
options;
|
|
1918
3019
|
lastRefreshAtByAgent = /* @__PURE__ */ new Map();
|
|
1919
3020
|
scheduler;
|
|
1920
3021
|
sessionIndex = new LiveSessionIndex();
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
currentBackfillProgress;
|
|
1924
|
-
cacheIntegrityCheckedAgents = /* @__PURE__ */ new Set();
|
|
1925
|
-
completedBackfillAgents = [];
|
|
1926
|
-
failedBackfillAgents = [];
|
|
3022
|
+
backfills = new BackfillLifecycle();
|
|
3023
|
+
cacheIntegrityValidUntilByAgent = /* @__PURE__ */ new Map();
|
|
1927
3024
|
sessionsChangedListeners = /* @__PURE__ */ new Set();
|
|
1928
3025
|
statusChangedListeners = /* @__PURE__ */ new Set();
|
|
3026
|
+
statusProgressThrottles = /* @__PURE__ */ new Map();
|
|
1929
3027
|
scanStatus = new ScanStatusModel();
|
|
1930
|
-
searchIndexJobs
|
|
3028
|
+
searchIndexJobs;
|
|
3029
|
+
searchIndexMaintenance;
|
|
1931
3030
|
nextPublicationId = 1;
|
|
1932
3031
|
backgroundRefreshTimer = null;
|
|
1933
3032
|
isShuttingDown = false;
|
|
@@ -1940,6 +3039,13 @@ var AgentSyncEngine = class {
|
|
|
1940
3039
|
options.cacheTimestamps?.[agent.name] ?? Date.now()
|
|
1941
3040
|
);
|
|
1942
3041
|
}
|
|
3042
|
+
for (const [agentName, failure] of Object.entries(snapshot.scanFailures ?? {})) {
|
|
3043
|
+
this.scanStatus.failAgent(
|
|
3044
|
+
agentName,
|
|
3045
|
+
`${failure.stage}: ${failure.message}`,
|
|
3046
|
+
snapshot.byAgent[agentName]?.length ?? 0
|
|
3047
|
+
);
|
|
3048
|
+
}
|
|
1943
3049
|
}
|
|
1944
3050
|
snapshot() {
|
|
1945
3051
|
return this.sessionIndex.snapshot();
|
|
@@ -1961,6 +3067,7 @@ var AgentSyncEngine = class {
|
|
|
1961
3067
|
publicationId: this.publicationId("scan.initial"),
|
|
1962
3068
|
agents: jobs.map((job) => job.agentName)
|
|
1963
3069
|
});
|
|
3070
|
+
for (const job of jobs) this.searchIndexMaintenance.enqueue(job.agentName);
|
|
1964
3071
|
}
|
|
1965
3072
|
handleAgentsChanged(agentNames) {
|
|
1966
3073
|
const snapshot = this.sessionIndex.snapshot();
|
|
@@ -1988,7 +3095,7 @@ var AgentSyncEngine = class {
|
|
|
1988
3095
|
const activeOperations = {
|
|
1989
3096
|
agent_operations: schedulerSnapshot.activeOperations,
|
|
1990
3097
|
refreshes: schedulerSnapshot.activeRefreshes,
|
|
1991
|
-
backfill_running: this.
|
|
3098
|
+
backfill_running: this.backfills.runningAttempt()?.agentName || void 0,
|
|
1992
3099
|
scan_workers: this.options.workerRunner.activeCount
|
|
1993
3100
|
};
|
|
1994
3101
|
if (activeOperations.agent_operations > 0 || activeOperations.scan_workers > 0) {
|
|
@@ -1999,16 +3106,17 @@ var AgentSyncEngine = class {
|
|
|
1999
3106
|
clearTimeout(this.backgroundRefreshTimer);
|
|
2000
3107
|
this.backgroundRefreshTimer = null;
|
|
2001
3108
|
}
|
|
2002
|
-
this.
|
|
2003
|
-
this.
|
|
2004
|
-
this.
|
|
2005
|
-
this.
|
|
3109
|
+
this.backfills.cancelAll();
|
|
3110
|
+
this.searchIndexMaintenance.stop();
|
|
3111
|
+
this.cancelProgressStatuses();
|
|
3112
|
+
this.cacheIntegrityValidUntilByAgent.clear();
|
|
2006
3113
|
const searchIndexSnapshot = this.searchIndexJobs.snapshot();
|
|
2007
3114
|
appLogger.info("search_index.shutdown.started", {
|
|
2008
3115
|
active_batch_id: searchIndexSnapshot.activeBatchId,
|
|
2009
3116
|
pending_batches: searchIndexSnapshot.pendingBatches
|
|
2010
3117
|
});
|
|
2011
3118
|
await this.searchIndexJobs.shutdown();
|
|
3119
|
+
await this.searchIndexMaintenance.waitForIdle();
|
|
2012
3120
|
await this.options.workerRunner.shutdown();
|
|
2013
3121
|
await this.scheduler.waitForIdle();
|
|
2014
3122
|
const stoppedSearchIndexSnapshot = this.searchIndexJobs.snapshot();
|
|
@@ -2028,41 +3136,106 @@ var AgentSyncEngine = class {
|
|
|
2028
3136
|
this.publishStatus(this.scanStatus.setPhase(phase));
|
|
2029
3137
|
}
|
|
2030
3138
|
beginAgentScan(agentName) {
|
|
3139
|
+
this.cancelProgressStatus(`scan:${agentName}`);
|
|
2031
3140
|
const snapshot = this.sessionIndex.snapshot();
|
|
2032
3141
|
if (!this.scanStatus.snapshot().active) this.startScanBatch([agentName], "scanning");
|
|
2033
3142
|
this.publishStatus(
|
|
2034
3143
|
this.scanStatus.beginAgent(agentName, snapshot.byAgent[agentName]?.length ?? 0)
|
|
2035
3144
|
);
|
|
2036
3145
|
}
|
|
2037
|
-
updateAgentScanProgress(agentName, progress) {
|
|
2038
|
-
if (
|
|
2039
|
-
|
|
3146
|
+
updateAgentScanProgress(agentName, progress, backfillAttempt) {
|
|
3147
|
+
if (backfillAttempt) {
|
|
3148
|
+
const key2 = `backfill:${agentName}`;
|
|
3149
|
+
const current = this.backfills.stateFor(agentName);
|
|
3150
|
+
if (current?.status !== "running" || current.attemptId !== backfillAttempt.attemptId) {
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
const currentPhase2 = current.progress?.phase ?? "scanning";
|
|
3154
|
+
const nextPhase2 = progress.phase ?? "scanning";
|
|
3155
|
+
if (currentPhase2 && currentPhase2 !== nextPhase2) this.flushProgressStatus(key2);
|
|
3156
|
+
if (this.backfills.updateProgress(backfillAttempt, {
|
|
2040
3157
|
phase: progress.phase,
|
|
2041
3158
|
total: progress.total,
|
|
2042
3159
|
processed: progress.processed,
|
|
2043
3160
|
sessions: progress.sessions
|
|
2044
|
-
}
|
|
2045
|
-
|
|
3161
|
+
})) {
|
|
3162
|
+
this.publishProgressStatus(
|
|
3163
|
+
key2,
|
|
3164
|
+
nextPhase2,
|
|
3165
|
+
this.scanStatus.updateBackfill(this.backfills.status())
|
|
3166
|
+
);
|
|
3167
|
+
}
|
|
2046
3168
|
return;
|
|
2047
3169
|
}
|
|
2048
|
-
|
|
3170
|
+
const key = `scan:${agentName}`;
|
|
3171
|
+
const currentPhase = this.scanStatus.snapshot().agentStatuses[agentName]?.status;
|
|
3172
|
+
const nextPhase = progress.phase === "finalizing" ? "finalizing" : "scanning";
|
|
3173
|
+
if (currentPhase && currentPhase !== nextPhase) this.flushProgressStatus(key);
|
|
3174
|
+
const status = this.scanStatus.updateAgent(agentName, progress);
|
|
3175
|
+
this.publishProgressStatus(key, status?.agentStatuses[agentName]?.status ?? nextPhase, status);
|
|
3176
|
+
}
|
|
3177
|
+
beginAgentPublishing(agentName) {
|
|
3178
|
+
this.flushProgressStatus(`scan:${agentName}`);
|
|
3179
|
+
this.publishStatus(this.scanStatus.publishAgent(agentName));
|
|
2049
3180
|
}
|
|
2050
|
-
|
|
2051
|
-
this.
|
|
3181
|
+
queueAgentPublication(agentName) {
|
|
3182
|
+
this.flushProgressStatus(`scan:${agentName}`);
|
|
3183
|
+
this.publishStatus(this.scanStatus.queueAgentPublication(agentName));
|
|
2052
3184
|
}
|
|
2053
3185
|
finishAgentScan(agentName) {
|
|
3186
|
+
this.flushProgressStatus(`scan:${agentName}`);
|
|
2054
3187
|
const count = this.sessionIndex.snapshot().byAgent[agentName]?.length;
|
|
2055
3188
|
this.publishStatus(this.scanStatus.finishAgent(agentName, count));
|
|
2056
3189
|
}
|
|
2057
3190
|
finishScanBatch() {
|
|
3191
|
+
this.flushProgressStatuses("scan:");
|
|
2058
3192
|
this.publishStatus(this.scanStatus.finishBatch());
|
|
2059
3193
|
}
|
|
2060
3194
|
publishBackfillStatus() {
|
|
2061
|
-
this.publishStatus(this.scanStatus.updateBackfill(this.
|
|
3195
|
+
this.publishStatus(this.scanStatus.updateBackfill(this.backfills.status()));
|
|
2062
3196
|
}
|
|
2063
3197
|
publishStatus(event) {
|
|
2064
3198
|
if (!event || this.isShuttingDown) return;
|
|
2065
|
-
for (const listener of this.statusChangedListeners)
|
|
3199
|
+
for (const listener of this.statusChangedListeners) {
|
|
3200
|
+
try {
|
|
3201
|
+
listener(event);
|
|
3202
|
+
} catch (error) {
|
|
3203
|
+
appLogger.error("scan.status_listener.error", { error });
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
publishProgressStatus(key, phase, event) {
|
|
3208
|
+
if (!event) return;
|
|
3209
|
+
let throttle = this.statusProgressThrottles.get(key);
|
|
3210
|
+
if (!throttle) {
|
|
3211
|
+
throttle = new LatestValueThrottle(STATUS_PROGRESS_INTERVAL_MS, () => {
|
|
3212
|
+
this.publishStatus(this.scanStatus.snapshot());
|
|
3213
|
+
});
|
|
3214
|
+
this.statusProgressThrottles.set(key, throttle);
|
|
3215
|
+
}
|
|
3216
|
+
throttle.push(void 0, phase);
|
|
3217
|
+
}
|
|
3218
|
+
flushProgressStatus(key) {
|
|
3219
|
+
const throttle = this.statusProgressThrottles.get(key);
|
|
3220
|
+
if (!throttle) return;
|
|
3221
|
+
throttle.flush();
|
|
3222
|
+
throttle.cancel();
|
|
3223
|
+
this.statusProgressThrottles.delete(key);
|
|
3224
|
+
}
|
|
3225
|
+
cancelProgressStatus(key) {
|
|
3226
|
+
const throttle = this.statusProgressThrottles.get(key);
|
|
3227
|
+
if (!throttle) return;
|
|
3228
|
+
throttle.cancel();
|
|
3229
|
+
this.statusProgressThrottles.delete(key);
|
|
3230
|
+
}
|
|
3231
|
+
flushProgressStatuses(prefix) {
|
|
3232
|
+
for (const key of this.statusProgressThrottles.keys()) {
|
|
3233
|
+
if (key.startsWith(prefix)) this.flushProgressStatus(key);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
cancelProgressStatuses() {
|
|
3237
|
+
for (const throttle of this.statusProgressThrottles.values()) throttle.cancel();
|
|
3238
|
+
this.statusProgressThrottles.clear();
|
|
2066
3239
|
}
|
|
2067
3240
|
emitSessionsChanged(change) {
|
|
2068
3241
|
if (this.isShuttingDown) return;
|
|
@@ -2070,20 +3243,33 @@ var AgentSyncEngine = class {
|
|
|
2070
3243
|
}
|
|
2071
3244
|
async performRefresh(agentName) {
|
|
2072
3245
|
this.beginAgentScan(agentName);
|
|
3246
|
+
const startedAt = performance.now();
|
|
3247
|
+
let failed = false;
|
|
3248
|
+
let cached = null;
|
|
3249
|
+
let result = null;
|
|
2073
3250
|
try {
|
|
2074
|
-
|
|
3251
|
+
if (this.findAgent(agentName)) cached = loadCachedSessions(agentName);
|
|
3252
|
+
result = await this.runRefresh(agentName, cached, startedAt);
|
|
3253
|
+
return result;
|
|
2075
3254
|
} catch (error) {
|
|
3255
|
+
this.options.workerRunner.discard?.(agentName);
|
|
3256
|
+
failed = true;
|
|
3257
|
+
const failure = toError(error);
|
|
2076
3258
|
appLogger.error("scan.refresh.error", { agent: agentName, error });
|
|
2077
3259
|
console.error(`[${agentName}] Session refresh failed:`, error);
|
|
3260
|
+
this.flushProgressStatus(`scan:${agentName}`);
|
|
3261
|
+
this.publishStatus(this.scanStatus.failAgent(agentName, failure.message));
|
|
2078
3262
|
return "failed";
|
|
2079
3263
|
} finally {
|
|
2080
|
-
this.finishAgentScan(agentName);
|
|
3264
|
+
if (!failed) this.finishAgentScan(agentName);
|
|
2081
3265
|
const agent = this.findAgent(agentName);
|
|
2082
|
-
if (agent && this.needsBackfill(agent))
|
|
3266
|
+
if (agent && this.needsBackfill(agent, cached, failed || result === "committed")) {
|
|
3267
|
+
this.enqueueBackfill(agentName);
|
|
3268
|
+
}
|
|
3269
|
+
if (agent) this.searchIndexMaintenance.enqueue(agentName);
|
|
2083
3270
|
}
|
|
2084
3271
|
}
|
|
2085
|
-
async runRefresh(agentName) {
|
|
2086
|
-
const startedAt = performance.now();
|
|
3272
|
+
async runRefresh(agentName, cached, startedAt) {
|
|
2087
3273
|
const pendingPathCount = this.scheduler.takePendingSignalCount(agentName);
|
|
2088
3274
|
const agent = this.findAgent(agentName);
|
|
2089
3275
|
if (!agent) {
|
|
@@ -2091,11 +3277,20 @@ var AgentSyncEngine = class {
|
|
|
2091
3277
|
return "skipped";
|
|
2092
3278
|
}
|
|
2093
3279
|
const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
|
|
2094
|
-
const cached = loadCachedSessions(agentName);
|
|
2095
3280
|
const refreshBaseline = cached?.sessions ?? previousSessions;
|
|
2096
3281
|
const cacheTimestamp = cached?.timestamp ?? this.lastRefreshAtByAgent.get(agentName) ?? 0;
|
|
2097
3282
|
if (cached) restoreAgentCacheMeta(agent, cached);
|
|
2098
|
-
const
|
|
3283
|
+
const durableMeta = new Map(agent.getSessionMetaMap());
|
|
3284
|
+
const durableLastRefreshAt = this.lastRefreshAtByAgent.get(agentName);
|
|
3285
|
+
const initialization = readAgentCacheInitialization(agentName);
|
|
3286
|
+
if (initialization.status === "failed") {
|
|
3287
|
+
appLogger.warn("scan.refresh.cache_state_unavailable", {
|
|
3288
|
+
agent: agentName,
|
|
3289
|
+
state: "initialization"
|
|
3290
|
+
});
|
|
3291
|
+
return "unchanged";
|
|
3292
|
+
}
|
|
3293
|
+
const isInitialized = initialization.value;
|
|
2099
3294
|
const availabilityStartedAt = performance.now();
|
|
2100
3295
|
const isAvailable = agent.isAvailable();
|
|
2101
3296
|
const availabilityDuration = performance.now() - availabilityStartedAt;
|
|
@@ -2116,10 +3311,29 @@ var AgentSyncEngine = class {
|
|
|
2116
3311
|
} else {
|
|
2117
3312
|
strategyResult = await this.scanAgentFully(agent, previousSessions);
|
|
2118
3313
|
}
|
|
2119
|
-
if (strategyResult.status === "unchanged")
|
|
3314
|
+
if (strategyResult.status === "unchanged") {
|
|
3315
|
+
this.options.workerRunner.commit?.(agentName);
|
|
3316
|
+
return "unchanged";
|
|
3317
|
+
}
|
|
2120
3318
|
const nextSessions = attachMissingProjectIdentities(strategyResult.nextSessions);
|
|
2121
3319
|
const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
|
|
2122
3320
|
const persistenceDiff = strategyResult.persistenceDiff;
|
|
3321
|
+
const publicationSessions = strategyResult.fullScanSessions ?? nextSessions;
|
|
3322
|
+
const publicationSessionIds = new Set(publicationSessions.map((session) => session.id));
|
|
3323
|
+
const missingBaselineSessions = refreshBaseline.reduce(
|
|
3324
|
+
(count, session) => count + Number(!publicationSessionIds.has(session.id)),
|
|
3325
|
+
0
|
|
3326
|
+
);
|
|
3327
|
+
const replacementDeleteCandidates = persistenceDiff ? persistenceDiff.removedSessionIds.length : strategyResult.completeness === "complete" ? missingBaselineSessions : strategyResult.explicitRemovedSessionIds.length;
|
|
3328
|
+
appLogger.info("scan.refresh.persistence_candidate", {
|
|
3329
|
+
agent: agentName,
|
|
3330
|
+
scope_from: strategyResult.scope.from,
|
|
3331
|
+
scope_to: strategyResult.scope.to,
|
|
3332
|
+
publication_completeness: strategyResult.completeness,
|
|
3333
|
+
durable_baseline_sessions: refreshBaseline.length,
|
|
3334
|
+
payload_sessions: publicationSessions.length,
|
|
3335
|
+
delete_candidates: replacementDeleteCandidates
|
|
3336
|
+
});
|
|
2123
3337
|
const changedSessionIds = persistenceDiff ? new Set(persistenceDiff.changedSessions.map(({ session }) => session.id)) : void 0;
|
|
2124
3338
|
const persistStartedAt = performance.now();
|
|
2125
3339
|
const persistentJob = persistenceDiff ? {
|
|
@@ -2134,39 +3348,62 @@ var AgentSyncEngine = class {
|
|
|
2134
3348
|
kind: "full",
|
|
2135
3349
|
context: "scan.refresh",
|
|
2136
3350
|
agentName,
|
|
2137
|
-
sessions:
|
|
3351
|
+
sessions: publicationSessions,
|
|
2138
3352
|
meta: buildAgentCacheMeta(agent),
|
|
3353
|
+
completeness: strategyResult.completeness,
|
|
3354
|
+
removedSessionIds: strategyResult.explicitRemovedSessionIds,
|
|
2139
3355
|
saveCache: true,
|
|
2140
3356
|
...searchIndexOptions ? { searchIndexOptions } : {}
|
|
2141
3357
|
};
|
|
2142
|
-
this.
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
3358
|
+
this.queueAgentPublication(agentName);
|
|
3359
|
+
let publication;
|
|
3360
|
+
try {
|
|
3361
|
+
publication = await this.commitSessionPublication({
|
|
3362
|
+
context: "scan.refresh",
|
|
3363
|
+
agentName,
|
|
3364
|
+
sessions: nextSessions,
|
|
3365
|
+
candidateChangedIds: strategyResult.preciseChangedIds ?? [],
|
|
3366
|
+
indexJob: persistentJob,
|
|
3367
|
+
onPublishing: () => this.beginAgentPublishing(agentName)
|
|
3368
|
+
});
|
|
3369
|
+
} catch (error) {
|
|
3370
|
+
agent.setSessionMetaMap(durableMeta);
|
|
3371
|
+
if (durableLastRefreshAt == null) this.lastRefreshAtByAgent.delete(agentName);
|
|
3372
|
+
else this.lastRefreshAtByAgent.set(agentName, durableLastRefreshAt);
|
|
3373
|
+
throw error;
|
|
3374
|
+
}
|
|
3375
|
+
this.options.workerRunner.commit?.(agentName);
|
|
2150
3376
|
const persistDuration = performance.now() - persistStartedAt;
|
|
2151
3377
|
logSearchIndexSync("scan.refresh", null, { pending_paths: pendingPathCount });
|
|
2152
3378
|
const totalDurationMs = performance.now() - startedAt;
|
|
2153
3379
|
this.scheduler.recordRefreshDuration(agentName, totalDurationMs);
|
|
2154
|
-
appLogger.info(
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
3380
|
+
appLogger.info(
|
|
3381
|
+
strategyResult.sourceFailures.length > 0 ? "scan.refresh.partial" : "scan.refresh.done",
|
|
3382
|
+
{
|
|
3383
|
+
agent: agentName,
|
|
3384
|
+
duration_ms: Math.round(totalDurationMs),
|
|
3385
|
+
sessions: nextSessions.length,
|
|
3386
|
+
new_sessions: publication.event?.newSessions ?? 0,
|
|
3387
|
+
updated_sessions: publication.event?.updatedSessions ?? 0,
|
|
3388
|
+
removed_sessions: publication.event?.removedSessions ?? 0,
|
|
3389
|
+
pending_paths: pendingPathCount,
|
|
3390
|
+
availability_ms: Math.round(availabilityDuration),
|
|
3391
|
+
check_ms: Math.round(strategyResult.checkDuration),
|
|
3392
|
+
scan_ms: Math.round(strategyResult.scanDuration),
|
|
3393
|
+
diff_ms: Math.round(publication.diffDuration),
|
|
3394
|
+
persist_ms: Math.round(persistDuration),
|
|
3395
|
+
search_index_ms: Math.round(persistDuration),
|
|
3396
|
+
persistent_index_worker_job: persistentJob.kind,
|
|
3397
|
+
failed_sources: strategyResult.sourceFailures.length
|
|
3398
|
+
}
|
|
3399
|
+
);
|
|
3400
|
+
if (strategyResult.sourceFailures.length > 0) {
|
|
3401
|
+
appLogger.warn("scan.refresh.source_failures", {
|
|
3402
|
+
agent: agentName,
|
|
3403
|
+
failures: strategyResult.sourceFailures
|
|
3404
|
+
});
|
|
3405
|
+
throw sourceFailureError(strategyResult.sourceFailures);
|
|
3406
|
+
}
|
|
2170
3407
|
return "committed";
|
|
2171
3408
|
}
|
|
2172
3409
|
/**
|
|
@@ -2183,28 +3420,29 @@ var AgentSyncEngine = class {
|
|
|
2183
3420
|
agent: agentName,
|
|
2184
3421
|
retained_sessions: previousSessions.length
|
|
2185
3422
|
});
|
|
2186
|
-
return this.refreshStrategyResult(previousSessions, { status: "unchanged" });
|
|
3423
|
+
return this.refreshStrategyResult(previousSessions, "partial", {}, { status: "unchanged" });
|
|
2187
3424
|
}
|
|
2188
|
-
return this.refreshStrategyResult([]);
|
|
3425
|
+
return this.refreshStrategyResult([], "partial", {});
|
|
2189
3426
|
}
|
|
2190
3427
|
async initializeAgent(agent, previousSessions) {
|
|
2191
3428
|
this.setScanPhase("initializing");
|
|
2192
3429
|
const scanStartedAt = performance.now();
|
|
2193
|
-
const
|
|
2194
|
-
|
|
2195
|
-
});
|
|
3430
|
+
const scope = this.startupScanOptions();
|
|
3431
|
+
const result = await this.runWorker(agent, previousSessions, { kind: "full-scan" }, scope);
|
|
2196
3432
|
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2197
3433
|
const sessions = attachMissingProjectIdentities(result.sessions);
|
|
2198
3434
|
this.lastRefreshAtByAgent.set(agent.name, Date.now());
|
|
2199
|
-
return this.refreshStrategyResult(sessions, {
|
|
3435
|
+
return this.refreshStrategyResult(sessions, result.completeness, scope, {
|
|
2200
3436
|
fullScanSessions: sessions,
|
|
2201
|
-
|
|
3437
|
+
explicitRemovedSessionIds: result.explicitRemovedSessionIds,
|
|
3438
|
+
scanDuration: performance.now() - scanStartedAt,
|
|
3439
|
+
sourceFailures: result.sourceFailures ?? []
|
|
2202
3440
|
});
|
|
2203
3441
|
}
|
|
2204
3442
|
async syncAgentSources(agent, cached, refreshStartedAt) {
|
|
2205
3443
|
const scanStartedAt = performance.now();
|
|
2206
|
-
const
|
|
2207
|
-
|
|
3444
|
+
const scope = this.startupScanOptions();
|
|
3445
|
+
const result = await this.runWorker(agent, cached.sessions, { kind: "source-refresh" }, scope, {
|
|
2208
3446
|
meta: cached.meta
|
|
2209
3447
|
});
|
|
2210
3448
|
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
@@ -2212,62 +3450,130 @@ var AgentSyncEngine = class {
|
|
|
2212
3450
|
const preciseChangedIds = result.changedIds ?? [];
|
|
2213
3451
|
const persistenceDiff = buildPersistenceDiff(cached.sessions, sessions, preciseChangedIds);
|
|
2214
3452
|
this.lastRefreshAtByAgent.set(agent.name, Date.now());
|
|
2215
|
-
if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0) {
|
|
3453
|
+
if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0 && (result.sourceFailures?.length ?? 0) === 0) {
|
|
2216
3454
|
this.logUnchangedRefresh(agent.name, refreshStartedAt);
|
|
2217
|
-
return this.refreshStrategyResult(sessions, {
|
|
3455
|
+
return this.refreshStrategyResult(sessions, result.completeness, scope, {
|
|
2218
3456
|
status: "unchanged",
|
|
2219
3457
|
scanDuration: performance.now() - scanStartedAt
|
|
2220
3458
|
});
|
|
2221
3459
|
}
|
|
2222
|
-
return this.refreshStrategyResult(sessions, {
|
|
3460
|
+
return this.refreshStrategyResult(sessions, result.completeness, scope, {
|
|
2223
3461
|
preciseChangedIds,
|
|
2224
3462
|
persistenceDiff,
|
|
2225
|
-
scanDuration: performance.now() - scanStartedAt
|
|
3463
|
+
scanDuration: performance.now() - scanStartedAt,
|
|
3464
|
+
sourceFailures: result.sourceFailures ?? []
|
|
2226
3465
|
});
|
|
2227
3466
|
}
|
|
2228
3467
|
async refreshChangedAgent(agent, baseline, cacheTimestamp, refreshStartedAt) {
|
|
3468
|
+
const scope = this.startupScanOptions();
|
|
2229
3469
|
const checkStartedAt = performance.now();
|
|
2230
3470
|
const checkResult = await Promise.resolve(agent.checkForChanges(cacheTimestamp, baseline));
|
|
2231
3471
|
const checkDuration = performance.now() - checkStartedAt;
|
|
3472
|
+
if (checkResult.status === "failed") {
|
|
3473
|
+
appLogger.warn("scan.refresh.change_check_failed", {
|
|
3474
|
+
agent: agent.name,
|
|
3475
|
+
source_path: checkResult.failure.sourcePath,
|
|
3476
|
+
error_class: checkResult.failure.errorClass,
|
|
3477
|
+
message: checkResult.failure.message
|
|
3478
|
+
});
|
|
3479
|
+
return this.refreshStrategyResult(
|
|
3480
|
+
baseline,
|
|
3481
|
+
"partial",
|
|
3482
|
+
{},
|
|
3483
|
+
{ status: "unchanged", checkDuration }
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
2232
3486
|
this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
|
|
2233
3487
|
if (!checkResult.hasChanges) {
|
|
2234
|
-
|
|
2235
|
-
|
|
3488
|
+
const scanStartedAt2 = performance.now();
|
|
3489
|
+
const result = await this.runWorker(agent, baseline, { kind: "recompute-derived" }, {});
|
|
3490
|
+
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
3491
|
+
const sessions2 = attachMissingProjectIdentities(result.sessions);
|
|
3492
|
+
const persistenceDiff = buildPersistenceDiff(baseline, sessions2);
|
|
3493
|
+
if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0) {
|
|
3494
|
+
this.logUnchangedRefresh(agent.name, refreshStartedAt);
|
|
3495
|
+
return this.refreshStrategyResult(
|
|
3496
|
+
sessions2,
|
|
3497
|
+
result.completeness,
|
|
3498
|
+
{},
|
|
3499
|
+
{
|
|
3500
|
+
status: "unchanged",
|
|
3501
|
+
checkDuration,
|
|
3502
|
+
scanDuration: performance.now() - scanStartedAt2
|
|
3503
|
+
}
|
|
3504
|
+
);
|
|
3505
|
+
}
|
|
3506
|
+
return this.refreshStrategyResult(
|
|
3507
|
+
sessions2,
|
|
3508
|
+
result.completeness,
|
|
3509
|
+
{},
|
|
3510
|
+
{
|
|
3511
|
+
persistenceDiff,
|
|
3512
|
+
checkDuration,
|
|
3513
|
+
scanDuration: performance.now() - scanStartedAt2,
|
|
3514
|
+
sourceFailures: result.sourceFailures ?? []
|
|
3515
|
+
}
|
|
3516
|
+
);
|
|
2236
3517
|
}
|
|
2237
3518
|
const preciseChangedIds = checkResult.changedIds ?? null;
|
|
2238
3519
|
const scanStartedAt = performance.now();
|
|
2239
3520
|
if (preciseChangedIds === null) {
|
|
2240
|
-
const result = await this.runWorker(agent, baseline,
|
|
3521
|
+
const result = await this.runWorker(agent, baseline, { kind: "full-scan" }, scope);
|
|
2241
3522
|
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2242
3523
|
const sessions2 = attachMissingProjectIdentities(result.sessions);
|
|
2243
|
-
return this.refreshStrategyResult(sessions2, {
|
|
2244
|
-
|
|
3524
|
+
return this.refreshStrategyResult(sessions2, result.completeness, scope, {
|
|
3525
|
+
// Meta-only changes (e.g. a pricing capture epoch bump) leave the head
|
|
3526
|
+
// signature intact; without the worker-reported ids they would never
|
|
3527
|
+
// persist and checkForChanges would rescan on every startup.
|
|
3528
|
+
persistenceDiff: buildPersistenceDiff(
|
|
3529
|
+
baseline,
|
|
3530
|
+
sessions2,
|
|
3531
|
+
result.changedIds ?? [],
|
|
3532
|
+
result.completeness,
|
|
3533
|
+
result.explicitRemovedSessionIds
|
|
3534
|
+
),
|
|
2245
3535
|
checkDuration,
|
|
2246
|
-
scanDuration: performance.now() - scanStartedAt
|
|
3536
|
+
scanDuration: performance.now() - scanStartedAt,
|
|
3537
|
+
sourceFailures: result.sourceFailures ?? []
|
|
2247
3538
|
});
|
|
2248
3539
|
}
|
|
3540
|
+
this.options.workerRunner.discard?.(agent.name);
|
|
2249
3541
|
const sessions = attachMissingProjectIdentities(
|
|
2250
|
-
await Promise.resolve(
|
|
3542
|
+
await Promise.resolve(
|
|
3543
|
+
agent.incrementalScan(baseline, preciseChangedIds, checkResult.refs, scope)
|
|
3544
|
+
)
|
|
2251
3545
|
);
|
|
2252
|
-
|
|
3546
|
+
const sourceFailures = checkResult.sourceFailures ?? [];
|
|
3547
|
+
const completeness = scope.from == null && scope.to == null && sourceFailures.length === 0 ? "complete" : "partial";
|
|
3548
|
+
return this.refreshStrategyResult(sessions, completeness, scope, {
|
|
2253
3549
|
preciseChangedIds,
|
|
2254
|
-
persistenceDiff: buildPersistenceDiff(
|
|
3550
|
+
persistenceDiff: buildPersistenceDiff(
|
|
3551
|
+
baseline,
|
|
3552
|
+
sessions,
|
|
3553
|
+
preciseChangedIds,
|
|
3554
|
+
completeness,
|
|
3555
|
+
preciseChangedIds
|
|
3556
|
+
),
|
|
2255
3557
|
checkDuration,
|
|
2256
|
-
scanDuration: performance.now() - scanStartedAt
|
|
3558
|
+
scanDuration: performance.now() - scanStartedAt,
|
|
3559
|
+
sourceFailures
|
|
2257
3560
|
});
|
|
2258
3561
|
}
|
|
2259
3562
|
async scanAgentFully(agent, previousSessions) {
|
|
2260
3563
|
const scanStartedAt = performance.now();
|
|
2261
|
-
const
|
|
3564
|
+
const scope = this.startupScanOptions();
|
|
3565
|
+
const result = await this.runWorker(agent, previousSessions, { kind: "full-scan" }, scope);
|
|
2262
3566
|
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2263
3567
|
const sessions = attachMissingProjectIdentities(result.sessions);
|
|
2264
3568
|
this.lastRefreshAtByAgent.set(agent.name, Date.now());
|
|
2265
|
-
return this.refreshStrategyResult(sessions, {
|
|
3569
|
+
return this.refreshStrategyResult(sessions, result.completeness, scope, {
|
|
2266
3570
|
fullScanSessions: sessions,
|
|
2267
|
-
|
|
3571
|
+
explicitRemovedSessionIds: result.explicitRemovedSessionIds,
|
|
3572
|
+
scanDuration: performance.now() - scanStartedAt,
|
|
3573
|
+
sourceFailures: result.sourceFailures ?? []
|
|
2268
3574
|
});
|
|
2269
3575
|
}
|
|
2270
|
-
refreshStrategyResult(nextSessions, overrides = {}) {
|
|
3576
|
+
refreshStrategyResult(nextSessions, completeness, scope, overrides = {}) {
|
|
2271
3577
|
return {
|
|
2272
3578
|
status: "continue",
|
|
2273
3579
|
nextSessions,
|
|
@@ -2276,86 +3582,55 @@ var AgentSyncEngine = class {
|
|
|
2276
3582
|
persistenceDiff: null,
|
|
2277
3583
|
checkDuration: 0,
|
|
2278
3584
|
scanDuration: 0,
|
|
3585
|
+
sourceFailures: [],
|
|
3586
|
+
completeness,
|
|
3587
|
+
scope,
|
|
3588
|
+
explicitRemovedSessionIds: [],
|
|
2279
3589
|
...overrides
|
|
2280
3590
|
};
|
|
2281
3591
|
}
|
|
2282
|
-
runWorker(agent, previousSessions,
|
|
3592
|
+
runWorker(agent, previousSessions, operation, scanOptions, runOptions = {}) {
|
|
2283
3593
|
return this.options.workerRunner.run(agent.name, {
|
|
2284
3594
|
previousSessions,
|
|
2285
|
-
|
|
3595
|
+
operation,
|
|
2286
3596
|
scanOptions,
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
checkpoint: workerOptions.checkpoint,
|
|
2291
|
-
meta: workerOptions.meta ?? buildAgentCacheMeta(agent),
|
|
2292
|
-
onProgress: (progress) => this.updateAgentScanProgress(agent.name, progress),
|
|
2293
|
-
onCheckpoint: (checkpoint) => this.handleWorkerCheckpoint(agent, checkpoint)
|
|
3597
|
+
meta: runOptions.meta ?? buildAgentCacheMeta(agent),
|
|
3598
|
+
onProgress: (progress) => this.updateAgentScanProgress(agent.name, progress, runOptions.backfillAttempt),
|
|
3599
|
+
onCheckpoint: runOptions.onCheckpoint
|
|
2294
3600
|
});
|
|
2295
3601
|
}
|
|
2296
|
-
|
|
2297
|
-
if (
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
} catch (error) {
|
|
2304
|
-
appLogger.error("scan.checkpoint.failed", {
|
|
2305
|
-
agent: agent.name,
|
|
2306
|
-
stage: checkpoint.stage,
|
|
2307
|
-
error
|
|
2308
|
-
});
|
|
2309
|
-
}
|
|
2310
|
-
agent.setSessionMetaMap(new Map(Object.entries(checkpoint.meta)));
|
|
2311
|
-
const event = this.sessionIndex.commitAgentSessions(agent.name, checkpoint.sessions);
|
|
2312
|
-
this.emitSessionsChanged({
|
|
2313
|
-
agentName: agent.name,
|
|
2314
|
-
sessions: this.sessionIndex.snapshot().byAgent[agent.name] ?? [],
|
|
2315
|
-
event
|
|
2316
|
-
});
|
|
2317
|
-
appLogger.info("scan.checkpoint.persisted", {
|
|
2318
|
-
agent: agent.name,
|
|
2319
|
-
stage: checkpoint.stage,
|
|
2320
|
-
sessions: checkpoint.sessions.length,
|
|
2321
|
-
persisted: persisted2
|
|
2322
|
-
});
|
|
2323
|
-
return;
|
|
2324
|
-
}
|
|
2325
|
-
let persisted = false;
|
|
2326
|
-
try {
|
|
2327
|
-
persisted = saveCachedSessionChanges(agent.name, checkpoint.changes, [], checkpoint.meta);
|
|
2328
|
-
} catch (error) {
|
|
2329
|
-
appLogger.error("scan.checkpoint.failed", {
|
|
2330
|
-
agent: agent.name,
|
|
2331
|
-
stage: checkpoint.stage,
|
|
2332
|
-
sessions: checkpoint.changes.length,
|
|
2333
|
-
error
|
|
2334
|
-
});
|
|
2335
|
-
}
|
|
2336
|
-
if (persisted && checkpoint.backfillCursor) {
|
|
2337
|
-
markAgentFullSyncProgress(agent.name, checkpoint.backfillCursor);
|
|
2338
|
-
}
|
|
2339
|
-
appLogger.debug("scan.checkpoint.persisted", {
|
|
2340
|
-
agent: agent.name,
|
|
2341
|
-
stage: checkpoint.stage,
|
|
2342
|
-
sessions: checkpoint.changes.length,
|
|
2343
|
-
backfill_cursor: checkpoint.backfillCursor,
|
|
2344
|
-
persisted
|
|
3602
|
+
handleBackfillCheckpoint(agentName, checkpoint) {
|
|
3603
|
+
if (checkpoint.stage !== "finalizing" || !checkpoint.backfillCursor) return;
|
|
3604
|
+
markAgentFullSyncProgress(agentName, checkpoint.backfillCursor);
|
|
3605
|
+
appLogger.debug("scan.backfill.checkpoint", {
|
|
3606
|
+
agent: agentName,
|
|
3607
|
+
cursor: checkpoint.backfillCursor,
|
|
3608
|
+
sessions: checkpoint.changes.length
|
|
2345
3609
|
});
|
|
2346
3610
|
}
|
|
2347
|
-
needsBackfill(agent) {
|
|
3611
|
+
needsBackfill(agent, cached, reloadCached = false) {
|
|
2348
3612
|
const startupScanOptions = this.startupScanOptions();
|
|
2349
3613
|
if (startupScanOptions.from == null && startupScanOptions.to == null) return false;
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
3614
|
+
const now = Date.now();
|
|
3615
|
+
if ((this.cacheIntegrityValidUntilByAgent.get(agent.name) ?? 0) >= now) return false;
|
|
3616
|
+
const lastSync = readAgentLastFullSyncAt(agent.name);
|
|
3617
|
+
if (lastSync.status === "failed") {
|
|
3618
|
+
appLogger.warn("scan.backfill.cache_state_unavailable", {
|
|
3619
|
+
agent: agent.name,
|
|
3620
|
+
state: "last_full_sync"
|
|
3621
|
+
});
|
|
3622
|
+
return false;
|
|
3623
|
+
}
|
|
3624
|
+
const lastSyncAt = lastSync.value;
|
|
3625
|
+
if (lastSyncAt == null || now - lastSyncAt > BACKFILL_INTERVAL_MS) {
|
|
3626
|
+
return agent.isAvailable();
|
|
3627
|
+
}
|
|
2353
3628
|
if (!(agent instanceof FileSystemSessionSource)) return false;
|
|
2354
|
-
if (
|
|
2355
|
-
const
|
|
3629
|
+
if (!agent.isAvailable()) return false;
|
|
3630
|
+
const cachedSessions = reloadCached || cached === void 0 ? loadCachedSessions(agent.name) : cached;
|
|
2356
3631
|
const sourceCount = agent.listSessionSources().length;
|
|
2357
|
-
const cachedCount =
|
|
2358
|
-
this.
|
|
3632
|
+
const cachedCount = cachedSessions?.sessions.length ?? 0;
|
|
3633
|
+
this.cacheIntegrityValidUntilByAgent.set(agent.name, lastSyncAt + BACKFILL_INTERVAL_MS);
|
|
2359
3634
|
if (sourceCount > 0 && cachedCount / sourceCount < CACHE_TRUNCATION_COVERAGE) {
|
|
2360
3635
|
appLogger.warn("scan.backfill.cache_truncated", {
|
|
2361
3636
|
agent: agent.name,
|
|
@@ -2368,43 +3643,45 @@ var AgentSyncEngine = class {
|
|
|
2368
3643
|
return false;
|
|
2369
3644
|
}
|
|
2370
3645
|
enqueueBackfill(agentName) {
|
|
2371
|
-
if (this.isShuttingDown || this.
|
|
2372
|
-
return;
|
|
2373
|
-
}
|
|
2374
|
-
this.backfillQueue.push(agentName);
|
|
3646
|
+
if (this.isShuttingDown || !this.backfills.enqueue(agentName)) return;
|
|
2375
3647
|
this.publishBackfillStatus();
|
|
2376
3648
|
this.pumpBackfillQueue();
|
|
2377
3649
|
}
|
|
2378
3650
|
pumpBackfillQueue() {
|
|
2379
|
-
if (this.isShuttingDown
|
|
2380
|
-
const
|
|
2381
|
-
if (!
|
|
2382
|
-
this.currentBackfillAgent = agentName;
|
|
2383
|
-
this.currentBackfillProgress = void 0;
|
|
3651
|
+
if (this.isShuttingDown) return;
|
|
3652
|
+
const attempt = this.backfills.startNext();
|
|
3653
|
+
if (!attempt) return;
|
|
2384
3654
|
this.publishBackfillStatus();
|
|
2385
|
-
void this.runBackfill(
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
3655
|
+
void this.runBackfill(attempt).then((result) => this.completeBackfillAttempt(attempt, result)).catch((error) => this.rejectBackfillAttempt(attempt, error));
|
|
3656
|
+
}
|
|
3657
|
+
completeBackfillAttempt(attempt, result) {
|
|
3658
|
+
if (this.isShuttingDown) return;
|
|
3659
|
+
const current = this.backfills.stateFor(attempt.agentName);
|
|
3660
|
+
if (current?.status === "running" && current.attemptId === attempt.attemptId) {
|
|
3661
|
+
this.flushProgressStatus(`backfill:${attempt.agentName}`);
|
|
3662
|
+
if (this.backfills.complete(attempt, result)) {
|
|
3663
|
+
if (result === "failed") {
|
|
3664
|
+
this.cacheIntegrityValidUntilByAgent.delete(attempt.agentName);
|
|
3665
|
+
} else if (result === "committed") {
|
|
3666
|
+
this.cacheIntegrityValidUntilByAgent.set(
|
|
3667
|
+
attempt.agentName,
|
|
3668
|
+
Date.now() + BACKFILL_INTERVAL_MS
|
|
3669
|
+
);
|
|
2392
3670
|
}
|
|
2393
|
-
this.
|
|
2394
|
-
(failedAgent) => failedAgent !== agentName
|
|
2395
|
-
);
|
|
2396
|
-
} else if (!this.failedBackfillAgents.includes(agentName)) {
|
|
2397
|
-
this.failedBackfillAgents.push(agentName);
|
|
2398
|
-
this.cacheIntegrityCheckedAgents.delete(agentName);
|
|
3671
|
+
this.publishBackfillStatus();
|
|
2399
3672
|
}
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
});
|
|
3673
|
+
}
|
|
3674
|
+
this.pumpBackfillQueue();
|
|
2403
3675
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
3676
|
+
rejectBackfillAttempt(attempt, error) {
|
|
3677
|
+
appLogger.error("scan.backfill.queue_error", { agent: attempt.agentName, error });
|
|
3678
|
+
this.completeBackfillAttempt(attempt, "failed");
|
|
2406
3679
|
}
|
|
2407
|
-
|
|
3680
|
+
runBackfill(attempt) {
|
|
3681
|
+
return this.scheduler.run(attempt.agentName, "backfill", () => this.performBackfill(attempt));
|
|
3682
|
+
}
|
|
3683
|
+
async performBackfill(attempt) {
|
|
3684
|
+
const { agentName } = attempt;
|
|
2408
3685
|
const startedAt = performance.now();
|
|
2409
3686
|
const agent = this.findAgent(agentName);
|
|
2410
3687
|
if (!agent || !agent.isAvailable()) return "skipped";
|
|
@@ -2414,25 +3691,42 @@ var AgentSyncEngine = class {
|
|
|
2414
3691
|
const meta = cached?.meta ?? buildAgentCacheMeta(agent);
|
|
2415
3692
|
const backfillCursor = getAgentFullSyncCursor(agentName);
|
|
2416
3693
|
if (cached) restoreAgentCacheMeta(agent, cached);
|
|
3694
|
+
let durableCommitted = false;
|
|
2417
3695
|
try {
|
|
2418
3696
|
markAgentFullSyncStarted(agentName);
|
|
3697
|
+
const operation = agent instanceof FileSystemSessionSource ? { kind: "source-backfill", cursor: backfillCursor, checkpoint: "durable" } : { kind: "full-backfill", cursor: backfillCursor, checkpoint: "durable" };
|
|
3698
|
+
appLogger.info("scan.backfill.started", {
|
|
3699
|
+
agent: agentName,
|
|
3700
|
+
cursor: backfillCursor ?? void 0,
|
|
3701
|
+
durable_checkpoints: operation.checkpoint === "durable"
|
|
3702
|
+
});
|
|
2419
3703
|
const result = await this.runWorker(
|
|
2420
3704
|
agent,
|
|
2421
3705
|
baseline,
|
|
2422
|
-
|
|
3706
|
+
operation,
|
|
2423
3707
|
{},
|
|
2424
3708
|
{
|
|
2425
|
-
|
|
2426
|
-
backfill: true,
|
|
2427
|
-
backfillCursor,
|
|
3709
|
+
backfillAttempt: attempt,
|
|
2428
3710
|
meta,
|
|
2429
|
-
|
|
3711
|
+
onCheckpoint: (checkpoint) => this.handleBackfillCheckpoint(agentName, checkpoint)
|
|
2430
3712
|
}
|
|
2431
3713
|
);
|
|
2432
3714
|
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2433
3715
|
const fullSessions = attachMissingProjectIdentities(result.sessions);
|
|
2434
|
-
this.
|
|
2435
|
-
|
|
3716
|
+
this.flushProgressStatus(`backfill:${agentName}`);
|
|
3717
|
+
const updatePublicationPhase = (phase) => {
|
|
3718
|
+
if (this.backfills.updateProgress(attempt, {
|
|
3719
|
+
phase,
|
|
3720
|
+
sessions: fullSessions.length
|
|
3721
|
+
})) {
|
|
3722
|
+
this.publishProgressStatus(
|
|
3723
|
+
`backfill:${agentName}`,
|
|
3724
|
+
phase,
|
|
3725
|
+
this.scanStatus.updateBackfill(this.backfills.status())
|
|
3726
|
+
);
|
|
3727
|
+
}
|
|
3728
|
+
};
|
|
3729
|
+
updatePublicationPhase("publish-queued");
|
|
2436
3730
|
await this.commitSessionPublication({
|
|
2437
3731
|
context: "scan.backfill",
|
|
2438
3732
|
agentName,
|
|
@@ -2444,9 +3738,15 @@ var AgentSyncEngine = class {
|
|
|
2444
3738
|
agentName,
|
|
2445
3739
|
sessions: fullSessions,
|
|
2446
3740
|
meta: buildAgentCacheMeta(agent),
|
|
3741
|
+
completeness: result.completeness,
|
|
3742
|
+
removedSessionIds: result.explicitRemovedSessionIds,
|
|
2447
3743
|
saveCache: true
|
|
2448
|
-
}
|
|
3744
|
+
},
|
|
3745
|
+
onPublishing: () => updatePublicationPhase("publishing")
|
|
2449
3746
|
});
|
|
3747
|
+
durableCommitted = true;
|
|
3748
|
+
this.options.workerRunner.commit?.(agentName);
|
|
3749
|
+
if (result.sourceFailures?.length) throw sourceFailureError(result.sourceFailures);
|
|
2450
3750
|
markAgentFullSyncCompleted(agentName);
|
|
2451
3751
|
appLogger.info("scan.backfill.done", {
|
|
2452
3752
|
agent: agentName,
|
|
@@ -2456,38 +3756,39 @@ var AgentSyncEngine = class {
|
|
|
2456
3756
|
});
|
|
2457
3757
|
return "committed";
|
|
2458
3758
|
} catch (error) {
|
|
3759
|
+
if (!durableCommitted) agent.setSessionMetaMap(new Map(Object.entries(meta)));
|
|
3760
|
+
this.options.workerRunner.discard?.(agentName);
|
|
2459
3761
|
appLogger.error("scan.backfill.error", { agent: agentName, error });
|
|
2460
3762
|
console.error(`[${agentName}] Backfill failed:`, error);
|
|
2461
3763
|
return "failed";
|
|
2462
3764
|
}
|
|
2463
3765
|
}
|
|
2464
|
-
backfillStatus() {
|
|
2465
|
-
return {
|
|
2466
|
-
active: this.currentBackfillAgent != null || this.backfillQueue.length > 0,
|
|
2467
|
-
pendingAgents: [...this.backfillQueue],
|
|
2468
|
-
currentAgent: this.currentBackfillAgent,
|
|
2469
|
-
progress: this.currentBackfillProgress,
|
|
2470
|
-
completedAgents: [...this.completedBackfillAgents],
|
|
2471
|
-
failedAgents: [...this.failedBackfillAgents]
|
|
2472
|
-
};
|
|
2473
|
-
}
|
|
2474
3766
|
buildFullSearchIndexJobs(context) {
|
|
2475
3767
|
const snapshot = this.sessionIndex.snapshot();
|
|
2476
|
-
return snapshot.agents.
|
|
3768
|
+
return snapshot.agents.flatMap((agent) => {
|
|
3769
|
+
if (!(agent.name in snapshot.byAgent)) return [];
|
|
2477
3770
|
const cached = loadCachedSessions(agent.name);
|
|
2478
|
-
return
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
3771
|
+
return [
|
|
3772
|
+
cached ? {
|
|
3773
|
+
kind: "full",
|
|
3774
|
+
context,
|
|
3775
|
+
agentName: agent.name,
|
|
3776
|
+
sessions: cached.sessions,
|
|
3777
|
+
meta: cached.meta,
|
|
3778
|
+
completeness: "partial",
|
|
3779
|
+
removedSessionIds: [],
|
|
3780
|
+
searchIndexOptions: { includePendingReindex: false }
|
|
3781
|
+
} : {
|
|
3782
|
+
kind: "full",
|
|
3783
|
+
context,
|
|
3784
|
+
agentName: agent.name,
|
|
3785
|
+
sessions: snapshot.byAgent[agent.name] ?? [],
|
|
3786
|
+
meta: buildAgentCacheMeta(agent),
|
|
3787
|
+
completeness: "partial",
|
|
3788
|
+
removedSessionIds: [],
|
|
3789
|
+
searchIndexOptions: { includePendingReindex: false }
|
|
3790
|
+
}
|
|
3791
|
+
];
|
|
2491
3792
|
});
|
|
2492
3793
|
}
|
|
2493
3794
|
publicationId(context, agentName) {
|
|
@@ -2503,7 +3804,11 @@ var AgentSyncEngine = class {
|
|
|
2503
3804
|
jobs: jobs.length
|
|
2504
3805
|
});
|
|
2505
3806
|
try {
|
|
2506
|
-
|
|
3807
|
+
const publicationJobs = jobs.map((job) => ({
|
|
3808
|
+
...job,
|
|
3809
|
+
publicationId: details.publicationId
|
|
3810
|
+
}));
|
|
3811
|
+
await (details.onStarted ? this.searchIndexJobs.enqueue(context, publicationJobs, details.onStarted) : this.searchIndexJobs.enqueue(context, publicationJobs));
|
|
2507
3812
|
} catch (error) {
|
|
2508
3813
|
appLogger.error("session.publication.failed", {
|
|
2509
3814
|
publication_id: details.publicationId,
|
|
@@ -2524,7 +3829,8 @@ var AgentSyncEngine = class {
|
|
|
2524
3829
|
const publicationId = this.publicationId(publication.context, publication.agentName);
|
|
2525
3830
|
await this.commitSearchIndex(publication.context, [publication.indexJob], {
|
|
2526
3831
|
publicationId,
|
|
2527
|
-
agent: publication.agentName
|
|
3832
|
+
agent: publication.agentName,
|
|
3833
|
+
...publication.onPublishing ? { onStarted: publication.onPublishing } : {}
|
|
2528
3834
|
});
|
|
2529
3835
|
const diffStartedAt = performance.now();
|
|
2530
3836
|
const event = this.sessionIndex.commitAgentSessions(
|
|
@@ -2701,7 +4007,7 @@ var SessionWatcher = class {
|
|
|
2701
4007
|
}
|
|
2702
4008
|
}
|
|
2703
4009
|
this.stablePaths.clear();
|
|
2704
|
-
await Promise.all(this.watchers.map((watcher) => watcher.close()));
|
|
4010
|
+
await Promise.all(this.watchers.map(({ watcher }) => watcher.close()));
|
|
2705
4011
|
this.watchers = [];
|
|
2706
4012
|
this.fallbackWatchScopes.clear();
|
|
2707
4013
|
this.listeners.clear();
|
|
@@ -2724,7 +4030,7 @@ var SessionWatcher = class {
|
|
|
2724
4030
|
watcher.on("error", (error) => {
|
|
2725
4031
|
this.reportWatchError("watch.error", { path, recursive, error });
|
|
2726
4032
|
});
|
|
2727
|
-
this.watchers.push(watcher);
|
|
4033
|
+
this.watchers.push({ path, recursive, watcher });
|
|
2728
4034
|
return true;
|
|
2729
4035
|
} catch (error) {
|
|
2730
4036
|
if (recursive && isRecursiveWatchUnavailable(error)) {
|
|
@@ -2770,7 +4076,20 @@ var SessionWatcher = class {
|
|
|
2770
4076
|
this.watchDirectoryTree(path, scopes);
|
|
2771
4077
|
}
|
|
2772
4078
|
} catch {
|
|
4079
|
+
this.unwatchFallbackTree(path);
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
unwatchFallbackTree(path) {
|
|
4083
|
+
const active = [];
|
|
4084
|
+
for (const registration of this.watchers) {
|
|
4085
|
+
if (registration.recursive || !isSameOrChildPath(path, registration.path)) {
|
|
4086
|
+
active.push(registration);
|
|
4087
|
+
continue;
|
|
4088
|
+
}
|
|
4089
|
+
registration.watcher.close();
|
|
4090
|
+
this.fallbackWatchScopes.delete(registration.path);
|
|
2773
4091
|
}
|
|
4092
|
+
this.watchers = active;
|
|
2774
4093
|
}
|
|
2775
4094
|
handleWatchEvent(watchPath, scopes, eventType, filename) {
|
|
2776
4095
|
const changedPath = resolveWatchEventPath(watchPath, filename);
|
|
@@ -2852,6 +4171,7 @@ var SessionWatcher = class {
|
|
|
2852
4171
|
import { Worker as Worker2 } from "worker_threads";
|
|
2853
4172
|
var SHUTDOWN_ERROR_MESSAGE2 = "Scan refresh worker shut down";
|
|
2854
4173
|
function applySessionChanges2(previousSessions, changes, removedSessionIds) {
|
|
4174
|
+
if (changes.length === 0 && removedSessionIds.length === 0) return previousSessions;
|
|
2855
4175
|
const replacedIds = new Set(removedSessionIds);
|
|
2856
4176
|
for (const { session } of changes) replacedIds.add(session.id);
|
|
2857
4177
|
const retained = previousSessions.filter((session) => !replacedIds.has(session.id));
|
|
@@ -2874,8 +4194,10 @@ function applySessionChanges2(previousSessions, changes, removedSessionIds) {
|
|
|
2874
4194
|
return next;
|
|
2875
4195
|
}
|
|
2876
4196
|
function applyMetaChanges(previous, changed, replacedSessionIds) {
|
|
4197
|
+
const replacedIds = [...replacedSessionIds];
|
|
4198
|
+
if (Object.keys(changed).length === 0 && replacedIds.length === 0) return previous;
|
|
2877
4199
|
const next = { ...previous };
|
|
2878
|
-
for (const id of
|
|
4200
|
+
for (const id of replacedIds) delete next[id];
|
|
2879
4201
|
return Object.assign(next, changed);
|
|
2880
4202
|
}
|
|
2881
4203
|
var ThreadWorkerRunner = class {
|
|
@@ -2888,23 +4210,27 @@ var ThreadWorkerRunner = class {
|
|
|
2888
4210
|
isShuttingDown = false;
|
|
2889
4211
|
get activeCount() {
|
|
2890
4212
|
let count = 0;
|
|
2891
|
-
for (const slot of this.workers.values())
|
|
4213
|
+
for (const slot of this.workers.values()) {
|
|
4214
|
+
count += slot.pending.size + Number(slot.awaitingCommit != null);
|
|
4215
|
+
}
|
|
2892
4216
|
return count;
|
|
2893
4217
|
}
|
|
2894
4218
|
run(agentName, payload) {
|
|
2895
4219
|
if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE2));
|
|
4220
|
+
const existingSlot = this.workers.get(agentName);
|
|
4221
|
+
if (existingSlot && (existingSlot.pending.size > 0 || existingSlot.awaitingCommit)) {
|
|
4222
|
+
return Promise.reject(new Error(`Scan refresh worker for ${agentName} is busy`));
|
|
4223
|
+
}
|
|
4224
|
+
const generation = existingSlot?.generation ?? 0;
|
|
2896
4225
|
const request = {
|
|
2897
4226
|
type: "run",
|
|
2898
4227
|
requestId: this.nextRequestId++,
|
|
2899
4228
|
agentName,
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
backfill: payload.backfill,
|
|
2904
|
-
backfillCursor: payload.backfillCursor,
|
|
2905
|
-
checkpoint: payload.checkpoint,
|
|
4229
|
+
generation,
|
|
4230
|
+
pricingGenerationId: getPricingGeneration().id,
|
|
4231
|
+
operation: payload.operation,
|
|
2906
4232
|
scanOptions: payload.scanOptions,
|
|
2907
|
-
meta: payload.meta
|
|
4233
|
+
...existingSlot ? {} : { previousSessions: payload.previousSessions, meta: payload.meta }
|
|
2908
4234
|
};
|
|
2909
4235
|
return new Promise((resolve4, reject) => {
|
|
2910
4236
|
let slot = this.workers.get(agentName);
|
|
@@ -2913,7 +4239,7 @@ var ThreadWorkerRunner = class {
|
|
|
2913
4239
|
try {
|
|
2914
4240
|
slot = this.createWorker(agentName, request);
|
|
2915
4241
|
} catch (error) {
|
|
2916
|
-
reject(
|
|
4242
|
+
reject(toError(error));
|
|
2917
4243
|
return;
|
|
2918
4244
|
}
|
|
2919
4245
|
}
|
|
@@ -2921,6 +4247,7 @@ var ThreadWorkerRunner = class {
|
|
|
2921
4247
|
resolve: resolve4,
|
|
2922
4248
|
reject,
|
|
2923
4249
|
payload,
|
|
4250
|
+
generation,
|
|
2924
4251
|
onProgress: payload.onProgress,
|
|
2925
4252
|
onCheckpoint: payload.onCheckpoint
|
|
2926
4253
|
});
|
|
@@ -2929,11 +4256,33 @@ var ThreadWorkerRunner = class {
|
|
|
2929
4256
|
slot.worker.postMessage(request);
|
|
2930
4257
|
} catch (error) {
|
|
2931
4258
|
slot.pending.delete(request.requestId);
|
|
2932
|
-
reject(
|
|
4259
|
+
reject(toError(error));
|
|
4260
|
+
this.invalidateWorker(agentName, slot);
|
|
2933
4261
|
}
|
|
2934
4262
|
}
|
|
2935
4263
|
});
|
|
2936
4264
|
}
|
|
4265
|
+
commit(agentName) {
|
|
4266
|
+
const slot = this.workers.get(agentName);
|
|
4267
|
+
const awaiting = slot?.awaitingCommit;
|
|
4268
|
+
if (!slot || !awaiting) return;
|
|
4269
|
+
const request = {
|
|
4270
|
+
type: "commit",
|
|
4271
|
+
requestId: awaiting.requestId,
|
|
4272
|
+
generation: awaiting.generation
|
|
4273
|
+
};
|
|
4274
|
+
try {
|
|
4275
|
+
slot.worker.postMessage(request);
|
|
4276
|
+
slot.awaitingCommit = null;
|
|
4277
|
+
slot.generation += 1;
|
|
4278
|
+
} catch {
|
|
4279
|
+
this.invalidateWorker(agentName, slot);
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
discard(agentName) {
|
|
4283
|
+
const slot = this.workers.get(agentName);
|
|
4284
|
+
if (slot) this.invalidateWorker(agentName, slot);
|
|
4285
|
+
}
|
|
2937
4286
|
async shutdown() {
|
|
2938
4287
|
this.isShuttingDown = true;
|
|
2939
4288
|
const slots = [...this.workers.values()];
|
|
@@ -2948,14 +4297,22 @@ var ThreadWorkerRunner = class {
|
|
|
2948
4297
|
}
|
|
2949
4298
|
createWorker(agentName, request) {
|
|
2950
4299
|
const worker = new Worker2(this.workerUrl, { workerData: request });
|
|
2951
|
-
const slot = {
|
|
4300
|
+
const slot = {
|
|
4301
|
+
agentName,
|
|
4302
|
+
worker,
|
|
4303
|
+
pending: /* @__PURE__ */ new Map(),
|
|
4304
|
+
generation: request.generation,
|
|
4305
|
+
awaitingCommit: null,
|
|
4306
|
+
closed: false
|
|
4307
|
+
};
|
|
2952
4308
|
worker.unref();
|
|
2953
4309
|
this.workers.set(agentName, slot);
|
|
2954
4310
|
worker.on("message", (message) => {
|
|
4311
|
+
if (appLogger.consumeWorkerMessage(message)) return;
|
|
2955
4312
|
this.handleMessage(slot, message);
|
|
2956
4313
|
});
|
|
2957
4314
|
worker.on("error", (error) => {
|
|
2958
|
-
this.closeWorker(agentName, slot, error);
|
|
4315
|
+
this.closeWorker(agentName, slot, toError(error));
|
|
2959
4316
|
});
|
|
2960
4317
|
worker.on("exit", (code) => {
|
|
2961
4318
|
if (slot.closed) return;
|
|
@@ -2970,34 +4327,59 @@ var ThreadWorkerRunner = class {
|
|
|
2970
4327
|
handleMessage(slot, message) {
|
|
2971
4328
|
const pending = slot.pending.get(message.requestId);
|
|
2972
4329
|
if (!pending) return;
|
|
4330
|
+
if (message.generation !== pending.generation) {
|
|
4331
|
+
const error = new Error(
|
|
4332
|
+
`Scan refresh generation mismatch: expected ${pending.generation}, received ${message.generation}`
|
|
4333
|
+
);
|
|
4334
|
+
slot.pending.delete(message.requestId);
|
|
4335
|
+
pending.reject(error);
|
|
4336
|
+
this.invalidateWorker(slot.agentName, slot);
|
|
4337
|
+
return;
|
|
4338
|
+
}
|
|
2973
4339
|
if (message.type === "progress") {
|
|
2974
4340
|
pending.onProgress?.(message.progress);
|
|
2975
4341
|
return;
|
|
2976
4342
|
}
|
|
2977
4343
|
if (message.type === "checkpoint") {
|
|
2978
|
-
|
|
4344
|
+
try {
|
|
4345
|
+
pending.onCheckpoint?.(message.checkpoint);
|
|
4346
|
+
} catch (error) {
|
|
4347
|
+
slot.pending.delete(message.requestId);
|
|
4348
|
+
pending.reject(toError(error));
|
|
4349
|
+
this.invalidateWorker(slot.agentName, slot);
|
|
4350
|
+
}
|
|
2979
4351
|
return;
|
|
2980
4352
|
}
|
|
2981
4353
|
slot.pending.delete(message.requestId);
|
|
2982
4354
|
if (message.type === "error") {
|
|
2983
4355
|
pending.reject(new Error(message.error));
|
|
4356
|
+
this.invalidateWorker(slot.agentName, slot);
|
|
2984
4357
|
return;
|
|
2985
4358
|
}
|
|
2986
4359
|
const changedIds = message.changes.map(({ session }) => session.id);
|
|
2987
4360
|
const replacedSessionIds = [...changedIds, ...message.removedSessionIds];
|
|
2988
4361
|
const removedMetaIds = [...message.removedSessionIds, ...message.removedMetaIds];
|
|
2989
4362
|
try {
|
|
2990
|
-
|
|
4363
|
+
const result = {
|
|
2991
4364
|
sessions: applySessionChanges2(
|
|
2992
4365
|
pending.payload.previousSessions,
|
|
2993
4366
|
message.changes,
|
|
2994
4367
|
message.removedSessionIds
|
|
2995
4368
|
),
|
|
2996
4369
|
meta: applyMetaChanges(pending.payload.meta, message.meta, removedMetaIds),
|
|
2997
|
-
changedIds:
|
|
2998
|
-
|
|
4370
|
+
changedIds: replacedSessionIds,
|
|
4371
|
+
sourceFailures: message.sourceFailures,
|
|
4372
|
+
completeness: message.completeness,
|
|
4373
|
+
explicitRemovedSessionIds: message.explicitRemovedSessionIds
|
|
4374
|
+
};
|
|
4375
|
+
slot.awaitingCommit = {
|
|
4376
|
+
requestId: message.requestId,
|
|
4377
|
+
generation: message.generation
|
|
4378
|
+
};
|
|
4379
|
+
pending.resolve(result);
|
|
2999
4380
|
} catch (error) {
|
|
3000
|
-
pending.reject(
|
|
4381
|
+
pending.reject(toError(error));
|
|
4382
|
+
this.invalidateWorker(slot.agentName, slot);
|
|
3001
4383
|
}
|
|
3002
4384
|
}
|
|
3003
4385
|
closeWorker(agentName, slot, error) {
|
|
@@ -3006,6 +4388,15 @@ var ThreadWorkerRunner = class {
|
|
|
3006
4388
|
if (this.workers.get(agentName) === slot) this.workers.delete(agentName);
|
|
3007
4389
|
for (const pending of slot.pending.values()) pending.reject(error);
|
|
3008
4390
|
slot.pending.clear();
|
|
4391
|
+
slot.awaitingCommit = null;
|
|
4392
|
+
}
|
|
4393
|
+
invalidateWorker(agentName, slot) {
|
|
4394
|
+
this.closeWorker(
|
|
4395
|
+
agentName,
|
|
4396
|
+
slot,
|
|
4397
|
+
new Error(`Scan refresh worker state discarded for ${agentName}`)
|
|
4398
|
+
);
|
|
4399
|
+
void slot.worker.terminate().catch(() => void 0);
|
|
3009
4400
|
}
|
|
3010
4401
|
};
|
|
3011
4402
|
|
|
@@ -3128,6 +4519,7 @@ var LiveScanStore = class {
|
|
|
3128
4519
|
await this.watcher.dispose();
|
|
3129
4520
|
this.watcher = null;
|
|
3130
4521
|
}
|
|
4522
|
+
closeCacheStorage();
|
|
3131
4523
|
}
|
|
3132
4524
|
emit(event) {
|
|
3133
4525
|
if (this.shuttingDown) return;
|
|
@@ -3192,6 +4584,12 @@ function printScanResults(agents) {
|
|
|
3192
4584
|
}
|
|
3193
4585
|
|
|
3194
4586
|
// src/session-index-output.ts
|
|
4587
|
+
function formatScanFailureDiagnostics(snapshot) {
|
|
4588
|
+
return Object.values(snapshot.scanFailures ?? {}).map((failure) => {
|
|
4589
|
+
const source = failure.sourcePath ? ` at ${failure.sourcePath}` : "";
|
|
4590
|
+
return `[${failure.agentName}] Scan failed during ${failure.stage}${source} (${failure.errorClass}): ${failure.message}`;
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
3195
4593
|
function buildSessionIndexOutput(snapshot, window = {}) {
|
|
3196
4594
|
const sessions = filterSessionTreeByActivityWindow(snapshot.sessions, window.from, window.to);
|
|
3197
4595
|
const agents = getAgentInfoMap(
|
|
@@ -3387,12 +4785,6 @@ var main = defineCommand({
|
|
|
3387
4785
|
const clearCache = args["clear-cache"];
|
|
3388
4786
|
const hostname = args.host;
|
|
3389
4787
|
const remoteAccess = args["remote-access"];
|
|
3390
|
-
if (!isLoopbackHostname(hostname) && !remoteAccess) {
|
|
3391
|
-
console.error(
|
|
3392
|
-
`Refusing to expose CodeSesh on ${hostname} without authentication. Add --remote-access to continue.`
|
|
3393
|
-
);
|
|
3394
|
-
process.exit(1);
|
|
3395
|
-
}
|
|
3396
4788
|
let transport;
|
|
3397
4789
|
try {
|
|
3398
4790
|
transport = resolveRemoteTransport({
|
|
@@ -3405,6 +4797,12 @@ var main = defineCommand({
|
|
|
3405
4797
|
console.error(error instanceof Error ? error.message : String(error));
|
|
3406
4798
|
process.exit(1);
|
|
3407
4799
|
}
|
|
4800
|
+
if (resolveRemoteAccessPolicy(hostname, transport).authenticationRequired && !remoteAccess) {
|
|
4801
|
+
console.error(
|
|
4802
|
+
`Refusing to expose CodeSesh on ${hostname} without authentication. Add --remote-access to continue.`
|
|
4803
|
+
);
|
|
4804
|
+
process.exit(1);
|
|
4805
|
+
}
|
|
3408
4806
|
if (trace) {
|
|
3409
4807
|
perf.enable();
|
|
3410
4808
|
}
|
|
@@ -3418,7 +4816,7 @@ var main = defineCommand({
|
|
|
3418
4816
|
log_path: appLogger.getLogPath()
|
|
3419
4817
|
});
|
|
3420
4818
|
if (clearCache) {
|
|
3421
|
-
const { clearCache: clear } = await import("./dist-
|
|
4819
|
+
const { clearCache: clear } = await import("./dist-N2PQATP2.js");
|
|
3422
4820
|
clear();
|
|
3423
4821
|
appLogger.info("cache.clear");
|
|
3424
4822
|
console.log("Cache cleared.");
|
|
@@ -3468,6 +4866,13 @@ var main = defineCommand({
|
|
|
3468
4866
|
}
|
|
3469
4867
|
if (jsonOnly) {
|
|
3470
4868
|
await pricingRefresh.cancel();
|
|
4869
|
+
const scanFailureDiagnostics = formatScanFailureDiagnostics(result);
|
|
4870
|
+
if (scanFailureDiagnostics.length > 0) {
|
|
4871
|
+
await store.shutdown();
|
|
4872
|
+
for (const diagnostic of scanFailureDiagnostics) console.error(diagnostic);
|
|
4873
|
+
process.exitCode = 1;
|
|
4874
|
+
return;
|
|
4875
|
+
}
|
|
3471
4876
|
const output = buildSessionIndexOutput(result, { from: listDefaultFrom, to: listDefaultTo });
|
|
3472
4877
|
appLogger.info("cli.json_output", {
|
|
3473
4878
|
sessions: output.sessions.length,
|