codesesh 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +29 -5
  2. package/dist/{chunk-TIHF4T3K.js → chunk-PROMURPM.js} +80 -16
  3. package/dist/chunk-PROMURPM.js.map +1 -0
  4. package/dist/{chunk-BEF6LLRG.js → chunk-VAUC2W7I.js} +5317 -2268
  5. package/dist/chunk-VAUC2W7I.js.map +1 -0
  6. package/dist/{dist-3F467V44.js → dist-6QTGTYZT.js} +40 -4
  7. package/dist/index.js +1818 -454
  8. package/dist/index.js.map +1 -1
  9. package/dist/scan-refresh-worker.js +274 -95
  10. package/dist/scan-refresh-worker.js.map +1 -1
  11. package/dist/search-index-worker.js +112 -50
  12. package/dist/search-index-worker.js.map +1 -1
  13. package/dist/smart-tag-worker.js +5 -3
  14. package/dist/smart-tag-worker.js.map +1 -1
  15. package/dist/web/assets/ErrorBoundary-D11tWzfn.js +1 -0
  16. package/dist/web/assets/OverviewScreen-DriPmCam.js +1 -0
  17. package/dist/web/assets/Projects-BPDIcUBK.js +1 -0
  18. package/dist/web/assets/SearchFilterBar-Bx73B5e9.js +1 -0
  19. package/dist/web/assets/SearchResultsPanel-A6_3TPhW.js +1 -0
  20. package/dist/web/assets/SessionDetail-BnpvDwtH.js +54 -0
  21. package/dist/web/assets/agents-BkRP_ww3.js +1 -0
  22. package/dist/web/assets/contract-V6QuD0UY.js +1 -0
  23. package/dist/web/assets/index-99ZxpV2y.js +1410 -0
  24. package/dist/web/assets/index-B2RqUgZ5.css +1 -0
  25. package/dist/web/assets/panel-C-gc79Gz.js +1 -0
  26. package/dist/web/assets/session-indexes-0oT5HSS5.js +4 -0
  27. package/dist/web/assets/utils-DgUgu15E.js +1 -0
  28. package/dist/web/icon/agent/dsh.svg +3 -0
  29. package/dist/web/index.html +10 -32
  30. package/dist/web/theme-bootstrap.js +19 -0
  31. package/package.json +10 -10
  32. package/dist/chunk-BEF6LLRG.js.map +0 -1
  33. package/dist/chunk-TIHF4T3K.js.map +0 -1
  34. package/dist/web/assets/ErrorBoundary-BY-N1QzJ.js +0 -1
  35. package/dist/web/assets/OverviewScreen-_ZaVjhxN.js +0 -1
  36. package/dist/web/assets/Projects-q6cEm2dc.js +0 -1
  37. package/dist/web/assets/SearchFilterBar-CrVBpo40.js +0 -1
  38. package/dist/web/assets/SearchResultsPanel-Cg5V8Ia6.js +0 -1
  39. package/dist/web/assets/SessionDetail-BSSuddsZ.js +0 -54
  40. package/dist/web/assets/chunk-62JRHF6Z-BWRsEBnL.js +0 -4
  41. package/dist/web/assets/index-C3KgxGwE.js +0 -1410
  42. package/dist/web/assets/index-DQX42cFQ.css +0 -1
  43. package/dist/web/assets/panel-B7kuE60V.js +0 -1
  44. package/dist/web/assets/session-indexes-DmnjZ_JL.js +0 -1
  45. package/dist/web/assets/utils-t2u9IbNT.js +0 -1
  46. /package/dist/{dist-3F467V44.js.map → dist-6QTGTYZT.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-TIHF4T3K.js";
5
+ } from "./chunk-PROMURPM.js";
6
6
  import {
7
7
  FileSystemSessionSource,
8
8
  SessionAliasValidationError,
9
9
  StateStorageUnavailableError,
10
10
  addCalendarDays,
11
11
  attachMissingProjectIdentities,
12
- attachProjectMetrics,
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
- getAgentLastFullSyncAt,
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-BEF6LLRG.js";
68
+ } from "./chunk-VAUC2W7I.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 SEARCH_LIMIT_MAX = 100;
155
- var SEARCH_LIMIT_DEFAULT = 50;
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
- if (value == null) return fallback;
170
- const ts = new Date(value).getTime();
171
- return Number.isNaN(ts) ? fallback : ts;
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, defaults, projectIdentity) {
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: optionalQueryValue(params.get("agent") ?? void 0),
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: parseDateParam(params.get("from") ?? void 0, defaults.from),
213
- to: parseDateParam(params.get("to") ?? void 0, defaults.to),
214
- limit: limitValue && limitValue > 0 ? Math.min(limitValue, SEARCH_LIMIT_MAX) : SEARCH_LIMIT_DEFAULT
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
- return {
258
- ...bookmark,
259
- session: aliases.decorate(bookmark.session, bookmark.reference)
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 parseBookmarkPayload(value) {
462
+ function parseBookmarkReference(value) {
360
463
  if (!isRecord(value)) return null;
361
464
  const reference = parseSessionReferencePayload(value.reference);
362
- if (reference) {
363
- const session2 = parseBookmarkSession(value.session, reference);
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
- const legacyReference = normalizeSessionReference({
469
+ return normalizeSessionReference({
370
470
  agentName: value.agentKey.trim().toLowerCase(),
371
471
  sessionId: value.sessionId
372
472
  });
373
- const session = parseBookmarkSession(
374
- {
375
- ...value,
376
- id: value.sessionId,
377
- slug: value.fullPath
378
- },
379
- legacyReference
380
- );
381
- return session ? { reference: legacyReference, session } : null;
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
- if (!session.model_usage) return session;
397
- const item = { ...session };
398
- delete item.model_usage;
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(`${headerJson.slice(0, -1)},"messages":[`));
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 = iterator.next();
425
- while (!next.done) {
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 from = parseDateParam(c.req.query("from"), defaults.from);
464
- const to = parseDateParam(c.req.query("to"), defaults.to);
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 from = parseDateParam(c.req.query("from"), defaults.from);
484
- const to = parseDateParam(c.req.query("to"), defaults.to);
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 sessions = filterSessionsByActivityWindow(scanResult.sessions, from, to);
612
+ const tree = buildSessionTree(scanResult.sessions);
613
+ const sessions = filterSessionTreeByActivityWindow(scanResult.sessions, from, to, tree);
491
614
  return {
492
- projects: attachProjectMetrics(listCachedProjectGroups(sessions), sessions)
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 agent = c.req.query("agent");
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 from = parseDateParam(c.req.query("from"), defaults.from);
512
- const to = parseDateParam(c.req.query("to"), defaults.to);
513
- let sessions = [];
514
- if (agent && scanResult.byAgent[agent]) {
515
- sessions = [...scanResult.byAgent[agent]];
516
- } else {
517
- sessions = [...scanResult.sessions];
518
- }
519
- if (projectIdentity) {
520
- sessions = sessions.filter(
521
- (session) => matchesProjectIdentity(session.project_identity, projectIdentity)
522
- );
523
- } else if (cwd) {
524
- const projectScope = createProjectScopeMatcher(cwd);
525
- sessions = sessions.filter((s) => matchesProjectScope(s, projectScope));
526
- }
527
- sessions = filterSessionsByActivityWindow(sessions, from, to);
528
- if (tag) {
529
- sessions = sessions.filter((s) => s.smart_tags?.includes(tag));
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 searchOptions = parseSearchOptions(c, defaults, projectIdentity);
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 deduped = /* @__PURE__ */ new Map();
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 limitValue = Number(c.req.query("limit"));
592
- const limit = Number.isFinite(limitValue) && limitValue > 0 ? Math.min(limitValue, 200) : 50;
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: optionalQueryValue(c.req.query("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: parseDateParam(c.req.query("from"), defaults.from),
612
- to: parseDateParam(c.req.query("to"), defaults.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 result = materializeSessionDetailResponse(scanSource.getSnapshot(), {
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: message }, 500);
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 handleGetBookmarks(c) {
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: listBookmarks().map((bookmark) => decorateBookmark(bookmark, aliases)),
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 = parseBookmarkPayload(await c.req.json().catch(() => null));
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) => parseBookmarkPayload(entry)).filter((entry) => entry !== null);
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
- () => c.json({ bookmarks: importBookmarks(bookmarks), storageAvailable: true }),
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 = parseDateParam(c.req.query("to"), defaults.to);
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
- const write = (event, data) => {
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
- write("connected", { timestamp: Date.now() });
868
- write("scan-status", eventSource.getScanStatus());
869
- const unsubscribeSessions = eventSource.subscribe((event) => {
870
- write(event.type, event);
871
- });
872
- const unsubscribeScanStatus = eventSource.subscribeScanStatus((event) => {
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
- clearInterval(heartbeat);
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
- const abortStream = () => {
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) => createSseResponse(eventSource, c.req.raw.signal));
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 isIP(normalized) === 4 && normalized.startsWith("127.");
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 isLoopback = isLoopbackHostname(hostname);
1063
- const transport = options.transport ?? (isLoopback ? { kind: "loopback" } : { kind: "plaintext" });
1064
- const remoteAccessToken = !isLoopback ? options.remoteAccessToken ?? (options.remoteAccess ? createRemoteAccessToken() : null) : null;
1065
- if (!isLoopback && !remoteAccessToken) {
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 = isLoopback ? `${scheme}://localhost:${actualPort}` : `${scheme}://${hostname}:${actualPort}`;
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 (!isLoopback) {
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
- await new Promise((resolve4) => {
1181
- if (!server) {
1182
- resolve4();
1183
- return;
1184
- }
1185
- server.close(() => resolve4());
1186
- });
1187
- await store.shutdown();
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.map((agent) => [agent.name, sortSessions(snapshot.byAgent[agent.name] ?? [])])
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: changes.map(({ session }) => ({
1402
- reference: { agentName, sessionId: session.id },
1403
- session
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
- return batch instanceof PendingSearchIndexJobBatch && batch.settle(error);
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
- return {
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 = this.pendingJobs.enqueue(batchId, context, jobs);
1579
- if (this.worker) {
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.worker) return;
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
- const worker = new Worker(workerUrl, {
1641
- workerData: {
1642
- context: batch.context,
1643
- jobs: batch.jobs,
1644
- agentNames: [],
1645
- sessionsByAgent: {},
1646
- metaByAgent: {}
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: 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.settle(
1666
- batch,
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.settle(batch);
2474
+ this.finishBatch(activeBatch);
1679
2475
  });
1680
2476
  worker.on("error", (error) => {
1681
- appLogger.error("search_index.worker_error", { context: batch.context, error });
1682
- this.settle(batch, toError(error));
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, batch, code));
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
- finishWorker(worker, batch, code) {
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.id,
1689
- context: batch.context,
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 === worker) this.worker = null;
1694
- if (this.activeBatch === batch) this.activeBatch = null;
1695
- const error = code === 0 ? new Error("Search index worker exited before completing its batch") : new Error(`Search index worker exited with code ${code}`);
1696
- if (code !== 0) appLogger.warn("search_index.worker_exit", { context: batch.context, code });
1697
- this.settle(batch, error);
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)) return;
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 === "indexing" && progress.phase !== "finalizing") {
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
- indexAgent(agentName) {
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: "indexing", updatedAt: now }
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
- { ...status, status: "complete", completedAt: status.completedAt ?? now, updatedAt: now }
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(patch) {
2938
+ updateBackfill(backfill) {
1914
2939
  return this.set({
1915
2940
  ...this.status,
1916
- backfill: { ...this.status.backfill, ...patch },
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 === "indexing";
2966
+ return status === "finalizing" || status === "publish-queued" || status === "publishing";
1924
2967
  })) {
1925
- return "indexing";
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
- function buildPersistenceDiff(previousSessions, nextSessions, candidateChangedIds = []) {
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
- return { changedSessions: changes, removedSessionIds };
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
- backfillQueue = [];
1963
- currentBackfillAgent;
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 = new SearchIndexJobRunner();
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.currentBackfillAgent != null || void 0,
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.backfillQueue.length = 0;
2044
- this.currentBackfillAgent = void 0;
2045
- this.currentBackfillProgress = void 0;
2046
- this.cacheIntegrityCheckedAgents.clear();
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 (this.currentBackfillAgent === agentName) {
2080
- this.currentBackfillProgress = {
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
- this.publishBackfillStatus();
3161
+ })) {
3162
+ this.publishProgressStatus(
3163
+ key2,
3164
+ nextPhase2,
3165
+ this.scanStatus.updateBackfill(this.backfills.status())
3166
+ );
3167
+ }
2087
3168
  return;
2088
3169
  }
2089
- this.publishStatus(this.scanStatus.updateAgent(agentName, progress));
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
- beginAgentIndexing(agentName) {
2092
- this.publishStatus(this.scanStatus.indexAgent(agentName));
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.backfillStatus()));
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) listener(event);
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
- return await this.runRefresh(agentName);
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)) this.enqueueBackfill(agentName);
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 isInitialized = isAgentCacheInitialized(agentName);
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") return "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: strategyResult.fullScanSessions ?? nextSessions,
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.beginAgentIndexing(agentName);
2184
- const publication = await this.commitSessionPublication({
2185
- context: "scan.refresh",
2186
- agentName,
2187
- sessions: nextSessions,
2188
- candidateChangedIds: strategyResult.preciseChangedIds ?? [],
2189
- indexJob: persistentJob
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("scan.refresh.done", {
2196
- agent: agentName,
2197
- duration_ms: Math.round(totalDurationMs),
2198
- sessions: nextSessions.length,
2199
- new_sessions: publication.event?.newSessions ?? 0,
2200
- updated_sessions: publication.event?.updatedSessions ?? 0,
2201
- removed_sessions: publication.event?.removedSessions ?? 0,
2202
- pending_paths: pendingPathCount,
2203
- availability_ms: Math.round(availabilityDuration),
2204
- check_ms: Math.round(strategyResult.checkDuration),
2205
- scan_ms: Math.round(strategyResult.scanDuration),
2206
- diff_ms: Math.round(publication.diffDuration),
2207
- persist_ms: Math.round(persistDuration),
2208
- search_index_ms: Math.round(persistDuration),
2209
- persistent_index_worker_job: persistentJob.kind
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 result = await this.runWorker(agent, previousSessions, null, this.startupScanOptions(), {
2235
- checkpoint: true
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
- scanDuration: performance.now() - scanStartedAt
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 result = await this.runWorker(agent, cached.sessions, null, this.startupScanOptions(), {
2248
- sourceSync: true,
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
- this.logUnchangedRefresh(agent.name, refreshStartedAt);
2276
- return this.refreshStrategyResult(baseline, { status: "unchanged", checkDuration });
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, null, {}, { checkpoint: true });
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
- persistenceDiff: buildPersistenceDiff(baseline, sessions2),
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(agent.incrementalScan(baseline, preciseChangedIds, checkResult.refs))
3542
+ await Promise.resolve(
3543
+ agent.incrementalScan(baseline, preciseChangedIds, checkResult.refs, scope)
3544
+ )
2292
3545
  );
2293
- return this.refreshStrategyResult(sessions, {
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(baseline, sessions, preciseChangedIds),
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 result = await this.runWorker(agent, previousSessions, null, {}, { checkpoint: true });
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
- scanDuration: performance.now() - scanStartedAt
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, changedIds, scanOptions, workerOptions = {}) {
3592
+ runWorker(agent, previousSessions, operation, scanOptions, runOptions = {}) {
2324
3593
  return this.options.workerRunner.run(agent.name, {
2325
3594
  previousSessions,
2326
- changedIds,
3595
+ operation,
2327
3596
  scanOptions,
2328
- sourceSync: workerOptions.sourceSync,
2329
- backfill: workerOptions.backfill,
2330
- backfillCursor: workerOptions.backfillCursor,
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
- handleWorkerCheckpoint(agent, checkpoint) {
2338
- if (this.isShuttingDown) return;
2339
- if (checkpoint.stage === "scanned") {
2340
- let persisted2 = false;
2341
- try {
2342
- persisted2 = saveCachedSessions(agent.name, checkpoint.sessions, checkpoint.meta);
2343
- if (persisted2) markAgentCacheInitialized(agent.name);
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
- if (!agent.isAvailable()) return false;
2392
- const lastSyncAt = getAgentLastFullSyncAt(agent.name);
2393
- if (lastSyncAt == null || Date.now() - lastSyncAt > BACKFILL_INTERVAL_MS) return true;
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 (this.cacheIntegrityCheckedAgents.has(agent.name)) return false;
2396
- const cached = loadCachedSessions(agent.name);
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 = cached?.sessions.length ?? 0;
2399
- this.cacheIntegrityCheckedAgents.add(agent.name);
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.currentBackfillAgent === agentName || this.backfillQueue.includes(agentName)) {
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 || this.currentBackfillAgent) return;
2421
- const agentName = this.backfillQueue.shift();
2422
- if (!agentName) return;
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(agentName).then((result) => {
2427
- if (this.isShuttingDown) return;
2428
- this.currentBackfillAgent = void 0;
2429
- this.currentBackfillProgress = void 0;
2430
- if (result === "committed") {
2431
- if (!this.completedBackfillAgents.includes(agentName)) {
2432
- this.completedBackfillAgents.push(agentName);
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.failedBackfillAgents = this.failedBackfillAgents.filter(
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
- this.publishBackfillStatus();
2442
- this.pumpBackfillQueue();
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(agentName) {
2446
- return this.scheduler.run(agentName, "backfill", () => this.performBackfill(agentName));
3680
+ runBackfill(attempt) {
3681
+ return this.scheduler.run(attempt.agentName, "backfill", () => this.performBackfill(attempt));
2447
3682
  }
2448
- async performBackfill(agentName) {
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
- null,
3706
+ operation,
2464
3707
  {},
2465
3708
  {
2466
- sourceSync: agent instanceof FileSystemSessionSource,
2467
- backfill: true,
2468
- backfillCursor,
3709
+ backfillAttempt: attempt,
2469
3710
  meta,
2470
- checkpoint: true
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.currentBackfillProgress = { phase: "indexing", sessions: fullSessions.length };
2476
- this.publishBackfillStatus();
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.map((agent) => {
3768
+ return snapshot.agents.flatMap((agent) => {
3769
+ if (!(agent.name in snapshot.byAgent)) return [];
2518
3770
  const cached = loadCachedSessions(agent.name);
2519
- return cached ? {
2520
- kind: "full",
2521
- context,
2522
- agentName: agent.name,
2523
- sessions: cached.sessions,
2524
- meta: cached.meta
2525
- } : {
2526
- kind: "full",
2527
- context,
2528
- agentName: agent.name,
2529
- sessions: snapshot.byAgent[agent.name] ?? [],
2530
- meta: buildAgentCacheMeta(agent)
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
- await this.searchIndexJobs.enqueue(context, jobs);
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 replacedSessionIds) delete next[id];
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()) count += slot.pending.size;
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
- previousSessions: payload.previousSessions,
2942
- changedIds: payload.changedIds,
2943
- sourceSync: payload.sourceSync,
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 = { worker, pending: /* @__PURE__ */ new Map(), closed: false };
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
- pending.onCheckpoint?.(message.checkpoint);
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
- pending.resolve({
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: pending.payload.sourceSync ? replacedSessionIds : void 0
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-3F467V44.js");
4819
+ const { clearCache: clear } = await import("./dist-6QTGTYZT.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,