codesesh 1.0.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 +27 -4
- package/dist/{chunk-BEF6LLRG.js → chunk-JHX5OLL4.js} +4010 -2248
- package/dist/chunk-JHX5OLL4.js.map +1 -0
- package/dist/{chunk-TIHF4T3K.js → chunk-OFTTM3UD.js} +80 -16
- package/dist/chunk-OFTTM3UD.js.map +1 -0
- package/dist/{dist-3F467V44.js → dist-N2PQATP2.js} +40 -4
- package/dist/index.js +1818 -454
- 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 -50
- 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/OverviewScreen-DriPmCam.js +1 -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/agents-BkRP_ww3.js +1 -0
- package/dist/web/assets/contract-V6QuD0UY.js +1 -0
- package/dist/web/assets/index-B2RqUgZ5.css +1 -0
- package/dist/web/assets/index-BgWoUD_4.js +1410 -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/utils-DgUgu15E.js +1 -0
- package/dist/web/index.html +10 -32
- package/dist/web/theme-bootstrap.js +19 -0
- package/package.json +10 -10
- package/dist/chunk-BEF6LLRG.js.map +0 -1
- package/dist/chunk-TIHF4T3K.js.map +0 -1
- package/dist/web/assets/ErrorBoundary-BY-N1QzJ.js +0 -1
- package/dist/web/assets/OverviewScreen-_ZaVjhxN.js +0 -1
- package/dist/web/assets/Projects-q6cEm2dc.js +0 -1
- package/dist/web/assets/SearchFilterBar-CrVBpo40.js +0 -1
- package/dist/web/assets/SearchResultsPanel-Cg5V8Ia6.js +0 -1
- package/dist/web/assets/SessionDetail-BSSuddsZ.js +0 -54
- package/dist/web/assets/chunk-62JRHF6Z-BWRsEBnL.js +0 -4
- package/dist/web/assets/index-C3KgxGwE.js +0 -1410
- package/dist/web/assets/index-DQX42cFQ.css +0 -1
- package/dist/web/assets/panel-B7kuE60V.js +0 -1
- package/dist/web/assets/session-indexes-DmnjZ_JL.js +0 -1
- package/dist/web/assets/utils-t2u9IbNT.js +0 -1
- /package/dist/{dist-3F467V44.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,25 +28,25 @@ import {
|
|
|
24
28
|
formatSessionReference,
|
|
25
29
|
getAgentFullSyncCursor,
|
|
26
30
|
getAgentInfoMap,
|
|
27
|
-
|
|
31
|
+
getPricingGeneration,
|
|
28
32
|
getSessionActivityTime,
|
|
29
33
|
getSessionAgentKey,
|
|
30
34
|
getSessionRouteKey,
|
|
31
35
|
importBookmarks,
|
|
32
|
-
isAgentCacheInitialized,
|
|
33
36
|
isProjectIdentityKind,
|
|
34
37
|
listBookmarks,
|
|
35
38
|
listCachedProjectGroups,
|
|
36
39
|
listFileActivity,
|
|
37
40
|
listModelCostDistribution,
|
|
38
41
|
listSessionAliases,
|
|
42
|
+
loadCachedSessionHeads,
|
|
39
43
|
loadCachedSessions,
|
|
40
|
-
markAgentCacheInitialized,
|
|
41
44
|
markAgentFullSyncCompleted,
|
|
42
45
|
markAgentFullSyncProgress,
|
|
43
46
|
markAgentFullSyncStarted,
|
|
44
47
|
matchesProjectIdentity,
|
|
45
48
|
matchesProjectScope,
|
|
49
|
+
materializeBookmarkViews,
|
|
46
50
|
materializeSessionDetailResponse,
|
|
47
51
|
mergeSearchQueryOptions,
|
|
48
52
|
mergeSessionsUpdatedEvents,
|
|
@@ -50,9 +54,10 @@ import {
|
|
|
50
54
|
normalizeSessionReference,
|
|
51
55
|
perf,
|
|
52
56
|
publishPendingPricing,
|
|
57
|
+
readAgentCacheInitialization,
|
|
58
|
+
readAgentLastFullSyncAt,
|
|
59
|
+
readPendingSearchIndexMaintenance,
|
|
53
60
|
refreshPricingCache,
|
|
54
|
-
saveCachedSessionChanges,
|
|
55
|
-
saveCachedSessions,
|
|
56
61
|
scanSessions,
|
|
57
62
|
sessionRoutePath,
|
|
58
63
|
sessionSignature,
|
|
@@ -60,7 +65,7 @@ import {
|
|
|
60
65
|
startOfCalendarDay,
|
|
61
66
|
upsertBookmark,
|
|
62
67
|
upsertSessionAlias
|
|
63
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-JHX5OLL4.js";
|
|
64
69
|
|
|
65
70
|
// src/index.ts
|
|
66
71
|
import { defineCommand, runMain } from "citty";
|
|
@@ -69,6 +74,7 @@ import { defineCommand, runMain } from "citty";
|
|
|
69
74
|
import { Hono as Hono2 } from "hono";
|
|
70
75
|
import { bodyLimit } from "hono/body-limit";
|
|
71
76
|
import { compress } from "hono/compress";
|
|
77
|
+
import { secureHeaders } from "hono/secure-headers";
|
|
72
78
|
import { serve } from "@hono/node-server";
|
|
73
79
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
74
80
|
import { existsSync } from "fs";
|
|
@@ -151,8 +157,9 @@ var SMART_TAGS = [
|
|
|
151
157
|
"exploration",
|
|
152
158
|
"planning"
|
|
153
159
|
];
|
|
154
|
-
var
|
|
155
|
-
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 };
|
|
156
163
|
function searchParams(c) {
|
|
157
164
|
return new URL(c.req.url ?? "http://localhost/", "http://localhost/").searchParams;
|
|
158
165
|
}
|
|
@@ -166,15 +173,51 @@ function optionalQueryValue(value) {
|
|
|
166
173
|
return normalized ? normalized : void 0;
|
|
167
174
|
}
|
|
168
175
|
function parseDateParam(value, fallback) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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 };
|
|
172
190
|
}
|
|
173
191
|
function parseNumberParam(value) {
|
|
174
192
|
if (value == null || !value.trim()) return void 0;
|
|
175
193
|
const number = Number(value);
|
|
176
194
|
return Number.isFinite(number) ? number : void 0;
|
|
177
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
|
+
}
|
|
178
221
|
function parseSmartTags(values) {
|
|
179
222
|
const tags = values.map((value) => value.toLowerCase()).filter((value) => SMART_TAGS.includes(value));
|
|
180
223
|
return tags.length > 0 ? [...new Set(tags)] : void 0;
|
|
@@ -192,11 +235,10 @@ function parseProjectIdentityFilter(kindValue, keyValue) {
|
|
|
192
235
|
if (!kind || !key || !isProjectIdentityKind(kind)) return null;
|
|
193
236
|
return { kind, key };
|
|
194
237
|
}
|
|
195
|
-
function parseSearchOptions(c,
|
|
238
|
+
function parseSearchOptions(c, window, request, projectIdentity) {
|
|
196
239
|
const params = searchParams(c);
|
|
197
|
-
const limitValue = parseNumberParam(params.get("limit") ?? void 0);
|
|
198
240
|
return {
|
|
199
|
-
agent:
|
|
241
|
+
agent: request.agent,
|
|
200
242
|
project: optionalQueryValue(params.get("project") ?? void 0),
|
|
201
243
|
projectKind: projectIdentity?.kind,
|
|
202
244
|
projectKey: projectIdentity?.key,
|
|
@@ -209,15 +251,72 @@ function parseSearchOptions(c, defaults, projectIdentity) {
|
|
|
209
251
|
),
|
|
210
252
|
costMin: parseNumberParam(params.get("costMin") ?? void 0),
|
|
211
253
|
costMax: parseNumberParam(params.get("costMax") ?? void 0),
|
|
212
|
-
from:
|
|
213
|
-
to:
|
|
214
|
-
limit:
|
|
254
|
+
from: window.from,
|
|
255
|
+
to: window.to,
|
|
256
|
+
limit: request.limit
|
|
215
257
|
};
|
|
216
258
|
}
|
|
217
259
|
function filterSessionsByActivityWindow(sessions, from, to) {
|
|
218
260
|
return filterSessionTreeByActivityWindow(sessions, from, to);
|
|
219
261
|
}
|
|
220
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
|
+
|
|
221
320
|
// src/api/session-aliases-view.ts
|
|
222
321
|
function aliasKey(reference) {
|
|
223
322
|
return `${reference.agentName.toLowerCase()}\0${reference.sessionId}`;
|
|
@@ -254,10 +353,14 @@ function invalidateAliasView() {
|
|
|
254
353
|
cachedView = null;
|
|
255
354
|
}
|
|
256
355
|
function decorateBookmark(bookmark, aliases) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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;
|
|
261
364
|
}
|
|
262
365
|
function decorateFileActivity(activity, aliases) {
|
|
263
366
|
return {
|
|
@@ -288,15 +391,34 @@ function findAliasSearchResults(query, options, scanResult, aliases) {
|
|
|
288
391
|
reference: alias.reference,
|
|
289
392
|
session: aliases.decorate(session, alias.reference),
|
|
290
393
|
snippet: `Alias \xB7 ${session.directory}`,
|
|
394
|
+
snippetHighlights: [],
|
|
291
395
|
matchType: "title"
|
|
292
396
|
});
|
|
293
397
|
}
|
|
294
|
-
return filterSessionSearchCandidates(results, search.options).sort(
|
|
398
|
+
return filterSessionSearchCandidates(results, search.options, scanResult.sessions).sort(
|
|
295
399
|
(a, b) => getSessionActivityTime(b.session) - getSessionActivityTime(a.session)
|
|
296
400
|
);
|
|
297
401
|
}
|
|
298
402
|
|
|
299
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
|
+
}
|
|
300
422
|
var SNAPSHOT_AGGREGATION_CACHE_LIMIT = 64;
|
|
301
423
|
var snapshotAggregationCaches = /* @__PURE__ */ new WeakMap();
|
|
302
424
|
function getSnapshotAggregation(source, sessions, key, build) {
|
|
@@ -328,25 +450,6 @@ function withStorageErrors(handler, onUnavailable) {
|
|
|
328
450
|
function isRecord(value) {
|
|
329
451
|
return typeof value === "object" && value !== null;
|
|
330
452
|
}
|
|
331
|
-
function isSessionStats(value) {
|
|
332
|
-
if (!isRecord(value)) return false;
|
|
333
|
-
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");
|
|
334
|
-
}
|
|
335
|
-
function parseBookmarkSession(value, reference) {
|
|
336
|
-
if (!isRecord(value)) return null;
|
|
337
|
-
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)) {
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
return {
|
|
341
|
-
id: reference.sessionId,
|
|
342
|
-
slug: formatSessionReference(reference),
|
|
343
|
-
title: value.title,
|
|
344
|
-
directory: value.directory,
|
|
345
|
-
time_created: value.time_created,
|
|
346
|
-
time_updated: value.time_updated ?? void 0,
|
|
347
|
-
stats: value.stats
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
453
|
function parseSessionReferencePayload(value) {
|
|
351
454
|
if (!isRecord(value) || typeof value.agentName !== "string" || typeof value.sessionId !== "string" || !value.agentName.trim() || !value.sessionId) {
|
|
352
455
|
return null;
|
|
@@ -356,29 +459,30 @@ function parseSessionReferencePayload(value) {
|
|
|
356
459
|
sessionId: value.sessionId
|
|
357
460
|
});
|
|
358
461
|
}
|
|
359
|
-
function
|
|
462
|
+
function parseBookmarkReference(value) {
|
|
360
463
|
if (!isRecord(value)) return null;
|
|
361
464
|
const reference = parseSessionReferencePayload(value.reference);
|
|
362
|
-
if (reference)
|
|
363
|
-
|
|
364
|
-
return session2 ? { reference, session: session2 } : null;
|
|
365
|
-
}
|
|
366
|
-
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) {
|
|
367
467
|
return null;
|
|
368
468
|
}
|
|
369
|
-
|
|
469
|
+
return normalizeSessionReference({
|
|
370
470
|
agentName: value.agentKey.trim().toLowerCase(),
|
|
371
471
|
sessionId: value.sessionId
|
|
372
472
|
});
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
+
};
|
|
382
486
|
}
|
|
383
487
|
function sanitizeClientLogData(value) {
|
|
384
488
|
if (!isRecord(value)) return {};
|
|
@@ -393,9 +497,14 @@ function sanitizeClientLogData(value) {
|
|
|
393
497
|
);
|
|
394
498
|
}
|
|
395
499
|
function toSessionListItem(session) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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;
|
|
399
508
|
return item;
|
|
400
509
|
}
|
|
401
510
|
function getSessionHeadReference(session) {
|
|
@@ -408,6 +517,7 @@ var SESSION_DETAIL_STREAM_BATCH_CHARS = 64 * 1024;
|
|
|
408
517
|
function createSessionDetailJsonResponse(data, messages) {
|
|
409
518
|
const encoder = new TextEncoder();
|
|
410
519
|
const headerJson = JSON.stringify(data);
|
|
520
|
+
const headerPrefix = headerJson === "{}" ? '{"messages":[' : `${headerJson.slice(0, -1)},"messages":[`;
|
|
411
521
|
const iterator = messages[Symbol.iterator]();
|
|
412
522
|
let wroteHeader = false;
|
|
413
523
|
let wroteMessage = false;
|
|
@@ -415,20 +525,30 @@ function createSessionDetailJsonResponse(data, messages) {
|
|
|
415
525
|
new ReadableStream({
|
|
416
526
|
pull(controller) {
|
|
417
527
|
if (!wroteHeader) {
|
|
418
|
-
controller.enqueue(encoder.encode(
|
|
528
|
+
controller.enqueue(encoder.encode(headerPrefix));
|
|
419
529
|
wroteHeader = true;
|
|
420
530
|
return;
|
|
421
531
|
}
|
|
422
532
|
const batch = [];
|
|
423
533
|
let batchLength = 0;
|
|
424
|
-
let next
|
|
425
|
-
|
|
426
|
-
const prefix = wroteMessage ? "," : "";
|
|
427
|
-
batch.push(prefix, next.value);
|
|
428
|
-
batchLength += prefix.length + next.value.length;
|
|
429
|
-
wroteMessage = true;
|
|
430
|
-
if (batchLength >= SESSION_DETAIL_STREAM_BATCH_CHARS) break;
|
|
534
|
+
let next;
|
|
535
|
+
try {
|
|
431
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;
|
|
432
552
|
}
|
|
433
553
|
if (next.done) {
|
|
434
554
|
batch.push("]}");
|
|
@@ -460,8 +580,9 @@ function handleGetScanStatus(c, scanSource) {
|
|
|
460
580
|
}
|
|
461
581
|
function handleGetAgents(c, scanSource, defaults = {}) {
|
|
462
582
|
const scanResult = scanSource.getSnapshot();
|
|
463
|
-
const
|
|
464
|
-
|
|
583
|
+
const window = parseDateWindowRequest(c, "agents", defaults);
|
|
584
|
+
if (window.kind === "rejected") return window.response;
|
|
585
|
+
const { from, to } = window;
|
|
465
586
|
const agents = getSnapshotAggregation(
|
|
466
587
|
scanSource,
|
|
467
588
|
scanResult.sessions,
|
|
@@ -480,16 +601,18 @@ function handleGetAgents(c, scanSource, defaults = {}) {
|
|
|
480
601
|
}
|
|
481
602
|
function handleGetProjects(c, scanSource, defaults = {}) {
|
|
482
603
|
const scanResult = scanSource.getSnapshot();
|
|
483
|
-
const
|
|
484
|
-
|
|
604
|
+
const window = parseDateWindowRequest(c, "projects", defaults);
|
|
605
|
+
if (window.kind === "rejected") return window.response;
|
|
606
|
+
const { from, to } = window;
|
|
485
607
|
const projects = getSnapshotAggregation(
|
|
486
608
|
scanSource,
|
|
487
609
|
scanResult.sessions,
|
|
488
610
|
["projects", from, to],
|
|
489
611
|
() => {
|
|
490
|
-
const
|
|
612
|
+
const tree = buildSessionTree(scanResult.sessions);
|
|
613
|
+
const sessions = filterSessionTreeByActivityWindow(scanResult.sessions, from, to, tree);
|
|
491
614
|
return {
|
|
492
|
-
projects:
|
|
615
|
+
projects: attachProjectMetricsFromTree(listCachedProjectGroups(sessions), tree, from, to)
|
|
493
616
|
};
|
|
494
617
|
}
|
|
495
618
|
);
|
|
@@ -497,7 +620,13 @@ function handleGetProjects(c, scanSource, defaults = {}) {
|
|
|
497
620
|
}
|
|
498
621
|
function handleGetSessions(c, scanSource, defaults = {}) {
|
|
499
622
|
const scanResult = scanSource.getSnapshot();
|
|
500
|
-
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
|
+
}
|
|
501
630
|
const q = c.req.query("q")?.toLowerCase();
|
|
502
631
|
const cwd = c.req.query("cwd");
|
|
503
632
|
const projectIdentity = parseProjectIdentityFilter(
|
|
@@ -508,26 +637,31 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
508
637
|
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
509
638
|
}
|
|
510
639
|
const tag = c.req.query("tag")?.toLowerCase();
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if (agent
|
|
515
|
-
sessions
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
sessions
|
|
521
|
-
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
+
);
|
|
531
665
|
const aliases = loadAliasView();
|
|
532
666
|
if (q) {
|
|
533
667
|
sessions = sessions.filter((session) => {
|
|
@@ -535,6 +669,28 @@ function handleGetSessions(c, scanSource, defaults = {}) {
|
|
|
535
669
|
return session.title.toLowerCase().includes(q) || alias?.toLowerCase().includes(q);
|
|
536
670
|
});
|
|
537
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
|
+
}
|
|
538
694
|
return c.json({
|
|
539
695
|
sessions: sessions.map(
|
|
540
696
|
(session) => toSessionListItem(aliases.decorate(session, getSessionHeadReference(session)))
|
|
@@ -558,9 +714,48 @@ function withParentContext(results, sessions, aliases) {
|
|
|
558
714
|
return { ...result, parent: { reference, title: aliases.get(reference) ?? parent.title } };
|
|
559
715
|
});
|
|
560
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
|
+
}
|
|
561
751
|
function handleSearchSessions(c, scanSource, defaults = {}) {
|
|
562
752
|
const query = c.req.query("q")?.trim() ?? "";
|
|
563
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
|
+
}
|
|
564
759
|
const projectIdentity = parseProjectIdentityFilter(
|
|
565
760
|
c.req.query("projectKind"),
|
|
566
761
|
c.req.query("projectKey")
|
|
@@ -568,28 +763,42 @@ function handleSearchSessions(c, scanSource, defaults = {}) {
|
|
|
568
763
|
if (projectIdentity === null) {
|
|
569
764
|
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
570
765
|
}
|
|
571
|
-
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
|
+
);
|
|
572
781
|
const aliases = loadAliasView();
|
|
573
782
|
const results = executeSessionSearch(query, searchOptions, scanResult).map((result) => ({
|
|
574
783
|
...result,
|
|
575
784
|
session: aliases.decorate(result.session, result.reference)
|
|
576
785
|
}));
|
|
577
786
|
const aliasResults = findAliasSearchResults(query, searchOptions, scanResult, aliases);
|
|
578
|
-
const
|
|
579
|
-
for (const result of [...aliasResults, ...results]) {
|
|
580
|
-
deduped.set(`${result.reference.agentName}\0${result.reference.sessionId}`, result);
|
|
581
|
-
}
|
|
787
|
+
const mergedResults = mergeAliasSearchResults(results, aliasResults, searchOptions.limit ?? 50);
|
|
582
788
|
return c.json({
|
|
583
|
-
results: withParentContext(
|
|
584
|
-
[...deduped.values()].slice(0, searchOptions.limit ?? 50),
|
|
585
|
-
scanResult.sessions,
|
|
586
|
-
aliases
|
|
587
|
-
)
|
|
789
|
+
results: withParentContext(mergedResults, scanResult.sessions, aliases)
|
|
588
790
|
});
|
|
589
791
|
}
|
|
590
792
|
function handleGetFileActivity(c, defaults = {}) {
|
|
591
|
-
const
|
|
592
|
-
|
|
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
|
+
}
|
|
593
802
|
const projectIdentity = parseProjectIdentityFilter(
|
|
594
803
|
c.req.query("projectKind"),
|
|
595
804
|
c.req.query("projectKey")
|
|
@@ -597,10 +806,16 @@ function handleGetFileActivity(c, defaults = {}) {
|
|
|
597
806
|
if (projectIdentity === null) {
|
|
598
807
|
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
599
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
|
+
}
|
|
600
815
|
const aliases = loadAliasView();
|
|
601
816
|
return c.json({
|
|
602
817
|
activity: listFileActivity({
|
|
603
|
-
agent:
|
|
818
|
+
agent: sessionQuery.agent.kind === "known" ? sessionQuery.agent.agentName : void 0,
|
|
604
819
|
sessionId: optionalQueryValue(c.req.query("sessionId")),
|
|
605
820
|
projectKind: projectIdentity?.kind,
|
|
606
821
|
projectKey: projectIdentity?.key,
|
|
@@ -608,9 +823,9 @@ function handleGetFileActivity(c, defaults = {}) {
|
|
|
608
823
|
cwd: optionalQueryValue(c.req.query("cwd")),
|
|
609
824
|
path: optionalQueryValue(c.req.query("path")),
|
|
610
825
|
kind: parseFileActivityKind(optionalQueryValue(c.req.query("kind"))),
|
|
611
|
-
from:
|
|
612
|
-
to:
|
|
613
|
-
limit
|
|
826
|
+
from: window.from,
|
|
827
|
+
to: window.to,
|
|
828
|
+
limit: sessionQuery.limit.value
|
|
614
829
|
}).map((activity) => decorateFileActivity(activity, aliases))
|
|
615
830
|
});
|
|
616
831
|
}
|
|
@@ -625,10 +840,12 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
625
840
|
return c.json({ error: "Missing session ID" }, 400);
|
|
626
841
|
}
|
|
627
842
|
try {
|
|
628
|
-
const
|
|
843
|
+
const reference = {
|
|
629
844
|
agentName,
|
|
630
845
|
sessionId
|
|
631
|
-
}
|
|
846
|
+
};
|
|
847
|
+
const messageCursor = optionalQueryValue(c.req.query("messageCursor"));
|
|
848
|
+
const result = messageCursor ? materializeSessionDetailResponse(scanSource.getSnapshot(), reference, { messageCursor }) : materializeSessionDetailResponse(scanSource.getSnapshot(), reference);
|
|
632
849
|
if (result.status === "unknown-agent") {
|
|
633
850
|
return c.json({ error: `Unknown agent: ${agentName}` }, 404);
|
|
634
851
|
}
|
|
@@ -644,6 +861,8 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
644
861
|
agent: agentName,
|
|
645
862
|
session_id: sessionId,
|
|
646
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",
|
|
647
866
|
duration_ms: Math.round(performance.now() - startedAt)
|
|
648
867
|
});
|
|
649
868
|
const aliases = loadAliasView();
|
|
@@ -662,7 +881,7 @@ async function handleGetSessionData(c, scanSource) {
|
|
|
662
881
|
duration_ms: Math.round(performance.now() - startedAt),
|
|
663
882
|
error: message
|
|
664
883
|
});
|
|
665
|
-
return c.json({ error:
|
|
884
|
+
return c.json({ error: "Failed to load session" }, 500);
|
|
666
885
|
}
|
|
667
886
|
}
|
|
668
887
|
async function handlePostClientLog(c) {
|
|
@@ -675,12 +894,27 @@ async function handlePostClientLog(c) {
|
|
|
675
894
|
appLogger.info(`client.${event}`, sanitizeClientLogData(payload?.data));
|
|
676
895
|
return c.json({ ok: true });
|
|
677
896
|
}
|
|
678
|
-
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) {
|
|
679
912
|
return withStorageErrors(
|
|
680
913
|
() => {
|
|
681
914
|
const aliases = loadAliasView();
|
|
915
|
+
const bookmarks = materializeStoredBookmarks(scanSource, listBookmarks(), aliases);
|
|
682
916
|
return c.json({
|
|
683
|
-
bookmarks
|
|
917
|
+
bookmarks,
|
|
684
918
|
storageAvailable: true
|
|
685
919
|
});
|
|
686
920
|
},
|
|
@@ -688,7 +922,7 @@ function handleGetBookmarks(c) {
|
|
|
688
922
|
);
|
|
689
923
|
}
|
|
690
924
|
async function handlePutBookmark(c) {
|
|
691
|
-
const payload =
|
|
925
|
+
const payload = parseBookmarkReference(await c.req.json().catch(() => null));
|
|
692
926
|
if (!payload) {
|
|
693
927
|
return c.json({ error: "Invalid bookmark payload" }, 400);
|
|
694
928
|
}
|
|
@@ -697,17 +931,21 @@ async function handlePutBookmark(c) {
|
|
|
697
931
|
() => c.json({ error: "Bookmark storage is unavailable" }, 503)
|
|
698
932
|
);
|
|
699
933
|
}
|
|
700
|
-
async function handleImportBookmarks(c) {
|
|
934
|
+
async function handleImportBookmarks(c, scanSource) {
|
|
701
935
|
const payload = await c.req.json().catch(() => null);
|
|
702
936
|
if (!Array.isArray(payload)) {
|
|
703
937
|
return c.json({ error: "Invalid bookmark payload" }, 400);
|
|
704
938
|
}
|
|
705
|
-
const bookmarks = payload.map((entry) =>
|
|
939
|
+
const bookmarks = payload.map((entry) => parseBookmarkImport(entry)).filter((entry) => entry !== null);
|
|
706
940
|
if (bookmarks.length !== payload.length) {
|
|
707
941
|
return c.json({ error: "Invalid bookmark payload" }, 400);
|
|
708
942
|
}
|
|
709
943
|
return withStorageErrors(
|
|
710
|
-
() =>
|
|
944
|
+
() => {
|
|
945
|
+
const imported = importBookmarks(bookmarks);
|
|
946
|
+
const views = materializeStoredBookmarks(scanSource, imported, loadAliasView());
|
|
947
|
+
return c.json({ bookmarks: views, storageAvailable: true });
|
|
948
|
+
},
|
|
711
949
|
() => c.json({ error: "Bookmark storage is unavailable" }, 503)
|
|
712
950
|
);
|
|
713
951
|
}
|
|
@@ -773,6 +1011,8 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
773
1011
|
if (projectIdentity === null) {
|
|
774
1012
|
return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
|
|
775
1013
|
}
|
|
1014
|
+
const dateWindow = parseDateWindowRequest(c, "dashboard", defaults);
|
|
1015
|
+
if (dateWindow.kind === "rejected") return dateWindow.response;
|
|
776
1016
|
const { from, to, days } = resolveTimeWindow({
|
|
777
1017
|
mode: "dashboard",
|
|
778
1018
|
query: {
|
|
@@ -788,7 +1028,7 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
788
1028
|
projectKey: projectIdentity?.key
|
|
789
1029
|
};
|
|
790
1030
|
const compare = from == null ? void 0 : { from: addCalendarDays(from, -(days ?? countCalendarDays(from, to))), to: from - 1 };
|
|
791
|
-
const fixedTo =
|
|
1031
|
+
const fixedTo = dateWindow.to;
|
|
792
1032
|
const aggregate = getSnapshotAggregation(
|
|
793
1033
|
scanSource,
|
|
794
1034
|
scanResult.sessions,
|
|
@@ -847,52 +1087,174 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
|
|
|
847
1087
|
});
|
|
848
1088
|
}
|
|
849
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
|
+
|
|
850
1205
|
// src/api/routes.ts
|
|
851
1206
|
function createSseResponse(eventSource, signal) {
|
|
852
|
-
const encoder = new TextEncoder();
|
|
853
1207
|
let cancelStream = () => {
|
|
854
1208
|
};
|
|
1209
|
+
let drainStream = () => {
|
|
1210
|
+
};
|
|
855
1211
|
return new Response(
|
|
856
1212
|
new ReadableStream({
|
|
857
1213
|
start(controller) {
|
|
858
1214
|
let isClosed = false;
|
|
859
|
-
|
|
860
|
-
if (isClosed) return;
|
|
861
|
-
controller.enqueue(encoder.encode(`event: ${event}
|
|
862
|
-
`));
|
|
863
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
864
|
-
|
|
865
|
-
`));
|
|
1215
|
+
let unsubscribeSessions = () => {
|
|
866
1216
|
};
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
write(event.type, event);
|
|
874
|
-
});
|
|
875
|
-
const heartbeat = setInterval(() => {
|
|
876
|
-
if (!isClosed) controller.enqueue(encoder.encode(": keepalive\n\n"));
|
|
877
|
-
}, 15e3);
|
|
1217
|
+
let unsubscribeScanStatus = () => {
|
|
1218
|
+
};
|
|
1219
|
+
let abortStream = () => {
|
|
1220
|
+
};
|
|
1221
|
+
let heartbeat;
|
|
1222
|
+
let buffer;
|
|
878
1223
|
const cleanup = () => {
|
|
879
1224
|
if (isClosed) return false;
|
|
880
1225
|
isClosed = true;
|
|
881
|
-
|
|
1226
|
+
buffer.close();
|
|
1227
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
882
1228
|
unsubscribeSessions();
|
|
883
1229
|
unsubscribeScanStatus();
|
|
884
1230
|
signal.removeEventListener("abort", abortStream);
|
|
885
1231
|
return true;
|
|
886
1232
|
};
|
|
887
|
-
|
|
1233
|
+
abortStream = () => {
|
|
888
1234
|
if (cleanup()) controller.close();
|
|
889
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);
|
|
890
1249
|
cancelStream = () => {
|
|
891
1250
|
cleanup();
|
|
892
1251
|
};
|
|
893
1252
|
if (signal.aborted) abortStream();
|
|
894
1253
|
else signal.addEventListener("abort", abortStream, { once: true });
|
|
895
1254
|
},
|
|
1255
|
+
pull() {
|
|
1256
|
+
drainStream();
|
|
1257
|
+
},
|
|
896
1258
|
cancel() {
|
|
897
1259
|
cancelStream();
|
|
898
1260
|
}
|
|
@@ -924,23 +1286,140 @@ function createApiRoutes(scanSource, eventSource, options = {}) {
|
|
|
924
1286
|
api.get("/file-activity", (c) => handleGetFileActivity(c, listDefaults));
|
|
925
1287
|
api.get("/sessions/:agent/:id", (c) => handleGetSessionData(c, scanSource));
|
|
926
1288
|
api.get("/dashboard", (c) => handleGetDashboard(c, scanSource, listDefaults));
|
|
927
|
-
api.get("/bookmarks", (c) => handleGetBookmarks(c));
|
|
1289
|
+
api.get("/bookmarks", (c) => handleGetBookmarks(c, scanSource));
|
|
928
1290
|
api.put("/bookmarks", (c) => handlePutBookmark(c));
|
|
929
|
-
api.post("/bookmarks/import", (c) => handleImportBookmarks(c));
|
|
1291
|
+
api.post("/bookmarks/import", (c) => handleImportBookmarks(c, scanSource));
|
|
930
1292
|
api.delete("/bookmarks/:agent/:id", (c) => handleDeleteBookmark(c));
|
|
931
1293
|
api.put("/session-aliases/:agent/:id", (c) => handlePutSessionAlias(c));
|
|
932
1294
|
api.delete("/session-aliases/:agent/:id", (c) => handleDeleteSessionAlias(c));
|
|
933
1295
|
api.post("/logs", (c) => handlePostClientLog(c));
|
|
934
1296
|
if (eventSource) {
|
|
935
|
-
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
|
+
});
|
|
936
1301
|
}
|
|
937
1302
|
return api;
|
|
938
1303
|
}
|
|
939
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
|
+
|
|
940
1419
|
// src/remote-access.ts
|
|
941
1420
|
import { randomBytes, timingSafeEqual } from "crypto";
|
|
942
1421
|
import { readFileSync } from "fs";
|
|
943
|
-
import { isIP } from "net";
|
|
1422
|
+
import { isIP as isIP2 } from "net";
|
|
944
1423
|
var REMOTE_ACCESS_QUERY_PARAM = "access_token";
|
|
945
1424
|
var RemoteTransportError = class extends Error {
|
|
946
1425
|
};
|
|
@@ -971,6 +1450,13 @@ function resolveRemoteTransport(request) {
|
|
|
971
1450
|
if (request.trustProxy) return { kind: "trusted-proxy" };
|
|
972
1451
|
return isLoopbackHostname(request.hostname) ? { kind: "loopback" } : { kind: "plaintext" };
|
|
973
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
|
+
}
|
|
974
1460
|
var FORWARDED_PROTO_HEADER = "X-Forwarded-Proto";
|
|
975
1461
|
function requireProxyTls() {
|
|
976
1462
|
return async (c, next) => {
|
|
@@ -987,7 +1473,7 @@ function createRemoteAccessToken() {
|
|
|
987
1473
|
function isLoopbackHostname(hostname) {
|
|
988
1474
|
const normalized = hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
989
1475
|
if (normalized === "localhost" || normalized === "::1") return true;
|
|
990
|
-
return
|
|
1476
|
+
return isIP2(normalized) === 4 && normalized.startsWith("127.");
|
|
991
1477
|
}
|
|
992
1478
|
function tokenMatches(actual, expected) {
|
|
993
1479
|
if (!actual) return false;
|
|
@@ -1017,6 +1503,7 @@ function remoteAccessAuth(expectedToken) {
|
|
|
1017
1503
|
|
|
1018
1504
|
// src/server.ts
|
|
1019
1505
|
var MAX_API_REQUEST_BYTES = 1024 * 1024;
|
|
1506
|
+
var SERVER_CLOSE_GRACE_MS = 1e3;
|
|
1020
1507
|
function findWebDistPath() {
|
|
1021
1508
|
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
1022
1509
|
const packagedPath = resolve(__dirname2, "web");
|
|
@@ -1056,17 +1543,91 @@ function getListeningPort(server, fallback) {
|
|
|
1056
1543
|
const address = server.address();
|
|
1057
1544
|
return typeof address === "object" && address !== null ? address.port : fallback;
|
|
1058
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
|
+
}
|
|
1059
1567
|
async function createServer(port, store, options = {}) {
|
|
1060
1568
|
const app = new Hono2();
|
|
1061
1569
|
const hostname = options.hostname ?? "127.0.0.1";
|
|
1062
|
-
const
|
|
1063
|
-
const
|
|
1064
|
-
const remoteAccessToken =
|
|
1065
|
-
|
|
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) {
|
|
1066
1583
|
throw new Error(
|
|
1067
1584
|
`Refusing to expose CodeSesh on ${hostname} without authentication. Add --remote-access to continue.`
|
|
1068
1585
|
);
|
|
1069
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
|
+
}
|
|
1070
1631
|
app.use("*", async (c, next) => {
|
|
1071
1632
|
const startedAt = performance.now();
|
|
1072
1633
|
let thrown;
|
|
@@ -1093,6 +1654,7 @@ async function createServer(port, store, options = {}) {
|
|
|
1093
1654
|
if (remoteAccessToken) {
|
|
1094
1655
|
app.use("/api/*", remoteAccessAuth(remoteAccessToken));
|
|
1095
1656
|
}
|
|
1657
|
+
app.use("/api/*", loopbackWriteGuard());
|
|
1096
1658
|
app.use(
|
|
1097
1659
|
"/api/*",
|
|
1098
1660
|
bodyLimit({
|
|
@@ -1104,7 +1666,8 @@ async function createServer(port, store, options = {}) {
|
|
|
1104
1666
|
const routeOptions = {
|
|
1105
1667
|
defaultSessionFrom: options.defaultSessionFrom,
|
|
1106
1668
|
defaultSessionTo: options.defaultSessionTo,
|
|
1107
|
-
defaultSessionDays: options.defaultSessionDays
|
|
1669
|
+
defaultSessionDays: options.defaultSessionDays,
|
|
1670
|
+
shutdownSignal: shutdownController.signal
|
|
1108
1671
|
};
|
|
1109
1672
|
app.route("/api", createApiRoutes(store, store, routeOptions));
|
|
1110
1673
|
const webDistPath = options.webDistPath ?? findWebDistPath();
|
|
@@ -1114,7 +1677,6 @@ async function createServer(port, store, options = {}) {
|
|
|
1114
1677
|
}
|
|
1115
1678
|
const attempts = Math.max(1, options.portFallbackAttempts ?? 1);
|
|
1116
1679
|
let server = null;
|
|
1117
|
-
let actualPort = port;
|
|
1118
1680
|
for (let offset = 0; offset < attempts; offset += 1) {
|
|
1119
1681
|
const candidatePort = port + offset;
|
|
1120
1682
|
server = serve({
|
|
@@ -1146,7 +1708,7 @@ async function createServer(port, store, options = {}) {
|
|
|
1146
1708
|
}
|
|
1147
1709
|
}
|
|
1148
1710
|
const scheme = transport.kind === "tls" ? "https" : "http";
|
|
1149
|
-
const baseUrl =
|
|
1711
|
+
const baseUrl = accessPolicy.bindCategory === "loopback" ? `${scheme}://localhost:${actualPort}` : `${scheme}://${hostname}:${actualPort}`;
|
|
1150
1712
|
const url = remoteAccessToken ? `${baseUrl}/?${REMOTE_ACCESS_QUERY_PARAM}=${encodeURIComponent(remoteAccessToken)}` : baseUrl;
|
|
1151
1713
|
appLogger.info("server.listen", {
|
|
1152
1714
|
port: actualPort,
|
|
@@ -1155,7 +1717,7 @@ async function createServer(port, store, options = {}) {
|
|
|
1155
1717
|
remote_access: Boolean(remoteAccessToken),
|
|
1156
1718
|
transport: transport.kind
|
|
1157
1719
|
});
|
|
1158
|
-
if (
|
|
1720
|
+
if (accessPolicy.authenticationRequired) {
|
|
1159
1721
|
appLogger.warn("server.listen.remote_access", {
|
|
1160
1722
|
hostname,
|
|
1161
1723
|
port: actualPort,
|
|
@@ -1169,6 +1731,14 @@ async function createServer(port, store, options = {}) {
|
|
|
1169
1731
|
console.warn(
|
|
1170
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
|
|
1171
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
|
|
1172
1742
|
`
|
|
1173
1743
|
);
|
|
1174
1744
|
}
|
|
@@ -1177,14 +1747,15 @@ async function createServer(port, store, options = {}) {
|
|
|
1177
1747
|
url,
|
|
1178
1748
|
shutdown: async () => {
|
|
1179
1749
|
appLogger.info("server.shutdown", { port: actualPort });
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
server.
|
|
1186
|
-
|
|
1187
|
-
|
|
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 });
|
|
1188
1759
|
}
|
|
1189
1760
|
};
|
|
1190
1761
|
}
|
|
@@ -1343,12 +1914,88 @@ var AgentOperationScheduler = class {
|
|
|
1343
1914
|
}
|
|
1344
1915
|
};
|
|
1345
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
|
+
|
|
1346
1992
|
// src/live-session-index.ts
|
|
1347
1993
|
var LiveSessionIndex = class {
|
|
1348
1994
|
agents = [];
|
|
1349
1995
|
agentsByName = /* @__PURE__ */ new Map();
|
|
1350
1996
|
byAgent = {};
|
|
1351
1997
|
sessions = [];
|
|
1998
|
+
scanFailures = {};
|
|
1352
1999
|
signatureCaches = /* @__PURE__ */ new Map();
|
|
1353
2000
|
initialize(snapshot, options = {}) {
|
|
1354
2001
|
const agentMap = /* @__PURE__ */ new Map();
|
|
@@ -1360,8 +2007,12 @@ var LiveSessionIndex = class {
|
|
|
1360
2007
|
(agent) => !options.allowedAgents || options.allowedAgents.has(agent.name.toLowerCase())
|
|
1361
2008
|
);
|
|
1362
2009
|
this.agentsByName = new Map(this.agents.map((agent) => [agent.name, agent]));
|
|
2010
|
+
this.scanFailures = { ...snapshot.scanFailures };
|
|
1363
2011
|
this.byAgent = Object.fromEntries(
|
|
1364
|
-
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
|
+
})
|
|
1365
2016
|
);
|
|
1366
2017
|
this.sessions = mergeSortedSessions(Object.values(this.byAgent));
|
|
1367
2018
|
this.signatureCaches.clear();
|
|
@@ -1370,14 +2021,17 @@ var LiveSessionIndex = class {
|
|
|
1370
2021
|
return {
|
|
1371
2022
|
agents: this.agents,
|
|
1372
2023
|
byAgent: this.byAgent,
|
|
1373
|
-
sessions: this.sessions
|
|
2024
|
+
sessions: this.sessions,
|
|
2025
|
+
...Object.keys(this.scanFailures).length > 0 ? { scanFailures: this.scanFailures } : {}
|
|
1374
2026
|
};
|
|
1375
2027
|
}
|
|
1376
2028
|
findAgent(agentName) {
|
|
1377
2029
|
return this.agentsByName.get(agentName);
|
|
1378
2030
|
}
|
|
1379
2031
|
commitAgentSessions(agentName, nextSessions, candidateChangedIds = []) {
|
|
2032
|
+
delete this.scanFailures[agentName];
|
|
1380
2033
|
const previousSessions = this.byAgent[agentName] ?? [];
|
|
2034
|
+
const previousGlobalSessions = this.sessions;
|
|
1381
2035
|
const signatureCache = this.signatureCache(agentName);
|
|
1382
2036
|
const { changes, removedSessionIds, counts } = computeSessionDiff(
|
|
1383
2037
|
previousSessions,
|
|
@@ -1390,19 +2044,34 @@ var LiveSessionIndex = class {
|
|
|
1390
2044
|
this.byAgent[agentName] = sortSessions(nextSessions);
|
|
1391
2045
|
this.sessions = mergeSortedSessions(Object.values(this.byAgent));
|
|
1392
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
|
+
);
|
|
1393
2062
|
return {
|
|
1394
2063
|
type: "sessions-updated",
|
|
1395
2064
|
changedAgents: [agentName],
|
|
1396
2065
|
newSessions: counts.new,
|
|
2066
|
+
newSessionRefs,
|
|
1397
2067
|
updatedSessions: counts.updated,
|
|
1398
2068
|
removedSessions: counts.removed,
|
|
1399
2069
|
totalSessions: this.sessions.length,
|
|
1400
2070
|
timestamp: Date.now(),
|
|
1401
|
-
changedSessionHeads
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
|
|
2071
|
+
changedSessionHeads,
|
|
2072
|
+
projectionRelatedSessionHeads: projectionContext.relatedSessionHeads,
|
|
2073
|
+
projectionSessionOrder: projectionContext.sessionOrder,
|
|
2074
|
+
removedSessionRefs
|
|
1406
2075
|
};
|
|
1407
2076
|
}
|
|
1408
2077
|
signatureCache(agentName) {
|
|
@@ -1414,6 +2083,69 @@ var LiveSessionIndex = class {
|
|
|
1414
2083
|
}
|
|
1415
2084
|
};
|
|
1416
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
|
+
|
|
1417
2149
|
// src/search-index-job-runner.ts
|
|
1418
2150
|
import { existsSync as existsSync2 } from "fs";
|
|
1419
2151
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -1436,6 +2168,7 @@ var PendingSearchIndexJobBatch = class {
|
|
|
1436
2168
|
jobsByAgent = /* @__PURE__ */ new Map();
|
|
1437
2169
|
waiters = [];
|
|
1438
2170
|
settled = false;
|
|
2171
|
+
started = false;
|
|
1439
2172
|
get jobs() {
|
|
1440
2173
|
const jobs = [];
|
|
1441
2174
|
for (const pending of this.jobsByAgent.values()) {
|
|
@@ -1452,6 +2185,17 @@ var PendingSearchIndexJobBatch = class {
|
|
|
1452
2185
|
}
|
|
1453
2186
|
return count;
|
|
1454
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
|
+
}
|
|
1455
2199
|
merge(context, jobs, waiter) {
|
|
1456
2200
|
this.context = context;
|
|
1457
2201
|
this.waiters.push(waiter);
|
|
@@ -1481,6 +2225,7 @@ var PendingSearchIndexJobBatch = class {
|
|
|
1481
2225
|
};
|
|
1482
2226
|
var PendingSearchIndexJobs = class {
|
|
1483
2227
|
pendingBatch = null;
|
|
2228
|
+
batches = /* @__PURE__ */ new WeakSet();
|
|
1484
2229
|
get batchCount() {
|
|
1485
2230
|
return this.pendingBatch ? 1 : 0;
|
|
1486
2231
|
}
|
|
@@ -1490,14 +2235,15 @@ var PendingSearchIndexJobs = class {
|
|
|
1490
2235
|
get changeCount() {
|
|
1491
2236
|
return this.pendingBatch?.changeCount ?? 0;
|
|
1492
2237
|
}
|
|
1493
|
-
enqueue(id, context, jobs) {
|
|
2238
|
+
enqueue(id, context, jobs, onStarted) {
|
|
1494
2239
|
if (jobs.length === 0) return Promise.resolve();
|
|
1495
2240
|
return new Promise((resolve4, reject) => {
|
|
1496
|
-
const waiter = { resolve: resolve4, reject };
|
|
2241
|
+
const waiter = { resolve: resolve4, reject, onStarted };
|
|
1497
2242
|
if (this.pendingBatch) {
|
|
1498
2243
|
this.pendingBatch.merge(context, jobs, waiter);
|
|
1499
2244
|
} else {
|
|
1500
2245
|
this.pendingBatch = new PendingSearchIndexJobBatch(id, context, jobs, waiter);
|
|
2246
|
+
this.batches.add(this.pendingBatch);
|
|
1501
2247
|
}
|
|
1502
2248
|
});
|
|
1503
2249
|
}
|
|
@@ -1507,7 +2253,9 @@ var PendingSearchIndexJobs = class {
|
|
|
1507
2253
|
return batch;
|
|
1508
2254
|
}
|
|
1509
2255
|
settle(batch, error) {
|
|
1510
|
-
|
|
2256
|
+
if (!(batch instanceof PendingSearchIndexJobBatch) || !this.batches.has(batch)) return false;
|
|
2257
|
+
this.batches.delete(batch);
|
|
2258
|
+
return batch.settle(error);
|
|
1511
2259
|
}
|
|
1512
2260
|
rejectAll(error) {
|
|
1513
2261
|
const batch = this.take();
|
|
@@ -1516,8 +2264,10 @@ var PendingSearchIndexJobs = class {
|
|
|
1516
2264
|
};
|
|
1517
2265
|
function createPendingChanges(job) {
|
|
1518
2266
|
return {
|
|
2267
|
+
kind: job.kind,
|
|
1519
2268
|
context: job.context,
|
|
1520
2269
|
agentName: job.agentName,
|
|
2270
|
+
...job.kind === "changes" && job.publicationId ? { publicationId: job.publicationId } : {},
|
|
1521
2271
|
changesBySessionId: /* @__PURE__ */ new Map(),
|
|
1522
2272
|
removedSessionIds: /* @__PURE__ */ new Set(),
|
|
1523
2273
|
meta: {},
|
|
@@ -1525,7 +2275,11 @@ function createPendingChanges(job) {
|
|
|
1525
2275
|
};
|
|
1526
2276
|
}
|
|
1527
2277
|
function mergeChanges(pending, job) {
|
|
2278
|
+
pending.kind = job.kind;
|
|
1528
2279
|
pending.context = job.context;
|
|
2280
|
+
if (job.kind === "changes" && job.publicationId) {
|
|
2281
|
+
pending.publicationId = job.publicationId;
|
|
2282
|
+
}
|
|
1529
2283
|
pending.searchIndexOptions = mergeSearchIndexOptions(
|
|
1530
2284
|
pending.searchIndexOptions,
|
|
1531
2285
|
job.searchIndexOptions
|
|
@@ -1552,8 +2306,7 @@ function mergeSearchIndexOptions(current, incoming) {
|
|
|
1552
2306
|
return { ...current, ...incoming };
|
|
1553
2307
|
}
|
|
1554
2308
|
function changesJobFromPending(pending) {
|
|
1555
|
-
|
|
1556
|
-
kind: "changes",
|
|
2309
|
+
const common = {
|
|
1557
2310
|
context: pending.context,
|
|
1558
2311
|
agentName: pending.agentName,
|
|
1559
2312
|
changes: [...pending.changesBySessionId.values()],
|
|
@@ -1561,6 +2314,11 @@ function changesJobFromPending(pending) {
|
|
|
1561
2314
|
meta: pending.meta,
|
|
1562
2315
|
...pending.searchIndexOptions ? { searchIndexOptions: pending.searchIndexOptions } : {}
|
|
1563
2316
|
};
|
|
2317
|
+
return pending.kind === "changes" ? {
|
|
2318
|
+
kind: "changes",
|
|
2319
|
+
...common,
|
|
2320
|
+
...pending.publicationId ? { publicationId: pending.publicationId } : {}
|
|
2321
|
+
} : { kind: "maintenance", ...common };
|
|
1564
2322
|
}
|
|
1565
2323
|
|
|
1566
2324
|
// src/search-index-job-runner.ts
|
|
@@ -1570,13 +2328,20 @@ var SearchIndexJobRunner = class {
|
|
|
1570
2328
|
activeBatch = null;
|
|
1571
2329
|
nextBatchId = 1;
|
|
1572
2330
|
pendingJobs = new PendingSearchIndexJobs();
|
|
2331
|
+
pendingMaintenanceJobs = new PendingSearchIndexJobs();
|
|
1573
2332
|
isShuttingDown = false;
|
|
1574
|
-
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) {
|
|
1575
2340
|
if (jobs.length === 0) return Promise.resolve();
|
|
1576
2341
|
if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE));
|
|
1577
2342
|
const batchId = this.nextBatchId++;
|
|
1578
|
-
const completion =
|
|
1579
|
-
if (this.
|
|
2343
|
+
const completion = queue.enqueue(batchId, context, jobs, onStarted);
|
|
2344
|
+
if (this.activeBatch) {
|
|
1580
2345
|
const snapshot = this.snapshot();
|
|
1581
2346
|
appLogger.debug("search_index.worker_queued", {
|
|
1582
2347
|
batch_id: batchId,
|
|
@@ -1596,7 +2361,9 @@ var SearchIndexJobRunner = class {
|
|
|
1596
2361
|
activeBatchId: this.activeBatch?.id,
|
|
1597
2362
|
pendingBatches: this.pendingJobs.batchCount,
|
|
1598
2363
|
pendingJobs: this.pendingJobs.jobCount,
|
|
1599
|
-
pendingChanges: this.pendingJobs.changeCount
|
|
2364
|
+
pendingChanges: this.pendingJobs.changeCount,
|
|
2365
|
+
pendingMaintenanceBatches: this.pendingMaintenanceJobs.batchCount,
|
|
2366
|
+
pendingMaintenanceJobs: this.pendingMaintenanceJobs.jobCount
|
|
1600
2367
|
};
|
|
1601
2368
|
}
|
|
1602
2369
|
async shutdown() {
|
|
@@ -1608,17 +2375,25 @@ var SearchIndexJobRunner = class {
|
|
|
1608
2375
|
const shutdownError = new Error(SHUTDOWN_ERROR_MESSAGE);
|
|
1609
2376
|
if (activeBatch) this.settle(activeBatch, shutdownError);
|
|
1610
2377
|
this.pendingJobs.rejectAll(shutdownError);
|
|
2378
|
+
this.pendingMaintenanceJobs.rejectAll(shutdownError);
|
|
1611
2379
|
if (worker) await worker.terminate();
|
|
1612
2380
|
}
|
|
1613
2381
|
startNextBatch() {
|
|
1614
|
-
if (this.isShuttingDown || this.
|
|
1615
|
-
const batch = this.pendingJobs.take();
|
|
2382
|
+
if (this.isShuttingDown || this.activeBatch) return;
|
|
2383
|
+
const batch = this.pendingJobs.take() ?? this.pendingMaintenanceJobs.take();
|
|
1616
2384
|
if (!batch) return;
|
|
1617
2385
|
appLogger.info("search_index.worker_dequeued", {
|
|
1618
2386
|
batch_id: batch.id,
|
|
1619
2387
|
context: batch.context,
|
|
1620
2388
|
pending_batches: this.pendingJobs.batchCount
|
|
1621
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
|
+
});
|
|
1622
2397
|
this.startBatch(batch);
|
|
1623
2398
|
}
|
|
1624
2399
|
startBatch(batch) {
|
|
@@ -1626,10 +2401,27 @@ var SearchIndexJobRunner = class {
|
|
|
1626
2401
|
this.settle(batch, new Error(SHUTDOWN_ERROR_MESSAGE));
|
|
1627
2402
|
return;
|
|
1628
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
|
+
}
|
|
1629
2420
|
const workerUrl = this.workerUrl();
|
|
1630
2421
|
if (!workerUrl) {
|
|
1631
2422
|
appLogger.warn("search_index.worker_missing", { context: batch.context });
|
|
1632
|
-
this.settle(batch);
|
|
2423
|
+
this.settle(batch, new Error("Search index worker is unavailable"));
|
|
2424
|
+
this.startNextBatch();
|
|
1633
2425
|
return;
|
|
1634
2426
|
}
|
|
1635
2427
|
appLogger.info("search_index.worker_started", {
|
|
@@ -1637,33 +2429,37 @@ var SearchIndexJobRunner = class {
|
|
|
1637
2429
|
context: batch.context,
|
|
1638
2430
|
jobs: batch.jobs.length
|
|
1639
2431
|
});
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
});
|
|
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
|
+
}
|
|
1649
2440
|
worker.unref();
|
|
1650
2441
|
this.worker = worker;
|
|
1651
2442
|
this.activeBatch = batch;
|
|
1652
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;
|
|
1653
2448
|
if (message.type === "sync-result") {
|
|
1654
2449
|
logSearchIndexSync(message.context, message.result);
|
|
1655
2450
|
return;
|
|
1656
2451
|
}
|
|
1657
2452
|
if (message.type === "persist-failed") {
|
|
1658
2453
|
appLogger.error("search_index.persist_failed", {
|
|
1659
|
-
batch_id:
|
|
2454
|
+
batch_id: activeBatch.id,
|
|
1660
2455
|
context: message.context,
|
|
1661
2456
|
stage: message.stage,
|
|
2457
|
+
publication_id: message.publicationId,
|
|
1662
2458
|
agent: message.agentName,
|
|
1663
2459
|
sessions: message.sessions
|
|
1664
2460
|
});
|
|
1665
|
-
this.
|
|
1666
|
-
|
|
2461
|
+
this.finishBatch(
|
|
2462
|
+
activeBatch,
|
|
1667
2463
|
new Error(
|
|
1668
2464
|
`Search index worker failed to persist ${message.stage} for ${message.agentName}`
|
|
1669
2465
|
)
|
|
@@ -1675,30 +2471,56 @@ var SearchIndexJobRunner = class {
|
|
|
1675
2471
|
duration_ms: Math.round(message.durationMs),
|
|
1676
2472
|
sessions: message.sessions
|
|
1677
2473
|
});
|
|
1678
|
-
this.
|
|
2474
|
+
this.finishBatch(activeBatch);
|
|
1679
2475
|
});
|
|
1680
2476
|
worker.on("error", (error) => {
|
|
1681
|
-
appLogger.error("search_index.worker_error", {
|
|
1682
|
-
|
|
2477
|
+
appLogger.error("search_index.worker_error", {
|
|
2478
|
+
context: this.activeBatch?.context ?? batch.context,
|
|
2479
|
+
error
|
|
2480
|
+
});
|
|
2481
|
+
this.invalidateWorker(worker, toError(error));
|
|
1683
2482
|
});
|
|
1684
|
-
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();
|
|
1685
2490
|
}
|
|
1686
|
-
|
|
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();
|
|
2499
|
+
}
|
|
2500
|
+
finishWorker(worker, code) {
|
|
2501
|
+
const batch = this.worker === worker ? this.activeBatch : null;
|
|
1687
2502
|
appLogger.info("search_index.worker_exited", {
|
|
1688
|
-
batch_id: batch
|
|
1689
|
-
context: batch
|
|
2503
|
+
batch_id: batch?.id,
|
|
2504
|
+
context: batch?.context,
|
|
1690
2505
|
code,
|
|
1691
2506
|
shutting_down: this.isShuttingDown || void 0
|
|
1692
2507
|
});
|
|
1693
|
-
if (this.worker
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
if (
|
|
1697
|
-
|
|
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
|
+
}
|
|
1698
2518
|
this.startNextBatch();
|
|
1699
2519
|
}
|
|
1700
2520
|
settle(batch, error) {
|
|
1701
|
-
if (!this.pendingJobs.settle(batch, error))
|
|
2521
|
+
if (!this.pendingJobs.settle(batch, error) && !this.pendingMaintenanceJobs.settle(batch, error)) {
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
1702
2524
|
appLogger.info("search_index.worker_settled", {
|
|
1703
2525
|
batch_id: batch.id,
|
|
1704
2526
|
context: batch.context,
|
|
@@ -1712,6 +2534,143 @@ var SearchIndexJobRunner = class {
|
|
|
1712
2534
|
}
|
|
1713
2535
|
};
|
|
1714
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
|
+
|
|
1715
2674
|
// src/scan-status-model.ts
|
|
1716
2675
|
var ScanStatusModel = class {
|
|
1717
2676
|
status = {
|
|
@@ -1723,7 +2682,13 @@ var ScanStatusModel = class {
|
|
|
1723
2682
|
agentStatuses: {},
|
|
1724
2683
|
totalAgents: 0,
|
|
1725
2684
|
updatedAt: Date.now(),
|
|
1726
|
-
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
|
+
}
|
|
1727
2692
|
};
|
|
1728
2693
|
snapshot() {
|
|
1729
2694
|
const backfill = {
|
|
@@ -1749,7 +2714,13 @@ var ScanStatusModel = class {
|
|
|
1749
2714
|
{ ...status }
|
|
1750
2715
|
])
|
|
1751
2716
|
),
|
|
1752
|
-
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
|
+
}
|
|
1753
2724
|
};
|
|
1754
2725
|
}
|
|
1755
2726
|
startBatch(agentNames, phase, sessionCounts) {
|
|
@@ -1819,7 +2790,7 @@ var ScanStatusModel = class {
|
|
|
1819
2790
|
}
|
|
1820
2791
|
updateAgent(agentName, progress) {
|
|
1821
2792
|
const status = this.status.agentStatuses[agentName];
|
|
1822
|
-
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") {
|
|
1823
2794
|
return null;
|
|
1824
2795
|
}
|
|
1825
2796
|
const now = Date.now();
|
|
@@ -1842,7 +2813,7 @@ var ScanStatusModel = class {
|
|
|
1842
2813
|
updatedAt: now
|
|
1843
2814
|
});
|
|
1844
2815
|
}
|
|
1845
|
-
|
|
2816
|
+
queueAgentPublication(agentName) {
|
|
1846
2817
|
const status = this.status.agentStatuses[agentName];
|
|
1847
2818
|
if (!this.status.active || !status || status.status !== "scanning" && status.status !== "finalizing") {
|
|
1848
2819
|
return null;
|
|
@@ -1850,7 +2821,24 @@ var ScanStatusModel = class {
|
|
|
1850
2821
|
const now = Date.now();
|
|
1851
2822
|
const agentStatuses = {
|
|
1852
2823
|
...this.status.agentStatuses,
|
|
1853
|
-
[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 }
|
|
1854
2842
|
};
|
|
1855
2843
|
return this.set({
|
|
1856
2844
|
...this.status,
|
|
@@ -1892,6 +2880,38 @@ var ScanStatusModel = class {
|
|
|
1892
2880
|
completedAt: isActive ? void 0 : now
|
|
1893
2881
|
});
|
|
1894
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
|
+
}
|
|
1895
2915
|
finishBatch() {
|
|
1896
2916
|
const now = Date.now();
|
|
1897
2917
|
return this.set({
|
|
@@ -1903,26 +2923,49 @@ var ScanStatusModel = class {
|
|
|
1903
2923
|
agentStatuses: Object.fromEntries(
|
|
1904
2924
|
Object.entries(this.status.agentStatuses).map(([agentName, status]) => [
|
|
1905
2925
|
agentName,
|
|
1906
|
-
|
|
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
|
+
}
|
|
1907
2932
|
])
|
|
1908
2933
|
),
|
|
1909
2934
|
updatedAt: now,
|
|
1910
2935
|
completedAt: now
|
|
1911
2936
|
});
|
|
1912
2937
|
}
|
|
1913
|
-
updateBackfill(
|
|
2938
|
+
updateBackfill(backfill) {
|
|
1914
2939
|
return this.set({
|
|
1915
2940
|
...this.status,
|
|
1916
|
-
backfill: {
|
|
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) {
|
|
2952
|
+
return this.set({
|
|
2953
|
+
...this.status,
|
|
2954
|
+
searchIndexMaintenance: {
|
|
2955
|
+
...searchIndexMaintenance,
|
|
2956
|
+
pendingAgents: [...searchIndexMaintenance.pendingAgents],
|
|
2957
|
+
completedAgents: [...searchIndexMaintenance.completedAgents],
|
|
2958
|
+
failedAgents: [...searchIndexMaintenance.failedAgents]
|
|
2959
|
+
},
|
|
1917
2960
|
updatedAt: Date.now()
|
|
1918
2961
|
});
|
|
1919
2962
|
}
|
|
1920
2963
|
activePhase(pendingAgents, scanningAgents, agentStatuses) {
|
|
1921
2964
|
if (pendingAgents.length === 0 && scanningAgents.length > 0 && scanningAgents.every((agentName) => {
|
|
1922
2965
|
const status = agentStatuses[agentName]?.status;
|
|
1923
|
-
return status === "finalizing" || status === "
|
|
2966
|
+
return status === "finalizing" || status === "publish-queued" || status === "publishing";
|
|
1924
2967
|
})) {
|
|
1925
|
-
return "
|
|
2968
|
+
return "publishing";
|
|
1926
2969
|
}
|
|
1927
2970
|
return this.status.phase === "initializing" ? "initializing" : "scanning";
|
|
1928
2971
|
}
|
|
@@ -1938,14 +2981,26 @@ var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
|
|
|
1938
2981
|
var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
|
|
1939
2982
|
var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1940
2983
|
var CACHE_TRUNCATION_COVERAGE = 0.5;
|
|
1941
|
-
|
|
2984
|
+
var STATUS_PROGRESS_INTERVAL_MS = 100;
|
|
2985
|
+
function buildPersistenceDiff(previousSessions, nextSessions, candidateChangedIds = [], completeness = "complete", explicitRemovedSessionIds = []) {
|
|
1942
2986
|
const { changes, removedSessionIds } = computeSessionDiff(
|
|
1943
2987
|
previousSessions,
|
|
1944
2988
|
nextSessions,
|
|
1945
2989
|
candidateChangedIds,
|
|
1946
2990
|
sessionSignature
|
|
1947
2991
|
);
|
|
1948
|
-
|
|
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`);
|
|
1949
3004
|
}
|
|
1950
3005
|
function restoreAgentCacheMeta(agent, cached) {
|
|
1951
3006
|
agent.setSessionMetaMap(new Map(Object.entries(cached.meta)));
|
|
@@ -1954,21 +3009,24 @@ var AgentSyncEngine = class {
|
|
|
1954
3009
|
constructor(options) {
|
|
1955
3010
|
this.options = options;
|
|
1956
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
|
+
);
|
|
1957
3017
|
}
|
|
1958
3018
|
options;
|
|
1959
3019
|
lastRefreshAtByAgent = /* @__PURE__ */ new Map();
|
|
1960
3020
|
scheduler;
|
|
1961
3021
|
sessionIndex = new LiveSessionIndex();
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
currentBackfillProgress;
|
|
1965
|
-
cacheIntegrityCheckedAgents = /* @__PURE__ */ new Set();
|
|
1966
|
-
completedBackfillAgents = [];
|
|
1967
|
-
failedBackfillAgents = [];
|
|
3022
|
+
backfills = new BackfillLifecycle();
|
|
3023
|
+
cacheIntegrityValidUntilByAgent = /* @__PURE__ */ new Map();
|
|
1968
3024
|
sessionsChangedListeners = /* @__PURE__ */ new Set();
|
|
1969
3025
|
statusChangedListeners = /* @__PURE__ */ new Set();
|
|
3026
|
+
statusProgressThrottles = /* @__PURE__ */ new Map();
|
|
1970
3027
|
scanStatus = new ScanStatusModel();
|
|
1971
|
-
searchIndexJobs
|
|
3028
|
+
searchIndexJobs;
|
|
3029
|
+
searchIndexMaintenance;
|
|
1972
3030
|
nextPublicationId = 1;
|
|
1973
3031
|
backgroundRefreshTimer = null;
|
|
1974
3032
|
isShuttingDown = false;
|
|
@@ -1981,6 +3039,13 @@ var AgentSyncEngine = class {
|
|
|
1981
3039
|
options.cacheTimestamps?.[agent.name] ?? Date.now()
|
|
1982
3040
|
);
|
|
1983
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
|
+
}
|
|
1984
3049
|
}
|
|
1985
3050
|
snapshot() {
|
|
1986
3051
|
return this.sessionIndex.snapshot();
|
|
@@ -2002,6 +3067,7 @@ var AgentSyncEngine = class {
|
|
|
2002
3067
|
publicationId: this.publicationId("scan.initial"),
|
|
2003
3068
|
agents: jobs.map((job) => job.agentName)
|
|
2004
3069
|
});
|
|
3070
|
+
for (const job of jobs) this.searchIndexMaintenance.enqueue(job.agentName);
|
|
2005
3071
|
}
|
|
2006
3072
|
handleAgentsChanged(agentNames) {
|
|
2007
3073
|
const snapshot = this.sessionIndex.snapshot();
|
|
@@ -2029,7 +3095,7 @@ var AgentSyncEngine = class {
|
|
|
2029
3095
|
const activeOperations = {
|
|
2030
3096
|
agent_operations: schedulerSnapshot.activeOperations,
|
|
2031
3097
|
refreshes: schedulerSnapshot.activeRefreshes,
|
|
2032
|
-
backfill_running: this.
|
|
3098
|
+
backfill_running: this.backfills.runningAttempt()?.agentName || void 0,
|
|
2033
3099
|
scan_workers: this.options.workerRunner.activeCount
|
|
2034
3100
|
};
|
|
2035
3101
|
if (activeOperations.agent_operations > 0 || activeOperations.scan_workers > 0) {
|
|
@@ -2040,16 +3106,17 @@ var AgentSyncEngine = class {
|
|
|
2040
3106
|
clearTimeout(this.backgroundRefreshTimer);
|
|
2041
3107
|
this.backgroundRefreshTimer = null;
|
|
2042
3108
|
}
|
|
2043
|
-
this.
|
|
2044
|
-
this.
|
|
2045
|
-
this.
|
|
2046
|
-
this.
|
|
3109
|
+
this.backfills.cancelAll();
|
|
3110
|
+
this.searchIndexMaintenance.stop();
|
|
3111
|
+
this.cancelProgressStatuses();
|
|
3112
|
+
this.cacheIntegrityValidUntilByAgent.clear();
|
|
2047
3113
|
const searchIndexSnapshot = this.searchIndexJobs.snapshot();
|
|
2048
3114
|
appLogger.info("search_index.shutdown.started", {
|
|
2049
3115
|
active_batch_id: searchIndexSnapshot.activeBatchId,
|
|
2050
3116
|
pending_batches: searchIndexSnapshot.pendingBatches
|
|
2051
3117
|
});
|
|
2052
3118
|
await this.searchIndexJobs.shutdown();
|
|
3119
|
+
await this.searchIndexMaintenance.waitForIdle();
|
|
2053
3120
|
await this.options.workerRunner.shutdown();
|
|
2054
3121
|
await this.scheduler.waitForIdle();
|
|
2055
3122
|
const stoppedSearchIndexSnapshot = this.searchIndexJobs.snapshot();
|
|
@@ -2069,41 +3136,106 @@ var AgentSyncEngine = class {
|
|
|
2069
3136
|
this.publishStatus(this.scanStatus.setPhase(phase));
|
|
2070
3137
|
}
|
|
2071
3138
|
beginAgentScan(agentName) {
|
|
3139
|
+
this.cancelProgressStatus(`scan:${agentName}`);
|
|
2072
3140
|
const snapshot = this.sessionIndex.snapshot();
|
|
2073
3141
|
if (!this.scanStatus.snapshot().active) this.startScanBatch([agentName], "scanning");
|
|
2074
3142
|
this.publishStatus(
|
|
2075
3143
|
this.scanStatus.beginAgent(agentName, snapshot.byAgent[agentName]?.length ?? 0)
|
|
2076
3144
|
);
|
|
2077
3145
|
}
|
|
2078
|
-
updateAgentScanProgress(agentName, progress) {
|
|
2079
|
-
if (
|
|
2080
|
-
|
|
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, {
|
|
2081
3157
|
phase: progress.phase,
|
|
2082
3158
|
total: progress.total,
|
|
2083
3159
|
processed: progress.processed,
|
|
2084
3160
|
sessions: progress.sessions
|
|
2085
|
-
}
|
|
2086
|
-
|
|
3161
|
+
})) {
|
|
3162
|
+
this.publishProgressStatus(
|
|
3163
|
+
key2,
|
|
3164
|
+
nextPhase2,
|
|
3165
|
+
this.scanStatus.updateBackfill(this.backfills.status())
|
|
3166
|
+
);
|
|
3167
|
+
}
|
|
2087
3168
|
return;
|
|
2088
3169
|
}
|
|
2089
|
-
|
|
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);
|
|
2090
3176
|
}
|
|
2091
|
-
|
|
2092
|
-
this.
|
|
3177
|
+
beginAgentPublishing(agentName) {
|
|
3178
|
+
this.flushProgressStatus(`scan:${agentName}`);
|
|
3179
|
+
this.publishStatus(this.scanStatus.publishAgent(agentName));
|
|
3180
|
+
}
|
|
3181
|
+
queueAgentPublication(agentName) {
|
|
3182
|
+
this.flushProgressStatus(`scan:${agentName}`);
|
|
3183
|
+
this.publishStatus(this.scanStatus.queueAgentPublication(agentName));
|
|
2093
3184
|
}
|
|
2094
3185
|
finishAgentScan(agentName) {
|
|
3186
|
+
this.flushProgressStatus(`scan:${agentName}`);
|
|
2095
3187
|
const count = this.sessionIndex.snapshot().byAgent[agentName]?.length;
|
|
2096
3188
|
this.publishStatus(this.scanStatus.finishAgent(agentName, count));
|
|
2097
3189
|
}
|
|
2098
3190
|
finishScanBatch() {
|
|
3191
|
+
this.flushProgressStatuses("scan:");
|
|
2099
3192
|
this.publishStatus(this.scanStatus.finishBatch());
|
|
2100
3193
|
}
|
|
2101
3194
|
publishBackfillStatus() {
|
|
2102
|
-
this.publishStatus(this.scanStatus.updateBackfill(this.
|
|
3195
|
+
this.publishStatus(this.scanStatus.updateBackfill(this.backfills.status()));
|
|
2103
3196
|
}
|
|
2104
3197
|
publishStatus(event) {
|
|
2105
3198
|
if (!event || this.isShuttingDown) return;
|
|
2106
|
-
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();
|
|
2107
3239
|
}
|
|
2108
3240
|
emitSessionsChanged(change) {
|
|
2109
3241
|
if (this.isShuttingDown) return;
|
|
@@ -2111,20 +3243,33 @@ var AgentSyncEngine = class {
|
|
|
2111
3243
|
}
|
|
2112
3244
|
async performRefresh(agentName) {
|
|
2113
3245
|
this.beginAgentScan(agentName);
|
|
3246
|
+
const startedAt = performance.now();
|
|
3247
|
+
let failed = false;
|
|
3248
|
+
let cached = null;
|
|
3249
|
+
let result = null;
|
|
2114
3250
|
try {
|
|
2115
|
-
|
|
3251
|
+
if (this.findAgent(agentName)) cached = loadCachedSessions(agentName);
|
|
3252
|
+
result = await this.runRefresh(agentName, cached, startedAt);
|
|
3253
|
+
return result;
|
|
2116
3254
|
} catch (error) {
|
|
3255
|
+
this.options.workerRunner.discard?.(agentName);
|
|
3256
|
+
failed = true;
|
|
3257
|
+
const failure = toError(error);
|
|
2117
3258
|
appLogger.error("scan.refresh.error", { agent: agentName, error });
|
|
2118
3259
|
console.error(`[${agentName}] Session refresh failed:`, error);
|
|
3260
|
+
this.flushProgressStatus(`scan:${agentName}`);
|
|
3261
|
+
this.publishStatus(this.scanStatus.failAgent(agentName, failure.message));
|
|
2119
3262
|
return "failed";
|
|
2120
3263
|
} finally {
|
|
2121
|
-
this.finishAgentScan(agentName);
|
|
3264
|
+
if (!failed) this.finishAgentScan(agentName);
|
|
2122
3265
|
const agent = this.findAgent(agentName);
|
|
2123
|
-
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);
|
|
2124
3270
|
}
|
|
2125
3271
|
}
|
|
2126
|
-
async runRefresh(agentName) {
|
|
2127
|
-
const startedAt = performance.now();
|
|
3272
|
+
async runRefresh(agentName, cached, startedAt) {
|
|
2128
3273
|
const pendingPathCount = this.scheduler.takePendingSignalCount(agentName);
|
|
2129
3274
|
const agent = this.findAgent(agentName);
|
|
2130
3275
|
if (!agent) {
|
|
@@ -2132,11 +3277,20 @@ var AgentSyncEngine = class {
|
|
|
2132
3277
|
return "skipped";
|
|
2133
3278
|
}
|
|
2134
3279
|
const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
|
|
2135
|
-
const cached = loadCachedSessions(agentName);
|
|
2136
3280
|
const refreshBaseline = cached?.sessions ?? previousSessions;
|
|
2137
3281
|
const cacheTimestamp = cached?.timestamp ?? this.lastRefreshAtByAgent.get(agentName) ?? 0;
|
|
2138
3282
|
if (cached) restoreAgentCacheMeta(agent, cached);
|
|
2139
|
-
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;
|
|
2140
3294
|
const availabilityStartedAt = performance.now();
|
|
2141
3295
|
const isAvailable = agent.isAvailable();
|
|
2142
3296
|
const availabilityDuration = performance.now() - availabilityStartedAt;
|
|
@@ -2157,10 +3311,29 @@ var AgentSyncEngine = class {
|
|
|
2157
3311
|
} else {
|
|
2158
3312
|
strategyResult = await this.scanAgentFully(agent, previousSessions);
|
|
2159
3313
|
}
|
|
2160
|
-
if (strategyResult.status === "unchanged")
|
|
3314
|
+
if (strategyResult.status === "unchanged") {
|
|
3315
|
+
this.options.workerRunner.commit?.(agentName);
|
|
3316
|
+
return "unchanged";
|
|
3317
|
+
}
|
|
2161
3318
|
const nextSessions = attachMissingProjectIdentities(strategyResult.nextSessions);
|
|
2162
3319
|
const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
|
|
2163
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
|
+
});
|
|
2164
3337
|
const changedSessionIds = persistenceDiff ? new Set(persistenceDiff.changedSessions.map(({ session }) => session.id)) : void 0;
|
|
2165
3338
|
const persistStartedAt = performance.now();
|
|
2166
3339
|
const persistentJob = persistenceDiff ? {
|
|
@@ -2175,39 +3348,62 @@ var AgentSyncEngine = class {
|
|
|
2175
3348
|
kind: "full",
|
|
2176
3349
|
context: "scan.refresh",
|
|
2177
3350
|
agentName,
|
|
2178
|
-
sessions:
|
|
3351
|
+
sessions: publicationSessions,
|
|
2179
3352
|
meta: buildAgentCacheMeta(agent),
|
|
3353
|
+
completeness: strategyResult.completeness,
|
|
3354
|
+
removedSessionIds: strategyResult.explicitRemovedSessionIds,
|
|
2180
3355
|
saveCache: true,
|
|
2181
3356
|
...searchIndexOptions ? { searchIndexOptions } : {}
|
|
2182
3357
|
};
|
|
2183
|
-
this.
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
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);
|
|
2191
3376
|
const persistDuration = performance.now() - persistStartedAt;
|
|
2192
3377
|
logSearchIndexSync("scan.refresh", null, { pending_paths: pendingPathCount });
|
|
2193
3378
|
const totalDurationMs = performance.now() - startedAt;
|
|
2194
3379
|
this.scheduler.recordRefreshDuration(agentName, totalDurationMs);
|
|
2195
|
-
appLogger.info(
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
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
|
+
}
|
|
2211
3407
|
return "committed";
|
|
2212
3408
|
}
|
|
2213
3409
|
/**
|
|
@@ -2224,28 +3420,29 @@ var AgentSyncEngine = class {
|
|
|
2224
3420
|
agent: agentName,
|
|
2225
3421
|
retained_sessions: previousSessions.length
|
|
2226
3422
|
});
|
|
2227
|
-
return this.refreshStrategyResult(previousSessions, { status: "unchanged" });
|
|
3423
|
+
return this.refreshStrategyResult(previousSessions, "partial", {}, { status: "unchanged" });
|
|
2228
3424
|
}
|
|
2229
|
-
return this.refreshStrategyResult([]);
|
|
3425
|
+
return this.refreshStrategyResult([], "partial", {});
|
|
2230
3426
|
}
|
|
2231
3427
|
async initializeAgent(agent, previousSessions) {
|
|
2232
3428
|
this.setScanPhase("initializing");
|
|
2233
3429
|
const scanStartedAt = performance.now();
|
|
2234
|
-
const
|
|
2235
|
-
|
|
2236
|
-
});
|
|
3430
|
+
const scope = this.startupScanOptions();
|
|
3431
|
+
const result = await this.runWorker(agent, previousSessions, { kind: "full-scan" }, scope);
|
|
2237
3432
|
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2238
3433
|
const sessions = attachMissingProjectIdentities(result.sessions);
|
|
2239
3434
|
this.lastRefreshAtByAgent.set(agent.name, Date.now());
|
|
2240
|
-
return this.refreshStrategyResult(sessions, {
|
|
3435
|
+
return this.refreshStrategyResult(sessions, result.completeness, scope, {
|
|
2241
3436
|
fullScanSessions: sessions,
|
|
2242
|
-
|
|
3437
|
+
explicitRemovedSessionIds: result.explicitRemovedSessionIds,
|
|
3438
|
+
scanDuration: performance.now() - scanStartedAt,
|
|
3439
|
+
sourceFailures: result.sourceFailures ?? []
|
|
2243
3440
|
});
|
|
2244
3441
|
}
|
|
2245
3442
|
async syncAgentSources(agent, cached, refreshStartedAt) {
|
|
2246
3443
|
const scanStartedAt = performance.now();
|
|
2247
|
-
const
|
|
2248
|
-
|
|
3444
|
+
const scope = this.startupScanOptions();
|
|
3445
|
+
const result = await this.runWorker(agent, cached.sessions, { kind: "source-refresh" }, scope, {
|
|
2249
3446
|
meta: cached.meta
|
|
2250
3447
|
});
|
|
2251
3448
|
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
@@ -2253,62 +3450,130 @@ var AgentSyncEngine = class {
|
|
|
2253
3450
|
const preciseChangedIds = result.changedIds ?? [];
|
|
2254
3451
|
const persistenceDiff = buildPersistenceDiff(cached.sessions, sessions, preciseChangedIds);
|
|
2255
3452
|
this.lastRefreshAtByAgent.set(agent.name, Date.now());
|
|
2256
|
-
if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0) {
|
|
3453
|
+
if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0 && (result.sourceFailures?.length ?? 0) === 0) {
|
|
2257
3454
|
this.logUnchangedRefresh(agent.name, refreshStartedAt);
|
|
2258
|
-
return this.refreshStrategyResult(sessions, {
|
|
3455
|
+
return this.refreshStrategyResult(sessions, result.completeness, scope, {
|
|
2259
3456
|
status: "unchanged",
|
|
2260
3457
|
scanDuration: performance.now() - scanStartedAt
|
|
2261
3458
|
});
|
|
2262
3459
|
}
|
|
2263
|
-
return this.refreshStrategyResult(sessions, {
|
|
3460
|
+
return this.refreshStrategyResult(sessions, result.completeness, scope, {
|
|
2264
3461
|
preciseChangedIds,
|
|
2265
3462
|
persistenceDiff,
|
|
2266
|
-
scanDuration: performance.now() - scanStartedAt
|
|
3463
|
+
scanDuration: performance.now() - scanStartedAt,
|
|
3464
|
+
sourceFailures: result.sourceFailures ?? []
|
|
2267
3465
|
});
|
|
2268
3466
|
}
|
|
2269
3467
|
async refreshChangedAgent(agent, baseline, cacheTimestamp, refreshStartedAt) {
|
|
3468
|
+
const scope = this.startupScanOptions();
|
|
2270
3469
|
const checkStartedAt = performance.now();
|
|
2271
3470
|
const checkResult = await Promise.resolve(agent.checkForChanges(cacheTimestamp, baseline));
|
|
2272
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
|
+
}
|
|
2273
3486
|
this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
|
|
2274
3487
|
if (!checkResult.hasChanges) {
|
|
2275
|
-
|
|
2276
|
-
|
|
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
|
+
);
|
|
2277
3517
|
}
|
|
2278
3518
|
const preciseChangedIds = checkResult.changedIds ?? null;
|
|
2279
3519
|
const scanStartedAt = performance.now();
|
|
2280
3520
|
if (preciseChangedIds === null) {
|
|
2281
|
-
const result = await this.runWorker(agent, baseline,
|
|
3521
|
+
const result = await this.runWorker(agent, baseline, { kind: "full-scan" }, scope);
|
|
2282
3522
|
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2283
3523
|
const sessions2 = attachMissingProjectIdentities(result.sessions);
|
|
2284
|
-
return this.refreshStrategyResult(sessions2, {
|
|
2285
|
-
|
|
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
|
+
),
|
|
2286
3535
|
checkDuration,
|
|
2287
|
-
scanDuration: performance.now() - scanStartedAt
|
|
3536
|
+
scanDuration: performance.now() - scanStartedAt,
|
|
3537
|
+
sourceFailures: result.sourceFailures ?? []
|
|
2288
3538
|
});
|
|
2289
3539
|
}
|
|
3540
|
+
this.options.workerRunner.discard?.(agent.name);
|
|
2290
3541
|
const sessions = attachMissingProjectIdentities(
|
|
2291
|
-
await Promise.resolve(
|
|
3542
|
+
await Promise.resolve(
|
|
3543
|
+
agent.incrementalScan(baseline, preciseChangedIds, checkResult.refs, scope)
|
|
3544
|
+
)
|
|
2292
3545
|
);
|
|
2293
|
-
|
|
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, {
|
|
2294
3549
|
preciseChangedIds,
|
|
2295
|
-
persistenceDiff: buildPersistenceDiff(
|
|
3550
|
+
persistenceDiff: buildPersistenceDiff(
|
|
3551
|
+
baseline,
|
|
3552
|
+
sessions,
|
|
3553
|
+
preciseChangedIds,
|
|
3554
|
+
completeness,
|
|
3555
|
+
preciseChangedIds
|
|
3556
|
+
),
|
|
2296
3557
|
checkDuration,
|
|
2297
|
-
scanDuration: performance.now() - scanStartedAt
|
|
3558
|
+
scanDuration: performance.now() - scanStartedAt,
|
|
3559
|
+
sourceFailures
|
|
2298
3560
|
});
|
|
2299
3561
|
}
|
|
2300
3562
|
async scanAgentFully(agent, previousSessions) {
|
|
2301
3563
|
const scanStartedAt = performance.now();
|
|
2302
|
-
const
|
|
3564
|
+
const scope = this.startupScanOptions();
|
|
3565
|
+
const result = await this.runWorker(agent, previousSessions, { kind: "full-scan" }, scope);
|
|
2303
3566
|
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2304
3567
|
const sessions = attachMissingProjectIdentities(result.sessions);
|
|
2305
3568
|
this.lastRefreshAtByAgent.set(agent.name, Date.now());
|
|
2306
|
-
return this.refreshStrategyResult(sessions, {
|
|
3569
|
+
return this.refreshStrategyResult(sessions, result.completeness, scope, {
|
|
2307
3570
|
fullScanSessions: sessions,
|
|
2308
|
-
|
|
3571
|
+
explicitRemovedSessionIds: result.explicitRemovedSessionIds,
|
|
3572
|
+
scanDuration: performance.now() - scanStartedAt,
|
|
3573
|
+
sourceFailures: result.sourceFailures ?? []
|
|
2309
3574
|
});
|
|
2310
3575
|
}
|
|
2311
|
-
refreshStrategyResult(nextSessions, overrides = {}) {
|
|
3576
|
+
refreshStrategyResult(nextSessions, completeness, scope, overrides = {}) {
|
|
2312
3577
|
return {
|
|
2313
3578
|
status: "continue",
|
|
2314
3579
|
nextSessions,
|
|
@@ -2317,86 +3582,55 @@ var AgentSyncEngine = class {
|
|
|
2317
3582
|
persistenceDiff: null,
|
|
2318
3583
|
checkDuration: 0,
|
|
2319
3584
|
scanDuration: 0,
|
|
3585
|
+
sourceFailures: [],
|
|
3586
|
+
completeness,
|
|
3587
|
+
scope,
|
|
3588
|
+
explicitRemovedSessionIds: [],
|
|
2320
3589
|
...overrides
|
|
2321
3590
|
};
|
|
2322
3591
|
}
|
|
2323
|
-
runWorker(agent, previousSessions,
|
|
3592
|
+
runWorker(agent, previousSessions, operation, scanOptions, runOptions = {}) {
|
|
2324
3593
|
return this.options.workerRunner.run(agent.name, {
|
|
2325
3594
|
previousSessions,
|
|
2326
|
-
|
|
3595
|
+
operation,
|
|
2327
3596
|
scanOptions,
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
checkpoint: workerOptions.checkpoint,
|
|
2332
|
-
meta: workerOptions.meta ?? buildAgentCacheMeta(agent),
|
|
2333
|
-
onProgress: (progress) => this.updateAgentScanProgress(agent.name, progress),
|
|
2334
|
-
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
|
|
2335
3600
|
});
|
|
2336
3601
|
}
|
|
2337
|
-
|
|
2338
|
-
if (
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
} catch (error) {
|
|
2345
|
-
appLogger.error("scan.checkpoint.failed", {
|
|
2346
|
-
agent: agent.name,
|
|
2347
|
-
stage: checkpoint.stage,
|
|
2348
|
-
error
|
|
2349
|
-
});
|
|
2350
|
-
}
|
|
2351
|
-
agent.setSessionMetaMap(new Map(Object.entries(checkpoint.meta)));
|
|
2352
|
-
const event = this.sessionIndex.commitAgentSessions(agent.name, checkpoint.sessions);
|
|
2353
|
-
this.emitSessionsChanged({
|
|
2354
|
-
agentName: agent.name,
|
|
2355
|
-
sessions: this.sessionIndex.snapshot().byAgent[agent.name] ?? [],
|
|
2356
|
-
event
|
|
2357
|
-
});
|
|
2358
|
-
appLogger.info("scan.checkpoint.persisted", {
|
|
2359
|
-
agent: agent.name,
|
|
2360
|
-
stage: checkpoint.stage,
|
|
2361
|
-
sessions: checkpoint.sessions.length,
|
|
2362
|
-
persisted: persisted2
|
|
2363
|
-
});
|
|
2364
|
-
return;
|
|
2365
|
-
}
|
|
2366
|
-
let persisted = false;
|
|
2367
|
-
try {
|
|
2368
|
-
persisted = saveCachedSessionChanges(agent.name, checkpoint.changes, [], checkpoint.meta);
|
|
2369
|
-
} catch (error) {
|
|
2370
|
-
appLogger.error("scan.checkpoint.failed", {
|
|
2371
|
-
agent: agent.name,
|
|
2372
|
-
stage: checkpoint.stage,
|
|
2373
|
-
sessions: checkpoint.changes.length,
|
|
2374
|
-
error
|
|
2375
|
-
});
|
|
2376
|
-
}
|
|
2377
|
-
if (persisted && checkpoint.backfillCursor) {
|
|
2378
|
-
markAgentFullSyncProgress(agent.name, checkpoint.backfillCursor);
|
|
2379
|
-
}
|
|
2380
|
-
appLogger.debug("scan.checkpoint.persisted", {
|
|
2381
|
-
agent: agent.name,
|
|
2382
|
-
stage: checkpoint.stage,
|
|
2383
|
-
sessions: checkpoint.changes.length,
|
|
2384
|
-
backfill_cursor: checkpoint.backfillCursor,
|
|
2385
|
-
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
|
|
2386
3609
|
});
|
|
2387
3610
|
}
|
|
2388
|
-
needsBackfill(agent) {
|
|
3611
|
+
needsBackfill(agent, cached, reloadCached = false) {
|
|
2389
3612
|
const startupScanOptions = this.startupScanOptions();
|
|
2390
3613
|
if (startupScanOptions.from == null && startupScanOptions.to == null) return false;
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
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
|
+
}
|
|
2394
3628
|
if (!(agent instanceof FileSystemSessionSource)) return false;
|
|
2395
|
-
if (
|
|
2396
|
-
const
|
|
3629
|
+
if (!agent.isAvailable()) return false;
|
|
3630
|
+
const cachedSessions = reloadCached || cached === void 0 ? loadCachedSessions(agent.name) : cached;
|
|
2397
3631
|
const sourceCount = agent.listSessionSources().length;
|
|
2398
|
-
const cachedCount =
|
|
2399
|
-
this.
|
|
3632
|
+
const cachedCount = cachedSessions?.sessions.length ?? 0;
|
|
3633
|
+
this.cacheIntegrityValidUntilByAgent.set(agent.name, lastSyncAt + BACKFILL_INTERVAL_MS);
|
|
2400
3634
|
if (sourceCount > 0 && cachedCount / sourceCount < CACHE_TRUNCATION_COVERAGE) {
|
|
2401
3635
|
appLogger.warn("scan.backfill.cache_truncated", {
|
|
2402
3636
|
agent: agent.name,
|
|
@@ -2409,43 +3643,45 @@ var AgentSyncEngine = class {
|
|
|
2409
3643
|
return false;
|
|
2410
3644
|
}
|
|
2411
3645
|
enqueueBackfill(agentName) {
|
|
2412
|
-
if (this.isShuttingDown || this.
|
|
2413
|
-
return;
|
|
2414
|
-
}
|
|
2415
|
-
this.backfillQueue.push(agentName);
|
|
3646
|
+
if (this.isShuttingDown || !this.backfills.enqueue(agentName)) return;
|
|
2416
3647
|
this.publishBackfillStatus();
|
|
2417
3648
|
this.pumpBackfillQueue();
|
|
2418
3649
|
}
|
|
2419
3650
|
pumpBackfillQueue() {
|
|
2420
|
-
if (this.isShuttingDown
|
|
2421
|
-
const
|
|
2422
|
-
if (!
|
|
2423
|
-
this.currentBackfillAgent = agentName;
|
|
2424
|
-
this.currentBackfillProgress = void 0;
|
|
3651
|
+
if (this.isShuttingDown) return;
|
|
3652
|
+
const attempt = this.backfills.startNext();
|
|
3653
|
+
if (!attempt) return;
|
|
2425
3654
|
this.publishBackfillStatus();
|
|
2426
|
-
void this.runBackfill(
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
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
|
+
);
|
|
2433
3670
|
}
|
|
2434
|
-
this.
|
|
2435
|
-
(failedAgent) => failedAgent !== agentName
|
|
2436
|
-
);
|
|
2437
|
-
} else if (!this.failedBackfillAgents.includes(agentName)) {
|
|
2438
|
-
this.failedBackfillAgents.push(agentName);
|
|
2439
|
-
this.cacheIntegrityCheckedAgents.delete(agentName);
|
|
3671
|
+
this.publishBackfillStatus();
|
|
2440
3672
|
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
3673
|
+
}
|
|
3674
|
+
this.pumpBackfillQueue();
|
|
3675
|
+
}
|
|
3676
|
+
rejectBackfillAttempt(attempt, error) {
|
|
3677
|
+
appLogger.error("scan.backfill.queue_error", { agent: attempt.agentName, error });
|
|
3678
|
+
this.completeBackfillAttempt(attempt, "failed");
|
|
2444
3679
|
}
|
|
2445
|
-
runBackfill(
|
|
2446
|
-
return this.scheduler.run(agentName, "backfill", () => this.performBackfill(
|
|
3680
|
+
runBackfill(attempt) {
|
|
3681
|
+
return this.scheduler.run(attempt.agentName, "backfill", () => this.performBackfill(attempt));
|
|
2447
3682
|
}
|
|
2448
|
-
async performBackfill(
|
|
3683
|
+
async performBackfill(attempt) {
|
|
3684
|
+
const { agentName } = attempt;
|
|
2449
3685
|
const startedAt = performance.now();
|
|
2450
3686
|
const agent = this.findAgent(agentName);
|
|
2451
3687
|
if (!agent || !agent.isAvailable()) return "skipped";
|
|
@@ -2455,25 +3691,42 @@ var AgentSyncEngine = class {
|
|
|
2455
3691
|
const meta = cached?.meta ?? buildAgentCacheMeta(agent);
|
|
2456
3692
|
const backfillCursor = getAgentFullSyncCursor(agentName);
|
|
2457
3693
|
if (cached) restoreAgentCacheMeta(agent, cached);
|
|
3694
|
+
let durableCommitted = false;
|
|
2458
3695
|
try {
|
|
2459
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
|
+
});
|
|
2460
3703
|
const result = await this.runWorker(
|
|
2461
3704
|
agent,
|
|
2462
3705
|
baseline,
|
|
2463
|
-
|
|
3706
|
+
operation,
|
|
2464
3707
|
{},
|
|
2465
3708
|
{
|
|
2466
|
-
|
|
2467
|
-
backfill: true,
|
|
2468
|
-
backfillCursor,
|
|
3709
|
+
backfillAttempt: attempt,
|
|
2469
3710
|
meta,
|
|
2470
|
-
|
|
3711
|
+
onCheckpoint: (checkpoint) => this.handleBackfillCheckpoint(agentName, checkpoint)
|
|
2471
3712
|
}
|
|
2472
3713
|
);
|
|
2473
3714
|
agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
|
|
2474
3715
|
const fullSessions = attachMissingProjectIdentities(result.sessions);
|
|
2475
|
-
this.
|
|
2476
|
-
|
|
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");
|
|
2477
3730
|
await this.commitSessionPublication({
|
|
2478
3731
|
context: "scan.backfill",
|
|
2479
3732
|
agentName,
|
|
@@ -2485,9 +3738,15 @@ var AgentSyncEngine = class {
|
|
|
2485
3738
|
agentName,
|
|
2486
3739
|
sessions: fullSessions,
|
|
2487
3740
|
meta: buildAgentCacheMeta(agent),
|
|
3741
|
+
completeness: result.completeness,
|
|
3742
|
+
removedSessionIds: result.explicitRemovedSessionIds,
|
|
2488
3743
|
saveCache: true
|
|
2489
|
-
}
|
|
3744
|
+
},
|
|
3745
|
+
onPublishing: () => updatePublicationPhase("publishing")
|
|
2490
3746
|
});
|
|
3747
|
+
durableCommitted = true;
|
|
3748
|
+
this.options.workerRunner.commit?.(agentName);
|
|
3749
|
+
if (result.sourceFailures?.length) throw sourceFailureError(result.sourceFailures);
|
|
2491
3750
|
markAgentFullSyncCompleted(agentName);
|
|
2492
3751
|
appLogger.info("scan.backfill.done", {
|
|
2493
3752
|
agent: agentName,
|
|
@@ -2497,38 +3756,39 @@ var AgentSyncEngine = class {
|
|
|
2497
3756
|
});
|
|
2498
3757
|
return "committed";
|
|
2499
3758
|
} catch (error) {
|
|
3759
|
+
if (!durableCommitted) agent.setSessionMetaMap(new Map(Object.entries(meta)));
|
|
3760
|
+
this.options.workerRunner.discard?.(agentName);
|
|
2500
3761
|
appLogger.error("scan.backfill.error", { agent: agentName, error });
|
|
2501
3762
|
console.error(`[${agentName}] Backfill failed:`, error);
|
|
2502
3763
|
return "failed";
|
|
2503
3764
|
}
|
|
2504
3765
|
}
|
|
2505
|
-
backfillStatus() {
|
|
2506
|
-
return {
|
|
2507
|
-
active: this.currentBackfillAgent != null || this.backfillQueue.length > 0,
|
|
2508
|
-
pendingAgents: [...this.backfillQueue],
|
|
2509
|
-
currentAgent: this.currentBackfillAgent,
|
|
2510
|
-
progress: this.currentBackfillProgress,
|
|
2511
|
-
completedAgents: [...this.completedBackfillAgents],
|
|
2512
|
-
failedAgents: [...this.failedBackfillAgents]
|
|
2513
|
-
};
|
|
2514
|
-
}
|
|
2515
3766
|
buildFullSearchIndexJobs(context) {
|
|
2516
3767
|
const snapshot = this.sessionIndex.snapshot();
|
|
2517
|
-
return snapshot.agents.
|
|
3768
|
+
return snapshot.agents.flatMap((agent) => {
|
|
3769
|
+
if (!(agent.name in snapshot.byAgent)) return [];
|
|
2518
3770
|
const cached = loadCachedSessions(agent.name);
|
|
2519
|
-
return
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
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
|
+
];
|
|
2532
3792
|
});
|
|
2533
3793
|
}
|
|
2534
3794
|
publicationId(context, agentName) {
|
|
@@ -2544,7 +3804,11 @@ var AgentSyncEngine = class {
|
|
|
2544
3804
|
jobs: jobs.length
|
|
2545
3805
|
});
|
|
2546
3806
|
try {
|
|
2547
|
-
|
|
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));
|
|
2548
3812
|
} catch (error) {
|
|
2549
3813
|
appLogger.error("session.publication.failed", {
|
|
2550
3814
|
publication_id: details.publicationId,
|
|
@@ -2565,7 +3829,8 @@ var AgentSyncEngine = class {
|
|
|
2565
3829
|
const publicationId = this.publicationId(publication.context, publication.agentName);
|
|
2566
3830
|
await this.commitSearchIndex(publication.context, [publication.indexJob], {
|
|
2567
3831
|
publicationId,
|
|
2568
|
-
agent: publication.agentName
|
|
3832
|
+
agent: publication.agentName,
|
|
3833
|
+
...publication.onPublishing ? { onStarted: publication.onPublishing } : {}
|
|
2569
3834
|
});
|
|
2570
3835
|
const diffStartedAt = performance.now();
|
|
2571
3836
|
const event = this.sessionIndex.commitAgentSessions(
|
|
@@ -2742,7 +4007,7 @@ var SessionWatcher = class {
|
|
|
2742
4007
|
}
|
|
2743
4008
|
}
|
|
2744
4009
|
this.stablePaths.clear();
|
|
2745
|
-
await Promise.all(this.watchers.map((watcher) => watcher.close()));
|
|
4010
|
+
await Promise.all(this.watchers.map(({ watcher }) => watcher.close()));
|
|
2746
4011
|
this.watchers = [];
|
|
2747
4012
|
this.fallbackWatchScopes.clear();
|
|
2748
4013
|
this.listeners.clear();
|
|
@@ -2765,7 +4030,7 @@ var SessionWatcher = class {
|
|
|
2765
4030
|
watcher.on("error", (error) => {
|
|
2766
4031
|
this.reportWatchError("watch.error", { path, recursive, error });
|
|
2767
4032
|
});
|
|
2768
|
-
this.watchers.push(watcher);
|
|
4033
|
+
this.watchers.push({ path, recursive, watcher });
|
|
2769
4034
|
return true;
|
|
2770
4035
|
} catch (error) {
|
|
2771
4036
|
if (recursive && isRecursiveWatchUnavailable(error)) {
|
|
@@ -2811,7 +4076,20 @@ var SessionWatcher = class {
|
|
|
2811
4076
|
this.watchDirectoryTree(path, scopes);
|
|
2812
4077
|
}
|
|
2813
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);
|
|
2814
4091
|
}
|
|
4092
|
+
this.watchers = active;
|
|
2815
4093
|
}
|
|
2816
4094
|
handleWatchEvent(watchPath, scopes, eventType, filename) {
|
|
2817
4095
|
const changedPath = resolveWatchEventPath(watchPath, filename);
|
|
@@ -2893,6 +4171,7 @@ var SessionWatcher = class {
|
|
|
2893
4171
|
import { Worker as Worker2 } from "worker_threads";
|
|
2894
4172
|
var SHUTDOWN_ERROR_MESSAGE2 = "Scan refresh worker shut down";
|
|
2895
4173
|
function applySessionChanges2(previousSessions, changes, removedSessionIds) {
|
|
4174
|
+
if (changes.length === 0 && removedSessionIds.length === 0) return previousSessions;
|
|
2896
4175
|
const replacedIds = new Set(removedSessionIds);
|
|
2897
4176
|
for (const { session } of changes) replacedIds.add(session.id);
|
|
2898
4177
|
const retained = previousSessions.filter((session) => !replacedIds.has(session.id));
|
|
@@ -2915,8 +4194,10 @@ function applySessionChanges2(previousSessions, changes, removedSessionIds) {
|
|
|
2915
4194
|
return next;
|
|
2916
4195
|
}
|
|
2917
4196
|
function applyMetaChanges(previous, changed, replacedSessionIds) {
|
|
4197
|
+
const replacedIds = [...replacedSessionIds];
|
|
4198
|
+
if (Object.keys(changed).length === 0 && replacedIds.length === 0) return previous;
|
|
2918
4199
|
const next = { ...previous };
|
|
2919
|
-
for (const id of
|
|
4200
|
+
for (const id of replacedIds) delete next[id];
|
|
2920
4201
|
return Object.assign(next, changed);
|
|
2921
4202
|
}
|
|
2922
4203
|
var ThreadWorkerRunner = class {
|
|
@@ -2929,23 +4210,27 @@ var ThreadWorkerRunner = class {
|
|
|
2929
4210
|
isShuttingDown = false;
|
|
2930
4211
|
get activeCount() {
|
|
2931
4212
|
let count = 0;
|
|
2932
|
-
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
|
+
}
|
|
2933
4216
|
return count;
|
|
2934
4217
|
}
|
|
2935
4218
|
run(agentName, payload) {
|
|
2936
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;
|
|
2937
4225
|
const request = {
|
|
2938
4226
|
type: "run",
|
|
2939
4227
|
requestId: this.nextRequestId++,
|
|
2940
4228
|
agentName,
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
backfill: payload.backfill,
|
|
2945
|
-
backfillCursor: payload.backfillCursor,
|
|
2946
|
-
checkpoint: payload.checkpoint,
|
|
4229
|
+
generation,
|
|
4230
|
+
pricingGenerationId: getPricingGeneration().id,
|
|
4231
|
+
operation: payload.operation,
|
|
2947
4232
|
scanOptions: payload.scanOptions,
|
|
2948
|
-
meta: payload.meta
|
|
4233
|
+
...existingSlot ? {} : { previousSessions: payload.previousSessions, meta: payload.meta }
|
|
2949
4234
|
};
|
|
2950
4235
|
return new Promise((resolve4, reject) => {
|
|
2951
4236
|
let slot = this.workers.get(agentName);
|
|
@@ -2962,6 +4247,7 @@ var ThreadWorkerRunner = class {
|
|
|
2962
4247
|
resolve: resolve4,
|
|
2963
4248
|
reject,
|
|
2964
4249
|
payload,
|
|
4250
|
+
generation,
|
|
2965
4251
|
onProgress: payload.onProgress,
|
|
2966
4252
|
onCheckpoint: payload.onCheckpoint
|
|
2967
4253
|
});
|
|
@@ -2971,10 +4257,32 @@ var ThreadWorkerRunner = class {
|
|
|
2971
4257
|
} catch (error) {
|
|
2972
4258
|
slot.pending.delete(request.requestId);
|
|
2973
4259
|
reject(toError(error));
|
|
4260
|
+
this.invalidateWorker(agentName, slot);
|
|
2974
4261
|
}
|
|
2975
4262
|
}
|
|
2976
4263
|
});
|
|
2977
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
|
+
}
|
|
2978
4286
|
async shutdown() {
|
|
2979
4287
|
this.isShuttingDown = true;
|
|
2980
4288
|
const slots = [...this.workers.values()];
|
|
@@ -2989,10 +4297,18 @@ var ThreadWorkerRunner = class {
|
|
|
2989
4297
|
}
|
|
2990
4298
|
createWorker(agentName, request) {
|
|
2991
4299
|
const worker = new Worker2(this.workerUrl, { workerData: request });
|
|
2992
|
-
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
|
+
};
|
|
2993
4308
|
worker.unref();
|
|
2994
4309
|
this.workers.set(agentName, slot);
|
|
2995
4310
|
worker.on("message", (message) => {
|
|
4311
|
+
if (appLogger.consumeWorkerMessage(message)) return;
|
|
2996
4312
|
this.handleMessage(slot, message);
|
|
2997
4313
|
});
|
|
2998
4314
|
worker.on("error", (error) => {
|
|
@@ -3011,34 +4327,59 @@ var ThreadWorkerRunner = class {
|
|
|
3011
4327
|
handleMessage(slot, message) {
|
|
3012
4328
|
const pending = slot.pending.get(message.requestId);
|
|
3013
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
|
+
}
|
|
3014
4339
|
if (message.type === "progress") {
|
|
3015
4340
|
pending.onProgress?.(message.progress);
|
|
3016
4341
|
return;
|
|
3017
4342
|
}
|
|
3018
4343
|
if (message.type === "checkpoint") {
|
|
3019
|
-
|
|
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
|
+
}
|
|
3020
4351
|
return;
|
|
3021
4352
|
}
|
|
3022
4353
|
slot.pending.delete(message.requestId);
|
|
3023
4354
|
if (message.type === "error") {
|
|
3024
4355
|
pending.reject(new Error(message.error));
|
|
4356
|
+
this.invalidateWorker(slot.agentName, slot);
|
|
3025
4357
|
return;
|
|
3026
4358
|
}
|
|
3027
4359
|
const changedIds = message.changes.map(({ session }) => session.id);
|
|
3028
4360
|
const replacedSessionIds = [...changedIds, ...message.removedSessionIds];
|
|
3029
4361
|
const removedMetaIds = [...message.removedSessionIds, ...message.removedMetaIds];
|
|
3030
4362
|
try {
|
|
3031
|
-
|
|
4363
|
+
const result = {
|
|
3032
4364
|
sessions: applySessionChanges2(
|
|
3033
4365
|
pending.payload.previousSessions,
|
|
3034
4366
|
message.changes,
|
|
3035
4367
|
message.removedSessionIds
|
|
3036
4368
|
),
|
|
3037
4369
|
meta: applyMetaChanges(pending.payload.meta, message.meta, removedMetaIds),
|
|
3038
|
-
changedIds:
|
|
3039
|
-
|
|
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);
|
|
3040
4380
|
} catch (error) {
|
|
3041
4381
|
pending.reject(toError(error));
|
|
4382
|
+
this.invalidateWorker(slot.agentName, slot);
|
|
3042
4383
|
}
|
|
3043
4384
|
}
|
|
3044
4385
|
closeWorker(agentName, slot, error) {
|
|
@@ -3047,6 +4388,15 @@ var ThreadWorkerRunner = class {
|
|
|
3047
4388
|
if (this.workers.get(agentName) === slot) this.workers.delete(agentName);
|
|
3048
4389
|
for (const pending of slot.pending.values()) pending.reject(error);
|
|
3049
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);
|
|
3050
4400
|
}
|
|
3051
4401
|
};
|
|
3052
4402
|
|
|
@@ -3169,6 +4519,7 @@ var LiveScanStore = class {
|
|
|
3169
4519
|
await this.watcher.dispose();
|
|
3170
4520
|
this.watcher = null;
|
|
3171
4521
|
}
|
|
4522
|
+
closeCacheStorage();
|
|
3172
4523
|
}
|
|
3173
4524
|
emit(event) {
|
|
3174
4525
|
if (this.shuttingDown) return;
|
|
@@ -3233,6 +4584,12 @@ function printScanResults(agents) {
|
|
|
3233
4584
|
}
|
|
3234
4585
|
|
|
3235
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
|
+
}
|
|
3236
4593
|
function buildSessionIndexOutput(snapshot, window = {}) {
|
|
3237
4594
|
const sessions = filterSessionTreeByActivityWindow(snapshot.sessions, window.from, window.to);
|
|
3238
4595
|
const agents = getAgentInfoMap(
|
|
@@ -3428,12 +4785,6 @@ var main = defineCommand({
|
|
|
3428
4785
|
const clearCache = args["clear-cache"];
|
|
3429
4786
|
const hostname = args.host;
|
|
3430
4787
|
const remoteAccess = args["remote-access"];
|
|
3431
|
-
if (!isLoopbackHostname(hostname) && !remoteAccess) {
|
|
3432
|
-
console.error(
|
|
3433
|
-
`Refusing to expose CodeSesh on ${hostname} without authentication. Add --remote-access to continue.`
|
|
3434
|
-
);
|
|
3435
|
-
process.exit(1);
|
|
3436
|
-
}
|
|
3437
4788
|
let transport;
|
|
3438
4789
|
try {
|
|
3439
4790
|
transport = resolveRemoteTransport({
|
|
@@ -3446,6 +4797,12 @@ var main = defineCommand({
|
|
|
3446
4797
|
console.error(error instanceof Error ? error.message : String(error));
|
|
3447
4798
|
process.exit(1);
|
|
3448
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
|
+
}
|
|
3449
4806
|
if (trace) {
|
|
3450
4807
|
perf.enable();
|
|
3451
4808
|
}
|
|
@@ -3459,7 +4816,7 @@ var main = defineCommand({
|
|
|
3459
4816
|
log_path: appLogger.getLogPath()
|
|
3460
4817
|
});
|
|
3461
4818
|
if (clearCache) {
|
|
3462
|
-
const { clearCache: clear } = await import("./dist-
|
|
4819
|
+
const { clearCache: clear } = await import("./dist-N2PQATP2.js");
|
|
3463
4820
|
clear();
|
|
3464
4821
|
appLogger.info("cache.clear");
|
|
3465
4822
|
console.log("Cache cleared.");
|
|
@@ -3509,6 +4866,13 @@ var main = defineCommand({
|
|
|
3509
4866
|
}
|
|
3510
4867
|
if (jsonOnly) {
|
|
3511
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
|
+
}
|
|
3512
4876
|
const output = buildSessionIndexOutput(result, { from: listDefaultFrom, to: listDefaultTo });
|
|
3513
4877
|
appLogger.info("cli.json_output", {
|
|
3514
4878
|
sessions: output.sessions.length,
|