codesesh 1.0.3 → 1.0.4

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 (61) hide show
  1. package/README.md +18 -10
  2. package/dist/chunk-3SXCTDD6.js +326 -0
  3. package/dist/chunk-3SXCTDD6.js.map +1 -0
  4. package/dist/chunk-BYB77UAJ.js +6054 -0
  5. package/dist/chunk-BYB77UAJ.js.map +1 -0
  6. package/dist/{chunk-WAKW5OPS.js → chunk-G4HRD3AR.js} +13 -8
  7. package/dist/chunk-G4HRD3AR.js.map +1 -0
  8. package/dist/chunk-MBZ4VV4C.js +670 -0
  9. package/dist/chunk-MBZ4VV4C.js.map +1 -0
  10. package/dist/chunk-QH2HOXJH.js +2 -0
  11. package/dist/chunk-W4PZHL2C.js +2035 -0
  12. package/dist/chunk-W4PZHL2C.js.map +1 -0
  13. package/dist/chunk-YCUN52HA.js +8422 -0
  14. package/dist/chunk-YCUN52HA.js.map +1 -0
  15. package/dist/discovery-K3GLISI6.js +102 -0
  16. package/dist/discovery-K3GLISI6.js.map +1 -0
  17. package/dist/index.js +1899 -894
  18. package/dist/index.js.map +1 -1
  19. package/dist/project-identity-worker.js +4 -2
  20. package/dist/project-identity-worker.js.map +1 -1
  21. package/dist/scan-refresh-worker.js +117 -136
  22. package/dist/scan-refresh-worker.js.map +1 -1
  23. package/dist/search-index-worker.js +25 -9
  24. package/dist/search-index-worker.js.map +1 -1
  25. package/dist/smart-tag-worker.js +8 -5
  26. package/dist/smart-tag-worker.js.map +1 -1
  27. package/dist/web/assets/InteractiveReceipt-Dk50TBsH.js +1 -0
  28. package/dist/web/assets/OverviewScreen-5hvQVp4H.js +1 -0
  29. package/dist/web/assets/{PrismHighlighter-BZkkX0AU.js → PrismHighlighter-B1a2dPJO.js} +2 -2
  30. package/dist/web/assets/Projects-Cy_v2yep.js +1 -0
  31. package/dist/web/assets/SearchResultsPanel-DfheEwpz.js +1 -0
  32. package/dist/web/assets/SessionDetail-DRI8R3st.js +53 -0
  33. package/dist/web/assets/api-GP9UCedC.js +1 -0
  34. package/dist/web/assets/contract-xJyW5u7a.js +2 -0
  35. package/dist/web/assets/{index-3UfOiTBO.css → index-Bd-YkWkZ.css} +1 -1
  36. package/dist/web/assets/index-DEjvzumm.js +1410 -0
  37. package/dist/web/assets/panel-ND0J63d3.js +1 -0
  38. package/dist/web/assets/{session-indexes-BPhf1X2P.js → session-indexes-BsgjUoc1.js} +2 -2
  39. package/dist/web/assets/utils-C7BqyxxV.js +1 -0
  40. package/dist/web/index.html +7 -11
  41. package/package.json +4 -10
  42. package/dist/chunk-PSHZZITB.js +0 -17307
  43. package/dist/chunk-PSHZZITB.js.map +0 -1
  44. package/dist/chunk-WAKW5OPS.js.map +0 -1
  45. package/dist/dist-RHDY2EZX.js +0 -173
  46. package/dist/web/assets/ErrorBoundary-DIwwrdlw.js +0 -1
  47. package/dist/web/assets/InteractiveReceipt-DmNBDVms.js +0 -1
  48. package/dist/web/assets/OverviewScreen-B5JekBLC.js +0 -1
  49. package/dist/web/assets/Projects-Bj6ma2k2.js +0 -1
  50. package/dist/web/assets/SearchFilterBar-B9VrUbym.js +0 -1
  51. package/dist/web/assets/SearchResultsPanel-C1cuuOKq.js +0 -1
  52. package/dist/web/assets/SessionDetail-Dc2CQ3hH.js +0 -54
  53. package/dist/web/assets/SmartTagChips-ux6idUGJ.js +0 -1
  54. package/dist/web/assets/agents-BkRP_ww3.js +0 -1
  55. package/dist/web/assets/code-metrics-UYFsCC7E.js +0 -1
  56. package/dist/web/assets/contract-DXvltqJG.js +0 -1
  57. package/dist/web/assets/index-DBMiEd61.js +0 -1410
  58. package/dist/web/assets/panel-CLnL4kMN.js +0 -1
  59. package/dist/web/assets/session-title-DdtlxtlR.js +0 -1
  60. package/dist/web/assets/utils-BKqufGdv.js +0 -1
  61. /package/dist/{dist-RHDY2EZX.js.map → chunk-QH2HOXJH.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,79 +1,94 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ attachProjectMetricsFromTree,
4
+ buildDashboard,
5
+ getSessionActivityTime,
6
+ summarizeProjects
7
+ } from "./chunk-MBZ4VV4C.js";
2
8
  import {
3
9
  AGENT_UNAVAILABLE_DURING_SCAN_ERROR_CODE,
4
10
  AgentUnavailableDuringScanError
5
11
  } from "./chunk-RF3EEWPH.js";
12
+ import "./chunk-QH2HOXJH.js";
6
13
  import {
7
14
  appLogger,
8
15
  logSearchIndexSync
9
- } from "./chunk-WAKW5OPS.js";
16
+ } from "./chunk-G4HRD3AR.js";
10
17
  import {
11
- FileSystemSessionSource,
12
- PROJECT_IDENTITY_RESOLVER_REVISION,
13
- SessionAliasValidationError,
14
- StateStorageUnavailableError,
15
- addCalendarDays,
16
18
  attachMissingProjectIdentities,
17
- attachProjectMetricsFromTree,
18
19
  buildAgentCacheMeta,
19
- buildDashboard,
20
- buildSessionTree,
20
+ buildSessionPersistenceDiff,
21
21
  closeCacheStorage,
22
+ commitAgentRefreshCheck,
22
23
  computeSessionDiff,
23
- countCalendarDays,
24
- createProjectScopeMatcherFromIdentity,
25
- createRegisteredAgents,
26
- createSessionIndex,
27
- createSessionProjectionContext,
28
- deleteBookmark,
29
- deleteSessionAlias,
30
- executeSessionSearch,
31
- filterSessionSearchCandidates,
32
- filterSessionTreeByActivityWindow,
33
- formatSessionReference,
24
+ filterIndexedSessionReferences,
34
25
  getAgentFullSyncCursor,
35
- getAgentInfoMap,
36
26
  getAnalyticsRevision,
37
- getPricingGeneration,
38
27
  getSearchProjectDirectory,
39
- getSessionActivityTime,
40
- getSessionAgentKey,
41
- importBookmarks,
42
- isProjectIdentityKind,
43
- listBookmarks,
44
28
  listCachedProjectGroups,
45
29
  listDashboardCostFacts,
46
30
  listFileActivity,
47
- listSessionAliases,
48
31
  loadCachedSessionHeads,
49
- loadCachedSessions,
50
32
  markAgentFullSyncCompleted,
51
33
  markAgentFullSyncProgress,
52
34
  markAgentFullSyncStarted,
53
- matchesProjectIdentity,
54
- matchesProjectScope,
55
- materializeBookmarkViews,
56
35
  materializeSessionDetailResponse,
57
36
  mergeSearchQueryOptions,
37
+ readAgentCacheInitialization,
38
+ readAgentLastFullSyncAt,
39
+ readCachedSessions,
40
+ readPendingSearchIndexMaintenance,
41
+ resolveSessionSnapshotCompleteness,
42
+ scanSessions,
43
+ searchFileActivitySessions,
44
+ searchSessions,
45
+ selectAgentRefresh,
46
+ sessionMatchesSearchCost,
47
+ sessionSignature,
48
+ sortSessions
49
+ } from "./chunk-BYB77UAJ.js";
50
+ import {
51
+ PROJECT_IDENTITY_RESOLVER_REVISION,
52
+ createProjectScopeMatcherFromIdentity,
53
+ matchesProjectScope,
54
+ normalizeProjectDirectory
55
+ } from "./chunk-3SXCTDD6.js";
56
+ import {
57
+ createRegisteredAgents,
58
+ getAgentInfoMap
59
+ } from "./chunk-YCUN52HA.js";
60
+ import {
61
+ addCalendarDays,
62
+ assertIdentifiedSessionHead,
63
+ assertSessionIdentity,
64
+ buildSessionTree,
65
+ columnExists,
66
+ countCalendarDays,
67
+ createSessionIndex,
68
+ createSessionProjectionContext,
69
+ filterSessionTreeByActivityWindow,
70
+ formatSessionReference,
71
+ getPricingGeneration,
72
+ getSessionReferenceKey,
73
+ getUserVersion,
74
+ isProjectIdentityKind,
75
+ isSmartTag,
76
+ matchesProjectIdentity,
58
77
  mergeSessionsUpdatedEvents,
59
78
  mergeSortedSessions,
60
- normalizeProjectDirectory,
61
79
  normalizeSessionReference,
80
+ openDb,
62
81
  perf,
63
82
  publishPendingPricing,
64
- readAgentCacheInitialization,
65
- readAgentLastFullSyncAt,
66
- readCachedSessions,
67
- readPendingSearchIndexMaintenance,
68
83
  refreshPricingCache,
69
- scanSessions,
84
+ runSchemaMigrations,
70
85
  sessionRoutePath,
71
- sessionSignature,
72
- sortSessions,
86
+ setUserVersion,
73
87
  startOfCalendarDay,
74
- upsertBookmark,
75
- upsertSessionAlias
76
- } from "./chunk-PSHZZITB.js";
88
+ tableExists,
89
+ toPublicReferencedSessionHead,
90
+ toPublicSessionHead
91
+ } from "./chunk-W4PZHL2C.js";
77
92
 
78
93
  // src/index.ts
79
94
  import { defineCommand, runMain } from "citty";
@@ -93,6 +108,422 @@ import { fileURLToPath } from "url";
93
108
  // src/api/routes.ts
94
109
  import { Hono } from "hono";
95
110
 
111
+ // ../core/dist/runtime/search.mjs
112
+ function executeSessionSearch(query, options, snapshot, context = {}) {
113
+ const merged = mergeSearchQueryOptions(query, options);
114
+ if (!needsIndexedSearch(merged.text, merged.options)) {
115
+ return searchRecentSessions(snapshot, merged.options, context);
116
+ }
117
+ return searchIndexedSessions(query, merged.text, merged.parsed, merged.options);
118
+ }
119
+ function needsIndexedSearch(textQuery, options) {
120
+ return Boolean(textQuery || options.file || options.fileKind || options.tools?.length);
121
+ }
122
+ function matchesRecentSearchFilters(session, options, projectScope, inclusiveCost) {
123
+ if (options.projectKind || options.projectKey) {
124
+ if (!options.projectKind || !options.projectKey || !matchesProjectIdentity(session.project_identity, {
125
+ kind: options.projectKind,
126
+ key: options.projectKey
127
+ })) {
128
+ return false;
129
+ }
130
+ }
131
+ if (projectScope && !matchesProjectScope(session, projectScope)) return false;
132
+ if (options.project) {
133
+ const projectNeedle = options.project.toLowerCase();
134
+ const projectText = [
135
+ session.project_identity?.key,
136
+ session.project_identity?.displayName,
137
+ session.directory
138
+ ].filter(Boolean).join("\n").toLowerCase();
139
+ if (!projectText.includes(projectNeedle)) return false;
140
+ }
141
+ if (options.tags?.length && !options.tags.every((tag) => session.smart_tags?.includes(tag))) {
142
+ return false;
143
+ }
144
+ if (!sessionMatchesSearchCost(session, options, inclusiveCost)) return false;
145
+ return true;
146
+ }
147
+ function matchesSessionSearchFilters(agentName, session, options, projectScope = null, inclusiveCost = session.stats.total_cost) {
148
+ if (options.agent && agentName !== options.agent) return false;
149
+ const activity = getSessionActivityTime(session);
150
+ if (options.from != null && activity < options.from) return false;
151
+ if (options.to != null && activity > options.to) return false;
152
+ return matchesRecentSearchFilters(session, options, projectScope, inclusiveCost);
153
+ }
154
+ function filterSessionSearchCandidates(candidates, options, context = {}) {
155
+ const projectScope = options.projectScope ?? null;
156
+ const sessionSnapshot = context.sessionSnapshot ?? candidates.map((candidate) => candidate.session);
157
+ const inclusiveCosts = buildInclusiveCostLookup(sessionSnapshot, options, context.sessionTree);
158
+ const headMatches = candidates.filter(
159
+ (candidate) => matchesSessionSearchFilters(
160
+ candidate.reference.agentName,
161
+ candidate.session,
162
+ options,
163
+ projectScope,
164
+ inclusiveCostFor(candidate.reference.agentName, candidate.session, inclusiveCosts)
165
+ )
166
+ );
167
+ if (!options.file && !options.fileKind && !options.tools?.length) return headMatches;
168
+ const indexedMatches = filterIndexedSessionReferences(
169
+ headMatches.map((candidate) => ({
170
+ agentName: candidate.reference.agentName,
171
+ sessionId: candidate.reference.sessionId
172
+ })),
173
+ {
174
+ file: options.file,
175
+ fileKind: options.fileKind,
176
+ tools: options.tools
177
+ }
178
+ );
179
+ return headMatches.filter(
180
+ (candidate) => indexedMatches.has(getSessionReferenceKey(candidate.reference))
181
+ );
182
+ }
183
+ function searchRecentSessions(snapshot, options, context) {
184
+ const limit = Math.max(0, Math.trunc(options.limit ?? 50));
185
+ if (limit === 0) return [];
186
+ const projectScope = options.projectScope ?? null;
187
+ const inclusiveCosts = buildInclusiveCostLookup(snapshot.sessions, options, context.sessionTree);
188
+ const sessions = options.agent ? snapshot.byAgent[options.agent] ?? [] : snapshot.sessions;
189
+ const results = [];
190
+ for (const session of sessions) {
191
+ const agentName = options.agent ?? session.reference.agentName;
192
+ if (!matchesSessionSearchFilters(
193
+ agentName,
194
+ session,
195
+ options,
196
+ projectScope,
197
+ inclusiveCostFor(agentName, session, inclusiveCosts)
198
+ )) {
199
+ continue;
200
+ }
201
+ results.push({
202
+ reference: session.reference,
203
+ session,
204
+ snippet: `Recent session \xB7 ${session.directory}`,
205
+ snippetHighlights: [],
206
+ matchType: "recent"
207
+ });
208
+ if (results.length >= limit) break;
209
+ }
210
+ return results;
211
+ }
212
+ function buildInclusiveCostLookup(sessions, options, sessionTree) {
213
+ if (options.costMin == null && options.costMax == null) return null;
214
+ return (sessionTree ?? buildSessionTree(sessions)).byRouteKey;
215
+ }
216
+ function inclusiveCostFor(agentName, session, lookup) {
217
+ return lookup?.get(getSessionReferenceKey({ agentName, sessionId: session.reference.sessionId }))?.inclusiveStats.cost ?? session.stats.total_cost;
218
+ }
219
+ function deriveFileQuery(query, parsed, options) {
220
+ return options.file ?? (!parsed.text ? parsed.filters.file : void 0) ?? (!parsed.hasQualifiers && query ? parsed.text || query : "");
221
+ }
222
+ function mergeSearchResultSources(results, limit) {
223
+ const seen = /* @__PURE__ */ new Set();
224
+ const merged = [];
225
+ for (const result of results) {
226
+ const key = getSessionReferenceKey(result.reference);
227
+ if (seen.has(key)) continue;
228
+ seen.add(key);
229
+ merged.push(result);
230
+ if (merged.length >= limit) break;
231
+ }
232
+ return merged;
233
+ }
234
+ function canSkipSessionsSearch(fileQuery, textQuery) {
235
+ return Boolean(fileQuery && !textQuery);
236
+ }
237
+ function searchIndexedSessions(query, textQuery, parsed, options) {
238
+ const fileQuery = deriveFileQuery(query, parsed, options);
239
+ const fileResults = fileQuery ? searchFileActivitySessions(fileQuery, options) : [];
240
+ const sessionResults = canSkipSessionsSearch(fileQuery, textQuery) ? [] : searchSessions(query, options);
241
+ const textMatchReferences = textQuery && options.file ? new Set(sessionResults.map((result) => getSessionReferenceKey(result.reference))) : null;
242
+ const matchingFileResults = textMatchReferences ? fileResults.filter(
243
+ (result) => textMatchReferences.has(getSessionReferenceKey(result.reference))
244
+ ) : fileResults;
245
+ return mergeSearchResultSources([...matchingFileResults, ...sessionResults], options.limit ?? 50);
246
+ }
247
+
248
+ // src/project-identity-resolver.ts
249
+ import { Worker } from "worker_threads";
250
+ var PROJECT_IDENTITY_RESOLVER_MAX_WORKERS = 2;
251
+ var PROJECT_IDENTITY_RESOLVER_MAX_QUEUED_REQUESTS = 8;
252
+ var PROJECT_IDENTITY_RESOLVER_TIMEOUT_MS = 2e3;
253
+ var SHUTDOWN_ERROR_MESSAGE = "Project identity resolver shut down";
254
+ var ProjectIdentityQueueFullError = class extends Error {
255
+ constructor() {
256
+ super("Project identity resolver queue is full");
257
+ this.name = "ProjectIdentityQueueFullError";
258
+ }
259
+ };
260
+ var ProjectIdentityTimeoutError = class extends Error {
261
+ constructor() {
262
+ super("Project identity resolution timed out");
263
+ this.name = "ProjectIdentityTimeoutError";
264
+ }
265
+ };
266
+ var ProjectIdentityRequestAbortedError = class extends Error {
267
+ constructor() {
268
+ super("Project identity resolution was aborted");
269
+ this.name = "ProjectIdentityRequestAbortedError";
270
+ }
271
+ };
272
+ function toError(error) {
273
+ return error instanceof Error ? error : new Error(String(error));
274
+ }
275
+ function requireSafeInteger(value, minimum, name) {
276
+ if (!Number.isSafeInteger(value) || value < minimum) {
277
+ throw new RangeError(`${name} must be a safe integer greater than or equal to ${minimum}`);
278
+ }
279
+ return value;
280
+ }
281
+ function isWorkerMessage(message) {
282
+ return typeof message === "object" && message !== null && "type" in message && (message.type === "resolved" && "requestId" in message && "projection" in message || message.type === "failed" && "requestId" in message && "error" in message);
283
+ }
284
+ var ThreadProjectIdentityResolver = class {
285
+ constructor(workerUrl, options = {}) {
286
+ this.workerUrl = workerUrl;
287
+ this.maxWorkers = requireSafeInteger(
288
+ options.maxWorkers ?? PROJECT_IDENTITY_RESOLVER_MAX_WORKERS,
289
+ 1,
290
+ "maxWorkers"
291
+ );
292
+ const maxQueuedRequests = requireSafeInteger(
293
+ options.maxQueuedRequests ?? PROJECT_IDENTITY_RESOLVER_MAX_QUEUED_REQUESTS,
294
+ 0,
295
+ "maxQueuedRequests"
296
+ );
297
+ this.timeoutMs = requireSafeInteger(
298
+ options.timeoutMs ?? PROJECT_IDENTITY_RESOLVER_TIMEOUT_MS,
299
+ 1,
300
+ "timeoutMs"
301
+ );
302
+ this.maxOutstandingRequests = requireSafeInteger(
303
+ this.maxWorkers + maxQueuedRequests,
304
+ this.maxWorkers,
305
+ "request capacity"
306
+ );
307
+ }
308
+ workerUrl;
309
+ workers = /* @__PURE__ */ new Set();
310
+ queue = [];
311
+ requestsByDirectory = /* @__PURE__ */ new Map();
312
+ maxWorkers;
313
+ maxOutstandingRequests;
314
+ timeoutMs;
315
+ nextRequestId = 1;
316
+ isShuttingDown = false;
317
+ resolve(cwd, signal) {
318
+ if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE));
319
+ if (signal?.aborted) return Promise.reject(new ProjectIdentityRequestAbortedError());
320
+ const normalizedCwd = normalizeProjectDirectory(cwd);
321
+ let request = this.requestsByDirectory.get(normalizedCwd);
322
+ if (!request) {
323
+ if (this.requestsByDirectory.size >= this.maxOutstandingRequests) {
324
+ appLogger.warn("project_identity.queue.full", {
325
+ active_workers: this.activeWorkerCount(),
326
+ queued_requests: this.queue.length,
327
+ request_capacity: this.maxOutstandingRequests
328
+ });
329
+ return Promise.reject(new ProjectIdentityQueueFullError());
330
+ }
331
+ request = {
332
+ requestId: this.nextRequestId++,
333
+ cwd: normalizedCwd,
334
+ subscribers: /* @__PURE__ */ new Set(),
335
+ timeout: null
336
+ };
337
+ this.requestsByDirectory.set(normalizedCwd, request);
338
+ this.queue.push(request);
339
+ }
340
+ const resolution = this.subscribe(request, signal);
341
+ this.pump();
342
+ return resolution;
343
+ }
344
+ async shutdown() {
345
+ if (this.isShuttingDown) return;
346
+ this.isShuttingDown = true;
347
+ const error = new Error(SHUTDOWN_ERROR_MESSAGE);
348
+ while (this.queue.length > 0) this.rejectRequest(this.queue[0], error);
349
+ const retirements = [...this.workers].map((slot) => this.retireWorker(slot, error));
350
+ await Promise.allSettled(retirements);
351
+ this.workers.clear();
352
+ }
353
+ subscribe(request, signal) {
354
+ return new Promise((resolve4, reject) => {
355
+ const subscriber = { resolve: resolve4, reject, signal };
356
+ request.subscribers.add(subscriber);
357
+ if (!signal) return;
358
+ subscriber.abortHandler = () => this.abortSubscriber(request, subscriber);
359
+ signal.addEventListener("abort", subscriber.abortHandler, { once: true });
360
+ });
361
+ }
362
+ abortSubscriber(request, subscriber) {
363
+ if (!request.subscribers.delete(subscriber)) return;
364
+ this.removeAbortHandler(subscriber);
365
+ subscriber.reject(new ProjectIdentityRequestAbortedError());
366
+ if (request.subscribers.size > 0) return;
367
+ const slot = this.workerForRequest(request);
368
+ if (slot) slot.pending = null;
369
+ this.releaseRequest(request);
370
+ if (slot) void this.retireWorker(slot);
371
+ else this.pump();
372
+ }
373
+ pump() {
374
+ while (!this.isShuttingDown && this.queue.length > 0) {
375
+ const slot = this.idleWorker() ?? this.createWorker();
376
+ if (!slot) return;
377
+ const request = this.queue.shift();
378
+ if (!request) return;
379
+ slot.pending = request;
380
+ request.timeout = setTimeout(() => {
381
+ if (slot.pending !== request || slot.closed) return;
382
+ appLogger.warn("project_identity.request.timeout", {
383
+ request_id: request.requestId,
384
+ timeout_ms: this.timeoutMs
385
+ });
386
+ void this.retireWorker(slot, new ProjectIdentityTimeoutError());
387
+ }, this.timeoutMs);
388
+ request.timeout.unref();
389
+ try {
390
+ slot.worker.postMessage({
391
+ type: "resolve",
392
+ requestId: request.requestId,
393
+ cwd: request.cwd
394
+ });
395
+ } catch (error) {
396
+ void this.retireWorker(slot, toError(error));
397
+ }
398
+ }
399
+ }
400
+ idleWorker() {
401
+ for (const slot of this.workers) {
402
+ if (!slot.closed && slot.pending == null) return slot;
403
+ }
404
+ return null;
405
+ }
406
+ activeWorkerCount() {
407
+ let count = 0;
408
+ for (const slot of this.workers) {
409
+ if (!slot.closed && slot.pending != null) count += 1;
410
+ }
411
+ return count;
412
+ }
413
+ createWorker() {
414
+ if (this.workers.size >= this.maxWorkers) return null;
415
+ let worker;
416
+ try {
417
+ worker = new Worker(this.workerUrl);
418
+ } catch (error) {
419
+ this.rejectQueued(toError(error));
420
+ return null;
421
+ }
422
+ const slot = {
423
+ worker,
424
+ pending: null,
425
+ closed: false,
426
+ retirement: null
427
+ };
428
+ this.workers.add(slot);
429
+ worker.on("message", (message) => {
430
+ if (appLogger.consumeWorkerMessage(message)) return;
431
+ this.handleWorkerMessage(slot, message);
432
+ });
433
+ worker.on("error", (error) => {
434
+ void this.retireWorker(slot, toError(error));
435
+ });
436
+ worker.on("exit", (code) => {
437
+ if (!slot.closed) {
438
+ this.closeExitedWorker(slot, new Error(`Project identity worker exited with code ${code}`));
439
+ }
440
+ });
441
+ return slot;
442
+ }
443
+ handleWorkerMessage(slot, message) {
444
+ if (!isWorkerMessage(message) || slot.closed) return;
445
+ const request = slot.pending;
446
+ if (!request || message.requestId !== request.requestId) return;
447
+ slot.pending = null;
448
+ if (message.type === "resolved") this.resolveRequest(request, message.projection);
449
+ else this.rejectRequest(request, new Error(message.error));
450
+ this.pump();
451
+ }
452
+ closeExitedWorker(slot, error) {
453
+ slot.closed = true;
454
+ this.workers.delete(slot);
455
+ const request = slot.pending;
456
+ slot.pending = null;
457
+ if (request) this.rejectRequest(request, error);
458
+ if (!this.isShuttingDown) this.pump();
459
+ }
460
+ retireWorker(slot, error) {
461
+ if (slot.retirement) return slot.retirement;
462
+ slot.closed = true;
463
+ const request = slot.pending;
464
+ slot.pending = null;
465
+ if (request && error) this.rejectRequest(request, error);
466
+ const retirement = (async () => {
467
+ try {
468
+ await slot.worker.terminate();
469
+ } catch (terminationError) {
470
+ appLogger.warn("project_identity.worker.termination_failed", {
471
+ error: toError(terminationError).message
472
+ });
473
+ } finally {
474
+ this.workers.delete(slot);
475
+ if (!this.isShuttingDown) this.pump();
476
+ }
477
+ })();
478
+ slot.retirement = retirement;
479
+ return retirement;
480
+ }
481
+ workerForRequest(request) {
482
+ for (const slot of this.workers) {
483
+ if (slot.pending === request) return slot;
484
+ }
485
+ return null;
486
+ }
487
+ resolveRequest(request, projection) {
488
+ this.releaseRequest(request);
489
+ for (const subscriber of request.subscribers) {
490
+ this.removeAbortHandler(subscriber);
491
+ subscriber.resolve(projection);
492
+ }
493
+ request.subscribers.clear();
494
+ }
495
+ rejectRequest(request, error) {
496
+ this.releaseRequest(request);
497
+ for (const subscriber of request.subscribers) {
498
+ this.removeAbortHandler(subscriber);
499
+ subscriber.reject(error);
500
+ }
501
+ request.subscribers.clear();
502
+ }
503
+ releaseRequest(request) {
504
+ if (request.timeout) clearTimeout(request.timeout);
505
+ request.timeout = null;
506
+ if (this.requestsByDirectory.get(request.cwd) === request) {
507
+ this.requestsByDirectory.delete(request.cwd);
508
+ }
509
+ const queueIndex = this.queue.indexOf(request);
510
+ if (queueIndex >= 0) this.queue.splice(queueIndex, 1);
511
+ }
512
+ removeAbortHandler(subscriber) {
513
+ if (subscriber.signal && subscriber.abortHandler) {
514
+ subscriber.signal.removeEventListener("abort", subscriber.abortHandler);
515
+ }
516
+ }
517
+ rejectQueued(error) {
518
+ while (this.queue.length > 0) this.rejectRequest(this.queue[0], error);
519
+ }
520
+ };
521
+ function createProjectIdentityResolver() {
522
+ return new ThreadProjectIdentityResolver(
523
+ new URL("./project-identity-worker.js", import.meta.url)
524
+ );
525
+ }
526
+
96
527
  // src/time-window-resolution.ts
97
528
  var ROLLING_DAY_MS = 24 * 60 * 60 * 1e3;
98
529
  var DEFAULT_DASHBOARD_DAYS = 30;
@@ -112,22 +543,20 @@ function resolveCliWindow(request) {
112
543
  }
113
544
  function resolveDashboardWindow(request) {
114
545
  const now = request.now ?? Date.now();
115
- const defaults = request.defaults ?? {};
116
- const to = parseOptionalDate(request.query.to) ?? defaults.to ?? now;
117
- const hasQueryDays = Boolean(request.query.days?.trim());
118
- const parsedDays = hasQueryDays ? parseDays(request.query.days) : void 0;
119
- let days = parsedDays != null && parsedDays > 0 ? parsedDays : defaults.days;
120
- const queryFrom = parseOptionalDate(request.query.from);
121
- if (queryFrom != null) {
122
- days ??= countCalendarDays(queryFrom, to);
123
- return { from: queryFrom, to, days };
124
- }
125
- if (parsedDays === 0 || !hasQueryDays && defaults.days === 0) {
546
+ const to = request.window.to ?? now;
547
+ const hasQueryDays = Boolean(request.days?.trim());
548
+ const parsedDays = hasQueryDays ? parseDays(request.days) : void 0;
549
+ let days = parsedDays != null && parsedDays > 0 ? parsedDays : request.defaultDays;
550
+ if (request.window.hasExplicitFrom && request.window.from != null) {
551
+ days ??= countCalendarDays(request.window.from, to);
552
+ return { from: request.window.from, to, days };
553
+ }
554
+ if (parsedDays === 0 || !hasQueryDays && request.defaultDays === 0) {
126
555
  return { to, days: 0 };
127
556
  }
128
- if (defaults.from != null) {
129
- days ??= countCalendarDays(defaults.from, to);
130
- return { from: defaults.from, to, days };
557
+ if (request.window.from != null) {
558
+ days ??= countCalendarDays(request.window.from, to);
559
+ return { from: request.window.from, to, days };
131
560
  }
132
561
  const resolvedDays = days != null && days > 0 ? days : DEFAULT_DASHBOARD_DAYS;
133
562
  return {
@@ -142,11 +571,6 @@ function parseRequiredDate(value) {
142
571
  if (Number.isNaN(timestamp)) throw new Error(`Invalid date: ${value}`);
143
572
  return timestamp;
144
573
  }
145
- function parseOptionalDate(value) {
146
- if (value == null) return void 0;
147
- const timestamp = new Date(value).getTime();
148
- return Number.isNaN(timestamp) ? void 0 : timestamp;
149
- }
150
574
  function parseDays(value) {
151
575
  if (value == null) return void 0;
152
576
  const days = Number.parseInt(value, 10);
@@ -154,19 +578,9 @@ function parseDays(value) {
154
578
  }
155
579
 
156
580
  // src/api/query-params.ts
157
- var SMART_TAGS = [
158
- "bugfix",
159
- "refactoring",
160
- "feature-dev",
161
- "testing",
162
- "docs",
163
- "git-ops",
164
- "build-deploy",
165
- "exploration",
166
- "planning"
167
- ];
168
581
  var SEARCH_LIMIT_POLICY = { defaultValue: 50, maxValue: 100 };
169
582
  var FILE_ACTIVITY_LIMIT_POLICY = { defaultValue: 50, maxValue: 200 };
583
+ var PROJECT_PAGE_LIMIT_POLICY = { defaultValue: 100, maxValue: 250 };
170
584
  var SESSION_PAGE_LIMIT_POLICY = { defaultValue: 250, maxValue: 500 };
171
585
  function searchParams(c) {
172
586
  return new URL(c.req.url ?? "http://localhost/", "http://localhost/").searchParams;
@@ -194,7 +608,12 @@ function parseDateWindow(params, defaults) {
194
608
  if (from.value != null && to.value != null && from.value > to.value) {
195
609
  return { kind: "invalid", parameter: "from", error: "must not be after to" };
196
610
  }
197
- return { kind: "valid", from: from.value, to: to.value };
611
+ return {
612
+ kind: "valid",
613
+ from: from.value,
614
+ to: to.value,
615
+ hasExplicitFrom: from.kind === "valid"
616
+ };
198
617
  }
199
618
  function parseNumberParam(value) {
200
619
  if (value == null || !value.trim()) return void 0;
@@ -227,7 +646,7 @@ function parseSessionQuery(params, knownAgentNames, limitPolicy) {
227
646
  return limitPolicy ? { agent, limit: parseLimit(params.get("limit"), limitPolicy) } : { agent };
228
647
  }
229
648
  function parseSmartTags(values) {
230
- const tags = values.map((value) => value.toLowerCase()).filter((value) => SMART_TAGS.includes(value));
649
+ const tags = values.map((value) => value.toLowerCase()).filter(isSmartTag);
231
650
  return tags.length > 0 ? [...new Set(tags)] : void 0;
232
651
  }
233
652
  function parseFileActivityKind(value) {
@@ -268,7 +687,7 @@ function filterSessionsByActivityWindow(sessions, from, to) {
268
687
  return filterSessionTreeByActivityWindow(sessions, from, to);
269
688
  }
270
689
 
271
- // src/api/session-pagination.ts
690
+ // src/api/snapshot-pagination.ts
272
691
  import { createHash, randomUUID } from "crypto";
273
692
  var identityVersions = /* @__PURE__ */ new WeakMap();
274
693
  function identityVersion(identity) {
@@ -302,7 +721,7 @@ function decodeCursor(cursor) {
302
721
  return null;
303
722
  }
304
723
  }
305
- function paginateSessionSnapshot(items, request) {
724
+ function paginateSnapshot(items, request) {
306
725
  const snapshot = identityVersion(request.snapshotIdentity);
307
726
  const view = identityVersion(request.viewIdentity);
308
727
  const query = queryFingerprint(request.query);
@@ -563,24 +982,412 @@ function mergeAliasSearchResults(rankedResults, aliasResults, limit) {
563
982
  ].slice(0, limit);
564
983
  }
565
984
 
566
- // src/api/session-aliases-view.ts
567
- function aliasKey(reference) {
568
- return `${reference.agentName.toLowerCase()}\0${reference.sessionId}`;
985
+ // ../core/dist/runtime/state.mjs
986
+ import { homedir, platform } from "os";
987
+ import { join } from "path";
988
+ var STATE_DB_FILENAME = "state.db";
989
+ var STATE_SCHEMA_VERSION = 3;
990
+ var MEMORY_STATE_STORE = "memory";
991
+ var stateSchemaEnsuredPath = null;
992
+ function getStateSchemaEnsuredPath() {
993
+ return stateSchemaEnsuredPath;
994
+ }
995
+ function setStateSchemaEnsuredPath(path) {
996
+ stateSchemaEnsuredPath = path;
997
+ }
998
+ var StateStorageUnavailableError = class extends Error {
999
+ constructor() {
1000
+ super("SQLite state database is unavailable");
1001
+ this.name = "StateStorageUnavailableError";
1002
+ }
1003
+ };
1004
+ function getStateDir() {
1005
+ if (process.env.CODESESH_STATE_DIR) return process.env.CODESESH_STATE_DIR;
1006
+ const currentPlatform = platform();
1007
+ if (currentPlatform === "darwin") {
1008
+ return join(homedir(), "Library", "Application Support", "codesesh");
1009
+ }
1010
+ if (currentPlatform === "win32") {
1011
+ const appData = process.env.APPDATA ?? process.env.LOCALAPPDATA;
1012
+ return join(appData ?? join(homedir(), "AppData", "Roaming"), "codesesh");
1013
+ }
1014
+ return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "codesesh");
1015
+ }
1016
+ function getStateDbPath() {
1017
+ return join(getStateDir(), STATE_DB_FILENAME);
1018
+ }
1019
+ function useMemoryStateStore() {
1020
+ return process.env.CODESESH_STATE_STORE === MEMORY_STATE_STORE;
1021
+ }
1022
+ function createBookmarksTable(db) {
1023
+ db.exec(`
1024
+ CREATE TABLE IF NOT EXISTS bookmarks (
1025
+ agent_name TEXT NOT NULL,
1026
+ session_id TEXT NOT NULL,
1027
+ bookmarked_at INTEGER NOT NULL,
1028
+ PRIMARY KEY (agent_name, session_id)
1029
+ );
1030
+ `);
1031
+ }
1032
+ function migrateBookmarkFacts(db) {
1033
+ db.exec("ALTER TABLE bookmarks RENAME TO bookmarks_with_snapshots");
1034
+ createBookmarksTable(db);
1035
+ db.exec(`
1036
+ INSERT INTO bookmarks(agent_name, session_id, bookmarked_at)
1037
+ SELECT agent_name, session_id, bookmarked_at
1038
+ FROM bookmarks_with_snapshots;
1039
+
1040
+ DROP TABLE bookmarks_with_snapshots;
1041
+ `);
1042
+ }
1043
+ function createSessionAliasesTable(db) {
1044
+ db.exec(`
1045
+ CREATE TABLE IF NOT EXISTS session_aliases (
1046
+ agent_name TEXT NOT NULL,
1047
+ session_id TEXT NOT NULL,
1048
+ alias TEXT NOT NULL,
1049
+ updated_at INTEGER NOT NULL,
1050
+ PRIMARY KEY (agent_name, session_id)
1051
+ );
1052
+ `);
1053
+ }
1054
+ function createStateSchema(db) {
1055
+ db.exec(`
1056
+ CREATE TABLE IF NOT EXISTS state_meta (
1057
+ key TEXT PRIMARY KEY,
1058
+ value TEXT NOT NULL
1059
+ );
1060
+ `);
1061
+ createBookmarksTable(db);
1062
+ createSessionAliasesTable(db);
1063
+ }
1064
+ function readLegacyStateVersion(db) {
1065
+ if (!tableExists(db, "state_meta") || !columnExists(db, "state_meta", "key") || !columnExists(db, "state_meta", "value")) {
1066
+ return 0;
1067
+ }
1068
+ const row = db.prepare("SELECT value FROM state_meta WHERE key = 'version'").get();
1069
+ return Number(row?.value ?? 0);
1070
+ }
1071
+ function getCurrentStateSchemaVersion(db) {
1072
+ const userVersion = getUserVersion(db);
1073
+ if (userVersion > 0) return userVersion;
1074
+ const legacyVersion = readLegacyStateVersion(db);
1075
+ if (legacyVersion > 0) return legacyVersion;
1076
+ return tableExists(db, "bookmarks") ? 1 : 0;
1077
+ }
1078
+ function hasAnyStateSchema(db) {
1079
+ return tableExists(db, "state_meta") || tableExists(db, "bookmarks");
1080
+ }
1081
+ function setStateSchemaVersion(db) {
1082
+ createStateSchema(db);
1083
+ setUserVersion(db, STATE_SCHEMA_VERSION);
1084
+ db.prepare(
1085
+ `
1086
+ INSERT INTO state_meta(key, value)
1087
+ VALUES ('version', ?)
1088
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
1089
+ `
1090
+ ).run(String(STATE_SCHEMA_VERSION));
1091
+ }
1092
+ function ensureSchema(db, dbPath) {
1093
+ const currentVersion = getCurrentStateSchemaVersion(db);
1094
+ if (currentVersion > STATE_SCHEMA_VERSION) return;
1095
+ if (currentVersion === 0 && !hasAnyStateSchema(db)) {
1096
+ setStateSchemaVersion(db);
1097
+ return;
1098
+ }
1099
+ runSchemaMigrations(db, {
1100
+ dbPath,
1101
+ currentVersion,
1102
+ targetVersion: STATE_SCHEMA_VERSION,
1103
+ backupLabel: "state-migration",
1104
+ backupTables: ["bookmarks", "session_aliases"],
1105
+ migrations: [
1106
+ { version: 1, migrate: createBookmarksTable },
1107
+ { version: 2, migrate: createSessionAliasesTable },
1108
+ { version: 3, destructive: true, migrate: migrateBookmarkFacts }
1109
+ ]
1110
+ });
1111
+ if (currentVersion < STATE_SCHEMA_VERSION) {
1112
+ setStateSchemaVersion(db);
1113
+ }
569
1114
  }
570
- function loadAliasMap() {
1115
+ function withStateDb(fn) {
1116
+ const statePath = getStateDbPath();
1117
+ const db = openDb(statePath);
1118
+ if (!db) throw new StateStorageUnavailableError();
571
1119
  try {
572
- return new Map(listSessionAliases().map((alias) => [aliasKey(alias.reference), alias]));
573
- } catch (error) {
574
- if (!(error instanceof StateStorageUnavailableError)) {
575
- appLogger.warn("api.session_aliases.load_failed", {
576
- error: error instanceof Error ? error.message : String(error)
577
- });
1120
+ if (getStateSchemaEnsuredPath() !== statePath) {
1121
+ ensureSchema(db, statePath);
1122
+ setStateSchemaEnsuredPath(statePath);
578
1123
  }
579
- return /* @__PURE__ */ new Map();
1124
+ return fn(db);
1125
+ } finally {
1126
+ db.close();
580
1127
  }
581
1128
  }
582
- function buildAliasView() {
583
- const aliases = loadAliasMap();
1129
+ var memoryBookmarks = /* @__PURE__ */ new Map();
1130
+ function getBookmarkKey(reference) {
1131
+ const normalized = normalizeSessionReference(reference);
1132
+ return JSON.stringify([normalized.agentName, normalized.sessionId]);
1133
+ }
1134
+ function compareBookmarks(left, right) {
1135
+ if (left.bookmarkedAt !== right.bookmarkedAt) {
1136
+ return left.bookmarkedAt > right.bookmarkedAt ? -1 : 1;
1137
+ }
1138
+ return getBookmarkKey(left.reference).localeCompare(getBookmarkKey(right.reference));
1139
+ }
1140
+ function normalizeBookmark(bookmark) {
1141
+ return {
1142
+ reference: normalizeSessionReference(bookmark.reference),
1143
+ bookmarkedAt: bookmark.bookmarkedAt
1144
+ };
1145
+ }
1146
+ function toBookmarkRecord(row) {
1147
+ return {
1148
+ reference: normalizeSessionReference({
1149
+ agentName: String(row.agent_name ?? ""),
1150
+ sessionId: String(row.session_id ?? "")
1151
+ }),
1152
+ bookmarkedAt: Number(row.bookmarked_at ?? 0)
1153
+ };
1154
+ }
1155
+ function readBookmarks(db) {
1156
+ const rows = db.prepare(
1157
+ `
1158
+ SELECT agent_name, session_id, bookmarked_at
1159
+ FROM bookmarks
1160
+ ORDER BY bookmarked_at DESC, agent_name ASC, session_id ASC
1161
+ `
1162
+ ).all();
1163
+ return rows.map(toBookmarkRecord);
1164
+ }
1165
+ function listMemoryBookmarks() {
1166
+ return [...memoryBookmarks.values()].sort(compareBookmarks);
1167
+ }
1168
+ function listBookmarks() {
1169
+ if (useMemoryStateStore()) return listMemoryBookmarks();
1170
+ return withStateDb(readBookmarks);
1171
+ }
1172
+ function upsertBookmark(reference) {
1173
+ const normalized = normalizeSessionReference(reference);
1174
+ const key = getBookmarkKey(normalized);
1175
+ if (useMemoryStateStore()) {
1176
+ const bookmark = memoryBookmarks.get(key) ?? {
1177
+ reference: normalized,
1178
+ bookmarkedAt: Date.now()
1179
+ };
1180
+ memoryBookmarks.set(key, bookmark);
1181
+ return bookmark;
1182
+ }
1183
+ return withStateDb((db) => {
1184
+ const row = db.prepare(
1185
+ `
1186
+ INSERT INTO bookmarks(agent_name, session_id, bookmarked_at)
1187
+ VALUES (?, ?, ?)
1188
+ ON CONFLICT(agent_name, session_id) DO UPDATE
1189
+ SET bookmarked_at = bookmarks.bookmarked_at
1190
+ RETURNING agent_name, session_id, bookmarked_at
1191
+ `
1192
+ ).get(normalized.agentName, normalized.sessionId, Date.now());
1193
+ if (!row) {
1194
+ throw new Error("Bookmark upsert returned no row");
1195
+ }
1196
+ return toBookmarkRecord(row);
1197
+ });
1198
+ }
1199
+ function importBookmarks(bookmarks) {
1200
+ const unique = /* @__PURE__ */ new Map();
1201
+ for (const bookmark of bookmarks) {
1202
+ const normalized = normalizeBookmark(bookmark);
1203
+ const key = getBookmarkKey(normalized.reference);
1204
+ if (!unique.has(key)) unique.set(key, normalized);
1205
+ }
1206
+ if (useMemoryStateStore()) {
1207
+ for (const [key, bookmark] of unique) {
1208
+ if (!memoryBookmarks.has(key)) memoryBookmarks.set(key, bookmark);
1209
+ }
1210
+ return listMemoryBookmarks();
1211
+ }
1212
+ return withStateDb((db) => {
1213
+ const insert = db.prepare(
1214
+ `
1215
+ INSERT INTO bookmarks(agent_name, session_id, bookmarked_at)
1216
+ VALUES (?, ?, ?)
1217
+ ON CONFLICT(agent_name, session_id) DO NOTHING
1218
+ `
1219
+ );
1220
+ const write = db.transaction(() => {
1221
+ for (const bookmark of unique.values()) {
1222
+ insert.run(
1223
+ bookmark.reference.agentName,
1224
+ bookmark.reference.sessionId,
1225
+ bookmark.bookmarkedAt
1226
+ );
1227
+ }
1228
+ });
1229
+ write.immediate();
1230
+ return readBookmarks(db);
1231
+ });
1232
+ }
1233
+ function deleteBookmark(reference) {
1234
+ const normalized = normalizeSessionReference(reference);
1235
+ if (useMemoryStateStore()) {
1236
+ memoryBookmarks.delete(getBookmarkKey(normalized));
1237
+ return;
1238
+ }
1239
+ withStateDb((db) => {
1240
+ db.prepare(
1241
+ `
1242
+ DELETE FROM bookmarks
1243
+ WHERE agent_name = ? AND session_id = ?
1244
+ `
1245
+ ).run(normalized.agentName, normalized.sessionId);
1246
+ });
1247
+ }
1248
+ var SESSION_ALIAS_MAX_LENGTH = 160;
1249
+ var SessionAliasValidationError = class extends Error {
1250
+ constructor() {
1251
+ super("Invalid session alias");
1252
+ this.name = "SessionAliasValidationError";
1253
+ }
1254
+ };
1255
+ var memoryAliases = /* @__PURE__ */ new Map();
1256
+ function getAliasKey(reference) {
1257
+ const normalized = normalizeSessionReference(reference);
1258
+ return JSON.stringify([normalized.agentName, normalized.sessionId]);
1259
+ }
1260
+ function toSessionAlias(row) {
1261
+ const reference = normalizeSessionReference({
1262
+ agentName: String(row.agent_name ?? ""),
1263
+ sessionId: String(row.session_id ?? "")
1264
+ });
1265
+ return {
1266
+ reference,
1267
+ alias: String(row.alias ?? ""),
1268
+ updatedAt: Number(row.updated_at ?? 0)
1269
+ };
1270
+ }
1271
+ function normalizeSessionAlias(value) {
1272
+ const alias = value.trim();
1273
+ if (!alias || alias.length > SESSION_ALIAS_MAX_LENGTH) return null;
1274
+ return alias;
1275
+ }
1276
+ function listSessionAliases() {
1277
+ if (useMemoryStateStore()) return [...memoryAliases.values()];
1278
+ return withStateDb(
1279
+ (db) => db.prepare(
1280
+ `
1281
+ SELECT agent_name, session_id, alias, updated_at
1282
+ FROM session_aliases
1283
+ ORDER BY updated_at DESC
1284
+ `
1285
+ ).all().map(toSessionAlias)
1286
+ );
1287
+ }
1288
+ function upsertSessionAlias(reference, alias) {
1289
+ const normalizedAlias = normalizeSessionAlias(alias);
1290
+ if (!normalizedAlias) {
1291
+ throw new SessionAliasValidationError();
1292
+ }
1293
+ const normalizedReference = normalizeSessionReference(reference);
1294
+ const saved = {
1295
+ reference: normalizedReference,
1296
+ alias: normalizedAlias,
1297
+ updatedAt: Date.now()
1298
+ };
1299
+ if (useMemoryStateStore()) {
1300
+ memoryAliases.set(getAliasKey(normalizedReference), saved);
1301
+ return saved;
1302
+ }
1303
+ return withStateDb((db) => {
1304
+ db.prepare(
1305
+ `
1306
+ INSERT INTO session_aliases(agent_name, session_id, alias, updated_at)
1307
+ VALUES (?, ?, ?, ?)
1308
+ ON CONFLICT(agent_name, session_id) DO UPDATE SET
1309
+ alias = excluded.alias,
1310
+ updated_at = excluded.updated_at
1311
+ `
1312
+ ).run(saved.reference.agentName, saved.reference.sessionId, saved.alias, saved.updatedAt);
1313
+ return saved;
1314
+ });
1315
+ }
1316
+ function deleteSessionAlias(reference) {
1317
+ const normalizedReference = normalizeSessionReference(reference);
1318
+ if (useMemoryStateStore()) {
1319
+ memoryAliases.delete(getAliasKey(normalizedReference));
1320
+ return;
1321
+ }
1322
+ withStateDb((db) => {
1323
+ db.prepare(
1324
+ `
1325
+ DELETE FROM session_aliases
1326
+ WHERE agent_name = ? AND session_id = ?
1327
+ `
1328
+ ).run(normalizedReference.agentName, normalizedReference.sessionId);
1329
+ });
1330
+ }
1331
+ function canonicalSession(reference, session) {
1332
+ const normalized = normalizeSessionReference(reference);
1333
+ assertSessionIdentity(session, normalized.agentName);
1334
+ if (session.reference.sessionId !== normalized.sessionId) {
1335
+ throw new Error("Session reference does not match expected session");
1336
+ }
1337
+ return session;
1338
+ }
1339
+ function compareDescending(left, right) {
1340
+ if (left === right) return 0;
1341
+ return left > right ? -1 : 1;
1342
+ }
1343
+ function getBookmarkViewTime(bookmark) {
1344
+ return bookmark.availability === "available" ? bookmark.session.time_updated ?? bookmark.session.time_created : bookmark.bookmarkedAt;
1345
+ }
1346
+ function compareBookmarkViews(left, right) {
1347
+ return compareDescending(getBookmarkViewTime(left), getBookmarkViewTime(right)) || compareDescending(left.bookmarkedAt, right.bookmarkedAt) || getSessionReferenceKey(left.reference).localeCompare(getSessionReferenceKey(right.reference));
1348
+ }
1349
+ function materializeBookmarkViews(bookmarks, options) {
1350
+ const liveByKey = /* @__PURE__ */ new Map();
1351
+ const missingByKey = /* @__PURE__ */ new Map();
1352
+ const normalizedBookmarks = bookmarks.map((bookmark) => {
1353
+ const reference = normalizeSessionReference(bookmark.reference);
1354
+ return {
1355
+ bookmark: { reference, bookmarkedAt: bookmark.bookmarkedAt },
1356
+ key: getSessionReferenceKey(reference)
1357
+ };
1358
+ });
1359
+ for (const { bookmark, key } of normalizedBookmarks) {
1360
+ const { reference } = bookmark;
1361
+ const live = options.liveSessionsByReference.get(key);
1362
+ if (live) liveByKey.set(key, canonicalSession(reference, live));
1363
+ else if (!missingByKey.has(key)) missingByKey.set(key, reference);
1364
+ }
1365
+ const cachedByKey = /* @__PURE__ */ new Map();
1366
+ if (missingByKey.size > 0 && options.resolveCachedSessions) {
1367
+ for (const cached of options.resolveCachedSessions([...missingByKey.values()])) {
1368
+ const reference = normalizeSessionReference(cached.reference);
1369
+ const key = getSessionReferenceKey(reference);
1370
+ if (missingByKey.has(key) && !cachedByKey.has(key)) {
1371
+ cachedByKey.set(key, canonicalSession(reference, cached.session));
1372
+ }
1373
+ }
1374
+ }
1375
+ const views = normalizedBookmarks.map(({ bookmark, key }) => {
1376
+ const session = liveByKey.get(key) ?? cachedByKey.get(key);
1377
+ if (session) return { ...bookmark, availability: "available", session };
1378
+ return {
1379
+ ...bookmark,
1380
+ availability: options.knownAgentNames.has(bookmark.reference.agentName) ? "session-unavailable" : "agent-unavailable"
1381
+ };
1382
+ });
1383
+ return views.sort(compareBookmarkViews);
1384
+ }
1385
+
1386
+ // src/api/session-aliases-view.ts
1387
+ function aliasKey(reference) {
1388
+ return `${reference.agentName.toLowerCase()}\0${reference.sessionId}`;
1389
+ }
1390
+ function buildAliasView(aliases) {
584
1391
  return {
585
1392
  size: aliases.size,
586
1393
  get: (reference) => aliases.get(aliasKey(reference))?.alias,
@@ -591,9 +1398,28 @@ function buildAliasView() {
591
1398
  entries: () => aliases.values()
592
1399
  };
593
1400
  }
1401
+ function readAliasView() {
1402
+ try {
1403
+ return buildAliasView(
1404
+ new Map(listSessionAliases().map((alias) => [aliasKey(alias.reference), alias]))
1405
+ );
1406
+ } catch (error) {
1407
+ if (!(error instanceof StateStorageUnavailableError)) {
1408
+ appLogger.warn("api.session_aliases.load_failed", {
1409
+ error: error instanceof Error ? error.message : String(error)
1410
+ });
1411
+ }
1412
+ return null;
1413
+ }
1414
+ }
1415
+ var EMPTY_ALIAS_VIEW = buildAliasView(/* @__PURE__ */ new Map());
594
1416
  var cachedView = null;
595
1417
  function loadAliasView() {
596
- return cachedView ??= buildAliasView();
1418
+ if (cachedView) return cachedView;
1419
+ const loaded = readAliasView();
1420
+ if (!loaded) return EMPTY_ALIAS_VIEW;
1421
+ cachedView = loaded;
1422
+ return loaded;
597
1423
  }
598
1424
  function invalidateAliasView() {
599
1425
  cachedView = null;
@@ -622,7 +1448,9 @@ function findAliasSearchResults(query, options, scanResult, aliases, context = {
622
1448
  const lookupSession = (agentName, sessionId) => {
623
1449
  let byId = sessionsByAgent.get(agentName);
624
1450
  if (!byId) {
625
- byId = new Map((scanResult.byAgent[agentName] ?? []).map((item) => [item.id, item]));
1451
+ byId = new Map(
1452
+ (scanResult.byAgent[agentName] ?? []).map((item) => [item.reference.sessionId, item])
1453
+ );
626
1454
  sessionsByAgent.set(agentName, byId);
627
1455
  }
628
1456
  return byId.get(sessionId);
@@ -650,16 +1478,16 @@ function findAliasSearchResults(query, options, scanResult, aliases, context = {
650
1478
  // src/api/handlers.ts
651
1479
  var KNOWN_AGENT_NAMES = getAgentInfoMap({}).map((agent) => agent.name);
652
1480
  var KNOWN_AGENT_NAME_SET = new Set(KNOWN_AGENT_NAMES);
653
- async function resolveProjectScope(cwd, sessions, resolver) {
1481
+ async function resolveProjectScope(cwd, sessions, resolver, signal) {
654
1482
  const normalizedCwd = normalizeProjectDirectory(cwd);
655
1483
  const matchingSession = sessions.find(
656
- (session) => normalizeProjectDirectory(session.directory) === normalizedCwd && session.project_identity != null && session.project_identity_resolver_revision === PROJECT_IDENTITY_RESOLVER_REVISION && Boolean(session.project_identity_input_signature)
1484
+ (session) => normalizeProjectDirectory(session.directory) === normalizedCwd && session.project_identity_resolver_revision === PROJECT_IDENTITY_RESOLVER_REVISION && Boolean(session.project_identity_input_signature)
657
1485
  );
658
- if (matchingSession?.project_identity) {
1486
+ if (matchingSession) {
659
1487
  return createProjectScopeMatcherFromIdentity(cwd, matchingSession.project_identity);
660
1488
  }
661
1489
  if (!resolver) throw new Error("Project identity resolver is unavailable");
662
- const projection = await resolver.resolve(cwd);
1490
+ const projection = await resolver.resolve(cwd, signal);
663
1491
  return createProjectScopeMatcherFromIdentity(cwd, projection.identity);
664
1492
  }
665
1493
  function reportProjectScopeResolutionFailure(endpoint, error) {
@@ -668,6 +1496,14 @@ function reportProjectScopeResolutionFailure(endpoint, error) {
668
1496
  error: error instanceof Error ? error.message : String(error)
669
1497
  });
670
1498
  }
1499
+ function projectScopeResolutionFailureResponse(c, endpoint, error) {
1500
+ if (error instanceof ProjectIdentityRequestAbortedError) throw error;
1501
+ reportProjectScopeResolutionFailure(endpoint, error);
1502
+ if (error instanceof ProjectIdentityQueueFullError) {
1503
+ return c.json({ error: "Project scope busy; retry later" }, 429);
1504
+ }
1505
+ return c.json({ error: "Project scope unavailable" }, 503);
1506
+ }
671
1507
  function reportInvalidQueryParameter(endpoint, parameter, validationOutcome) {
672
1508
  appLogger.warn("api.query_parameter.invalid", {
673
1509
  endpoint,
@@ -684,22 +1520,8 @@ function parseDateWindowRequest(c, endpoint, defaults) {
684
1520
  response: c.json({ error: `${outcome.parameter} ${outcome.error}` }, 400)
685
1521
  };
686
1522
  }
687
- function toSessionListItem(session) {
688
- const {
689
- model_usage: _modelUsage,
690
- project_identity_resolver_revision: _resolverRevision,
691
- project_identity_input_signature: _identityInputSignature,
692
- smart_tags_source_updated_at: _smartTagsSourceUpdatedAt,
693
- smart_tags_classifier_revision: _smartTagsClassifierRevision,
694
- ...item
695
- } = session;
696
- return item;
697
- }
698
1523
  function getSessionHeadReference(session) {
699
- return {
700
- agentName: getSessionAgentKey(session),
701
- sessionId: session.id
702
- };
1524
+ return session.reference;
703
1525
  }
704
1526
  function handleGetConfig(c, defaults) {
705
1527
  const payload = {
@@ -737,31 +1559,64 @@ function handleGetAgents(c, scanSource, defaults = {}) {
737
1559
  }
738
1560
  function handleGetProjects(c, scanSource, defaults = {}) {
739
1561
  const scanResult = scanSource.getSnapshot();
1562
+ const params = searchParams(c);
1563
+ const limit = parseLimit(params.get("limit"), PROJECT_PAGE_LIMIT_POLICY);
1564
+ if (limit.kind === "invalid") {
1565
+ reportInvalidQueryParameter("projects", "limit", "rejected");
1566
+ return c.json({ error: limit.error }, 400);
1567
+ }
1568
+ const projectIdentity = parseProjectIdentityFilter(
1569
+ c.req.query("projectKind"),
1570
+ c.req.query("projectKey")
1571
+ );
1572
+ if (projectIdentity === null) {
1573
+ return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
1574
+ }
740
1575
  const window = parseDateWindowRequest(c, "projects", defaults);
741
1576
  if (window.kind === "rejected") return window.response;
742
1577
  const { from, to } = window;
743
1578
  const analyticsRevision = getAnalyticsRevision();
744
- const projects = getSnapshotAggregation(
1579
+ const catalog = getSnapshotAggregation(
745
1580
  scanSource,
746
1581
  scanResult.sessions,
747
1582
  ["projects", from, to, analyticsRevision],
748
1583
  () => {
749
1584
  const tree = getSnapshotSessionTree(scanSource, scanResult.sessions);
750
1585
  const costFacts = listDashboardCostFacts({ from, to, includeModelCosts: false });
751
- return {
752
- projects: attachProjectMetricsFromTree(
753
- listCachedProjectGroups(scanResult.sessions),
754
- tree,
755
- from,
756
- to,
757
- costFacts
758
- ).filter(
759
- (project) => project.sessionCount > 0 || project.messages > 0 || project.tokens > 0 || project.cost > 0
760
- )
761
- };
1586
+ const projects2 = attachProjectMetricsFromTree(
1587
+ listCachedProjectGroups(scanResult.sessions),
1588
+ tree,
1589
+ from,
1590
+ to,
1591
+ costFacts
1592
+ ).filter(
1593
+ (project) => project.sessionCount > 0 || project.messages > 0 || project.tokens > 0 || project.cost > 0
1594
+ );
1595
+ return { projects: projects2, summary: summarizeProjects(projects2) };
762
1596
  }
763
1597
  );
764
- return c.json(projects);
1598
+ const projects = projectIdentity ? catalog.projects.filter(
1599
+ (project) => project.identityKind === projectIdentity.kind && project.identityKey === projectIdentity.key
1600
+ ) : catalog.projects;
1601
+ const page = paginateSnapshot(projects, {
1602
+ cursor: params.get("cursor") ?? void 0,
1603
+ limit: limit.value,
1604
+ query: params,
1605
+ snapshotIdentity: scanResult.sessions,
1606
+ viewIdentity: catalog.projects
1607
+ });
1608
+ if (page.kind === "invalid_cursor") {
1609
+ reportInvalidQueryParameter("projects", "cursor", "rejected");
1610
+ return c.json({ error: "cursor is invalid for this request" }, 400);
1611
+ }
1612
+ if (page.kind === "stale_snapshot") {
1613
+ return c.json({ error: "project snapshot changed; restart pagination" }, 409);
1614
+ }
1615
+ return c.json({
1616
+ projects: page.items,
1617
+ summary: projectIdentity ? summarizeProjects(projects) : catalog.summary,
1618
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {}
1619
+ });
765
1620
  }
766
1621
  async function handleGetSessions(c, scanSource, defaults = {}, resolver) {
767
1622
  const scanResult = scanSource.getSnapshot();
@@ -788,10 +1643,14 @@ async function handleGetSessions(c, scanSource, defaults = {}, resolver) {
788
1643
  let projectScope;
789
1644
  if (cwd && !projectIdentity) {
790
1645
  try {
791
- projectScope = await resolveProjectScope(cwd, scanResult.sessions, resolver);
1646
+ projectScope = await resolveProjectScope(
1647
+ cwd,
1648
+ scanResult.sessions,
1649
+ resolver,
1650
+ c.req.raw.signal
1651
+ );
792
1652
  } catch (error) {
793
- reportProjectScopeResolutionFailure("sessions", error);
794
- return c.json({ error: "Project scope unavailable" }, 503);
1653
+ return projectScopeResolutionFailureResponse(c, "sessions", error);
795
1654
  }
796
1655
  }
797
1656
  if (sessionQuery.agent.kind === "unknown") {
@@ -834,7 +1693,7 @@ async function handleGetSessions(c, scanSource, defaults = {}, resolver) {
834
1693
  });
835
1694
  }
836
1695
  if (paginationRequested) {
837
- const page = paginateSessionSnapshot(sessions, {
1696
+ const page = paginateSnapshot(sessions, {
838
1697
  cursor: params.get("cursor") ?? void 0,
839
1698
  limit: sessionQuery.limit.value,
840
1699
  query: params,
@@ -850,14 +1709,14 @@ async function handleGetSessions(c, scanSource, defaults = {}, resolver) {
850
1709
  }
851
1710
  return c.json({
852
1711
  sessions: page.items.map(
853
- (session) => toSessionListItem(aliases.decorate(session, getSessionHeadReference(session)))
1712
+ (session) => toPublicSessionHead(aliases.decorate(session, getSessionHeadReference(session)))
854
1713
  ),
855
1714
  ...page.nextCursor ? { nextCursor: page.nextCursor } : {}
856
1715
  });
857
1716
  }
858
1717
  return c.json({
859
1718
  sessions: sessions.map(
860
- (session) => toSessionListItem(aliases.decorate(session, getSessionHeadReference(session)))
1719
+ (session) => toPublicSessionHead(aliases.decorate(session, getSessionHeadReference(session)))
861
1720
  )
862
1721
  });
863
1722
  }
@@ -895,10 +1754,14 @@ async function handleSearchSessions(c, scanSource, defaults = {}, resolver) {
895
1754
  let projectScope;
896
1755
  if (cwd) {
897
1756
  try {
898
- projectScope = await resolveProjectScope(cwd, scanResult.sessions, resolver);
1757
+ projectScope = await resolveProjectScope(
1758
+ cwd,
1759
+ scanResult.sessions,
1760
+ resolver,
1761
+ c.req.raw.signal
1762
+ );
899
1763
  } catch (error) {
900
- reportProjectScopeResolutionFailure("search", error);
901
- return c.json({ error: "Project scope unavailable" }, 503);
1764
+ return projectScopeResolutionFailureResponse(c, "search", error);
902
1765
  }
903
1766
  }
904
1767
  const { cwd: _cwd, ...searchOptions } = searchRequestOptions;
@@ -926,7 +1789,9 @@ async function handleSearchSessions(c, scanSource, defaults = {}, resolver) {
926
1789
  resolvedSearchOptions.limit ?? 50
927
1790
  );
928
1791
  return c.json({
929
- results: withParentContext(mergedResults, getSessionTree, aliases)
1792
+ results: withParentContext(mergedResults, getSessionTree, aliases).map(
1793
+ toPublicReferencedSessionHead
1794
+ )
930
1795
  });
931
1796
  }
932
1797
  async function handleGetFileActivity(c, defaults = {}, resolver) {
@@ -956,10 +1821,9 @@ async function handleGetFileActivity(c, defaults = {}, resolver) {
956
1821
  let projectScope;
957
1822
  if (cwd) {
958
1823
  try {
959
- projectScope = await resolveProjectScope(cwd, [], resolver);
1824
+ projectScope = await resolveProjectScope(cwd, [], resolver, c.req.raw.signal);
960
1825
  } catch (error) {
961
- reportProjectScopeResolutionFailure("file-activity", error);
962
- return c.json({ error: "Project scope unavailable" }, 503);
1826
+ return projectScopeResolutionFailureResponse(c, "file-activity", error);
963
1827
  }
964
1828
  }
965
1829
  const aliases = loadAliasView();
@@ -976,10 +1840,10 @@ async function handleGetFileActivity(c, defaults = {}, resolver) {
976
1840
  from: window.from,
977
1841
  to: window.to,
978
1842
  limit: sessionQuery.limit.value
979
- }).map((activity) => decorateFileActivity(activity, aliases))
1843
+ }).map((activity) => toPublicReferencedSessionHead(decorateFileActivity(activity, aliases)))
980
1844
  });
981
1845
  }
982
- async function handleGetSessionData(c, scanSource, resolver) {
1846
+ async function handleGetSessionData(c, scanSource) {
983
1847
  const startedAt = performance.now();
984
1848
  const agentName = c.req.param("agent");
985
1849
  const sessionId = c.req.param("id");
@@ -995,18 +1859,11 @@ async function handleGetSessionData(c, scanSource, resolver) {
995
1859
  sessionId
996
1860
  };
997
1861
  const messageCursor = optionalQueryValue(c.req.query("messageCursor"));
998
- const materialize = (fallback) => materializeSessionDetailResponse(scanSource.getSnapshot(), reference, {
999
- ...messageCursor ? { messageCursor } : {},
1000
- ...fallback ? { projectIdentityFallback: fallback } : {}
1001
- });
1002
- let result = materialize();
1003
- if (result.status === "needs-identity") {
1004
- if (!resolver) throw new Error("Project identity resolver is unavailable");
1005
- result = materialize((await resolver.resolve(result.directory)).identity);
1006
- }
1007
- if (result.status === "needs-identity") {
1008
- throw new Error("Project identity fallback was not applied");
1009
- }
1862
+ const result = materializeSessionDetailResponse(
1863
+ scanSource.getSnapshot(),
1864
+ reference,
1865
+ messageCursor ? { messageCursor } : {}
1866
+ );
1010
1867
  if (result.status === "unknown-agent") {
1011
1868
  return c.json({ error: `Unknown agent: ${agentName}` }, 404);
1012
1869
  }
@@ -1068,12 +1925,9 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
1068
1925
  if (dateWindow.kind === "rejected") return dateWindow.response;
1069
1926
  const { from, to, days } = resolveTimeWindow({
1070
1927
  mode: "dashboard",
1071
- query: {
1072
- days: c.req.query("days"),
1073
- from: c.req.query("from"),
1074
- to: c.req.query("to")
1075
- },
1076
- defaults
1928
+ window: dateWindow,
1929
+ days: c.req.query("days"),
1930
+ defaultDays: defaults.days
1077
1931
  });
1078
1932
  const scope = {
1079
1933
  agent: optionalQueryValue(c.req.query("agent"))?.toLowerCase(),
@@ -1137,12 +1991,14 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
1137
1991
  const aliases = loadAliasView();
1138
1992
  return c.json({
1139
1993
  ...data,
1140
- recentSessions: data.recentSessions.map((item) => ({
1141
- ...item,
1142
- session: aliases.decorate(item.session, item.reference)
1143
- })),
1994
+ recentSessions: data.recentSessions.map(
1995
+ (item) => toPublicReferencedSessionHead({
1996
+ ...item,
1997
+ session: aliases.decorate(item.session, item.reference)
1998
+ })
1999
+ ),
1144
2000
  recentFileActivities: data.recentFileActivities.map(
1145
- (activity) => decorateFileActivity(activity, aliases)
2001
+ (activity) => toPublicReferencedSessionHead(decorateFileActivity(activity, aliases))
1146
2002
  )
1147
2003
  });
1148
2004
  }
@@ -1170,7 +2026,10 @@ function materializeStoredBookmarks(scanSource, bookmarks, aliases) {
1170
2026
  liveSessionsByReference: sessionIndex.byRouteKey,
1171
2027
  knownAgentNames: KNOWN_AGENT_NAME_SET,
1172
2028
  resolveCachedSessions: loadCachedSessionHeads
1173
- }).map((bookmark) => decorateBookmark(bookmark, aliases));
2029
+ }).map((bookmark) => {
2030
+ const decorated = decorateBookmark(bookmark, aliases);
2031
+ return decorated.availability === "available" ? { ...decorated, session: toPublicSessionHead(decorated.session) } : decorated;
2032
+ });
1174
2033
  }
1175
2034
  function handleGetBookmarks(c, scanSource) {
1176
2035
  return withStorageErrors(
@@ -1418,7 +2277,26 @@ data: ${JSON.stringify(data)}
1418
2277
  };
1419
2278
 
1420
2279
  // src/api/routes.ts
1421
- function createSseResponse(eventSource, signal) {
2280
+ var MAX_ACTIVE_SSE_CONNECTIONS = 32;
2281
+ function createSseConnectionBudget(maxConnections) {
2282
+ let activeConnections = 0;
2283
+ return {
2284
+ acquire() {
2285
+ if (activeConnections >= maxConnections) return null;
2286
+ activeConnections += 1;
2287
+ let released = false;
2288
+ return () => {
2289
+ if (released) return;
2290
+ released = true;
2291
+ activeConnections -= 1;
2292
+ };
2293
+ },
2294
+ activeCount() {
2295
+ return activeConnections;
2296
+ }
2297
+ };
2298
+ }
2299
+ function createSseResponse(eventSource, signal, releaseConnection) {
1422
2300
  let cancelStream = () => {
1423
2301
  };
1424
2302
  let drainStream = () => {
@@ -1438,11 +2316,12 @@ function createSseResponse(eventSource, signal) {
1438
2316
  const cleanup = () => {
1439
2317
  if (isClosed) return false;
1440
2318
  isClosed = true;
1441
- buffer.close();
2319
+ buffer?.close();
1442
2320
  if (heartbeat) clearInterval(heartbeat);
1443
2321
  unsubscribeSessions();
1444
2322
  unsubscribeScanStatus();
1445
2323
  signal.removeEventListener("abort", abortStream);
2324
+ releaseConnection();
1446
2325
  return true;
1447
2326
  };
1448
2327
  abortStream = () => {
@@ -1452,20 +2331,25 @@ function createSseResponse(eventSource, signal) {
1452
2331
  if (cleanup()) controller.error(new Error("SSE client fell behind"));
1453
2332
  });
1454
2333
  drainStream = () => buffer.drain();
1455
- buffer.enqueue("connected", { timestamp: Date.now() });
1456
- buffer.enqueueScanStatus(eventSource.getScanStatus());
1457
- unsubscribeSessions = eventSource.subscribe((event) => {
1458
- buffer.enqueue(event.type, event);
1459
- });
1460
- unsubscribeScanStatus = eventSource.subscribeScanStatus((event) => {
1461
- buffer.enqueueScanStatus(event);
1462
- });
1463
- heartbeat = setInterval(() => buffer.enqueueHeartbeat(), 15e3);
1464
- cancelStream = () => {
2334
+ try {
2335
+ buffer.enqueue("connected", { timestamp: Date.now() });
2336
+ buffer.enqueueScanStatus(eventSource.getScanStatus());
2337
+ unsubscribeSessions = eventSource.subscribe((event) => {
2338
+ buffer?.enqueue(event.type, event);
2339
+ });
2340
+ unsubscribeScanStatus = eventSource.subscribeScanStatus((event) => {
2341
+ buffer?.enqueueScanStatus(event);
2342
+ });
2343
+ heartbeat = setInterval(() => buffer?.enqueueHeartbeat(), 15e3);
2344
+ cancelStream = () => {
2345
+ cleanup();
2346
+ };
2347
+ if (signal.aborted) abortStream();
2348
+ else signal.addEventListener("abort", abortStream, { once: true });
2349
+ } catch (error) {
1465
2350
  cleanup();
1466
- };
1467
- if (signal.aborted) abortStream();
1468
- else signal.addEventListener("abort", abortStream, { once: true });
2351
+ throw error;
2352
+ }
1469
2353
  },
1470
2354
  pull() {
1471
2355
  drainStream();
@@ -1485,6 +2369,7 @@ function createSseResponse(eventSource, signal) {
1485
2369
  }
1486
2370
  function createApiRoutes(scanSource, eventSource, options = {}) {
1487
2371
  const api = new Hono();
2372
+ const sseConnections = createSseConnectionBudget(MAX_ACTIVE_SSE_CONNECTIONS);
1488
2373
  const listDefaults = {
1489
2374
  from: options.defaultSessionFrom,
1490
2375
  to: options.defaultSessionTo,
@@ -1493,158 +2378,51 @@ function createApiRoutes(scanSource, eventSource, options = {}) {
1493
2378
  api.get("/config", (c) => handleGetConfig(c, listDefaults));
1494
2379
  if (eventSource) {
1495
2380
  api.get("/status", (c) => handleGetScanStatus(c, eventSource));
1496
- }
1497
- api.get("/agents", (c) => handleGetAgents(c, scanSource, listDefaults));
1498
- api.get("/projects", (c) => handleGetProjects(c, scanSource, listDefaults));
1499
- api.get(
1500
- "/sessions",
1501
- (c) => handleGetSessions(c, scanSource, listDefaults, options.projectIdentityResolver)
1502
- );
1503
- api.get(
1504
- "/search",
1505
- (c) => handleSearchSessions(c, scanSource, listDefaults, options.projectIdentityResolver)
1506
- );
1507
- api.get(
1508
- "/file-activity",
1509
- (c) => handleGetFileActivity(c, listDefaults, options.projectIdentityResolver)
1510
- );
1511
- api.get(
1512
- "/sessions/:agent/:id",
1513
- (c) => handleGetSessionData(c, scanSource, options.projectIdentityResolver)
1514
- );
1515
- api.get("/dashboard", (c) => handleGetDashboard(c, scanSource, listDefaults));
1516
- api.get("/bookmarks", (c) => handleGetBookmarks(c, scanSource));
1517
- api.put("/bookmarks", (c) => handlePutBookmark(c));
1518
- api.post("/bookmarks/import", (c) => handleImportBookmarks(c, scanSource));
1519
- api.delete("/bookmarks/:agent/:id", (c) => handleDeleteBookmark(c));
1520
- api.put("/session-aliases/:agent/:id", (c) => handlePutSessionAlias(c));
1521
- api.delete("/session-aliases/:agent/:id", (c) => handleDeleteSessionAlias(c));
1522
- api.post("/logs", (c) => handlePostClientLog(c));
1523
- if (eventSource) {
1524
- api.get("/events", (c) => {
1525
- const signal = options.shutdownSignal ? AbortSignal.any([c.req.raw.signal, options.shutdownSignal]) : c.req.raw.signal;
1526
- return createSseResponse(eventSource, signal);
1527
- });
1528
- }
1529
- return api;
1530
- }
1531
-
1532
- // src/project-identity-resolver.ts
1533
- import { Worker } from "worker_threads";
1534
- var PROJECT_IDENTITY_RESOLVER_MAX_WORKERS = 2;
1535
- var SHUTDOWN_ERROR_MESSAGE = "Project identity resolver shut down";
1536
- function toError(error) {
1537
- return error instanceof Error ? error : new Error(String(error));
1538
- }
1539
- function isWorkerMessage(message) {
1540
- return typeof message === "object" && message !== null && "type" in message && (message.type === "resolved" && "requestId" in message && "projection" in message || message.type === "failed" && "requestId" in message && "error" in message);
1541
- }
1542
- var ThreadProjectIdentityResolver = class {
1543
- constructor(workerUrl, maxWorkers = PROJECT_IDENTITY_RESOLVER_MAX_WORKERS) {
1544
- this.workerUrl = workerUrl;
1545
- this.maxWorkers = maxWorkers;
1546
- }
1547
- workerUrl;
1548
- maxWorkers;
1549
- workers = /* @__PURE__ */ new Set();
1550
- queue = [];
1551
- nextRequestId = 1;
1552
- isShuttingDown = false;
1553
- resolve(cwd) {
1554
- if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE));
1555
- return new Promise((resolve4, reject) => {
1556
- this.queue.push({ requestId: this.nextRequestId++, cwd, resolve: resolve4, reject });
1557
- this.pump();
1558
- });
1559
- }
1560
- async shutdown() {
1561
- if (this.isShuttingDown) return;
1562
- this.isShuttingDown = true;
1563
- const error = new Error(SHUTDOWN_ERROR_MESSAGE);
1564
- for (const request of this.queue.splice(0)) request.reject(error);
1565
- const workers = [...this.workers];
1566
- this.workers.clear();
1567
- for (const slot of workers) {
1568
- slot.closed = true;
1569
- slot.pending?.reject(error);
1570
- slot.pending = null;
1571
- }
1572
- await Promise.allSettled(workers.map(({ worker }) => worker.terminate()));
1573
- }
1574
- pump() {
1575
- while (!this.isShuttingDown && this.queue.length > 0) {
1576
- const slot = this.idleWorker() ?? this.createWorker();
1577
- if (!slot) return;
1578
- const request = this.queue.shift();
1579
- if (!request) return;
1580
- slot.pending = request;
1581
- try {
1582
- slot.worker.postMessage({
1583
- type: "resolve",
1584
- requestId: request.requestId,
1585
- cwd: request.cwd
2381
+ }
2382
+ api.get("/agents", (c) => handleGetAgents(c, scanSource, listDefaults));
2383
+ api.get("/projects", (c) => handleGetProjects(c, scanSource, listDefaults));
2384
+ api.get(
2385
+ "/sessions",
2386
+ (c) => handleGetSessions(c, scanSource, listDefaults, options.projectIdentityResolver)
2387
+ );
2388
+ api.get(
2389
+ "/search",
2390
+ (c) => handleSearchSessions(c, scanSource, listDefaults, options.projectIdentityResolver)
2391
+ );
2392
+ api.get(
2393
+ "/file-activity",
2394
+ (c) => handleGetFileActivity(c, listDefaults, options.projectIdentityResolver)
2395
+ );
2396
+ api.get("/sessions/:agent/:id", (c) => handleGetSessionData(c, scanSource));
2397
+ api.get("/dashboard", (c) => handleGetDashboard(c, scanSource, listDefaults));
2398
+ api.get("/bookmarks", (c) => handleGetBookmarks(c, scanSource));
2399
+ api.put("/bookmarks", (c) => handlePutBookmark(c));
2400
+ api.post("/bookmarks/import", (c) => handleImportBookmarks(c, scanSource));
2401
+ api.delete("/bookmarks/:agent/:id", (c) => handleDeleteBookmark(c));
2402
+ api.put("/session-aliases/:agent/:id", (c) => handlePutSessionAlias(c));
2403
+ api.delete("/session-aliases/:agent/:id", (c) => handleDeleteSessionAlias(c));
2404
+ api.post("/logs", (c) => handlePostClientLog(c));
2405
+ if (eventSource) {
2406
+ api.get("/events", (c) => {
2407
+ const releaseConnection = sseConnections.acquire();
2408
+ if (!releaseConnection) {
2409
+ appLogger.warn("api.sse.connection_limit_reached", {
2410
+ active_connections: sseConnections.activeCount(),
2411
+ connection_limit: MAX_ACTIVE_SSE_CONNECTIONS
1586
2412
  });
1587
- } catch (error) {
1588
- this.closeWorker(slot, toError(error));
2413
+ c.header("Retry-After", "1");
2414
+ return c.json({ error: "Too many active event streams" }, 429);
1589
2415
  }
1590
- }
1591
- }
1592
- idleWorker() {
1593
- for (const slot of this.workers) {
1594
- if (!slot.closed && slot.pending == null) return slot;
1595
- }
1596
- return null;
1597
- }
1598
- createWorker() {
1599
- if (this.workers.size >= this.maxWorkers) return null;
1600
- let worker;
1601
- try {
1602
- worker = new Worker(this.workerUrl);
1603
- } catch (error) {
1604
- this.rejectQueued(toError(error));
1605
- return null;
1606
- }
1607
- const slot = { worker, pending: null, closed: false };
1608
- this.workers.add(slot);
1609
- worker.unref();
1610
- worker.on("message", (message) => {
1611
- if (appLogger.consumeWorkerMessage(message)) return;
1612
- this.handleWorkerMessage(slot, message);
1613
- });
1614
- worker.on("error", (error) => this.closeWorker(slot, toError(error)));
1615
- worker.on("exit", (code) => {
1616
- if (!slot.closed) {
1617
- this.closeWorker(slot, new Error(`Project identity worker exited with code ${code}`));
2416
+ const signal = options.shutdownSignal ? AbortSignal.any([c.req.raw.signal, options.shutdownSignal]) : c.req.raw.signal;
2417
+ try {
2418
+ return createSseResponse(eventSource, signal, releaseConnection);
2419
+ } catch (error) {
2420
+ releaseConnection();
2421
+ throw error;
1618
2422
  }
1619
2423
  });
1620
- return slot;
1621
- }
1622
- handleWorkerMessage(slot, message) {
1623
- if (!isWorkerMessage(message) || slot.closed) return;
1624
- const request = slot.pending;
1625
- if (!request || message.requestId !== request.requestId) return;
1626
- slot.pending = null;
1627
- if (message.type === "resolved") request.resolve(message.projection);
1628
- else request.reject(new Error(message.error));
1629
- this.pump();
1630
- }
1631
- closeWorker(slot, error) {
1632
- if (slot.closed) return;
1633
- slot.closed = true;
1634
- this.workers.delete(slot);
1635
- const request = slot.pending;
1636
- slot.pending = null;
1637
- request?.reject(error);
1638
- if (!this.isShuttingDown) this.pump();
1639
- }
1640
- rejectQueued(error) {
1641
- for (const request of this.queue.splice(0)) request.reject(error);
1642
2424
  }
1643
- };
1644
- function createProjectIdentityResolver() {
1645
- return new ThreadProjectIdentityResolver(
1646
- new URL("./project-identity-worker.js", import.meta.url)
1647
- );
2425
+ return api;
1648
2426
  }
1649
2427
 
1650
2428
  // src/loopback-authority.ts
@@ -1763,9 +2541,16 @@ function loopbackWriteGuard() {
1763
2541
  import { randomBytes, timingSafeEqual } from "crypto";
1764
2542
  import { readFileSync } from "fs";
1765
2543
  import { isIP as isIP2 } from "net";
1766
- var REMOTE_ACCESS_QUERY_PARAM = "access_token";
2544
+ var ACCESS_TOKEN_QUERY_PARAM = "access_token";
1767
2545
  var RemoteTransportError = class extends Error {
1768
2546
  };
2547
+ function assertTrustedProxyListener(hostname, transport) {
2548
+ if (transport.kind === "trusted-proxy" && !isLoopbackHostname(hostname)) {
2549
+ throw new RemoteTransportError(
2550
+ "--trust-proxy requires a loopback --host so clients cannot reach the HTTP backend directly."
2551
+ );
2552
+ }
2553
+ }
1769
2554
  function resolveRemoteTransport(request) {
1770
2555
  const hasCert = Boolean(request.tlsCertPath);
1771
2556
  const hasKey = Boolean(request.tlsKeyPath);
@@ -1790,16 +2575,45 @@ function resolveRemoteTransport(request) {
1790
2575
  );
1791
2576
  }
1792
2577
  }
1793
- if (request.trustProxy) return { kind: "trusted-proxy" };
2578
+ if (request.trustProxy) {
2579
+ const transport = { kind: "trusted-proxy" };
2580
+ assertTrustedProxyListener(request.hostname, transport);
2581
+ return transport;
2582
+ }
1794
2583
  return isLoopbackHostname(request.hostname) ? { kind: "loopback" } : { kind: "plaintext" };
1795
2584
  }
1796
2585
  function resolveRemoteAccessPolicy(hostname, transport) {
2586
+ assertTrustedProxyListener(hostname, transport);
1797
2587
  const bindCategory = isLoopbackHostname(hostname) ? "loopback" : "network";
2588
+ const directLoopback = bindCategory === "loopback" && transport.kind === "loopback";
1798
2589
  return {
1799
2590
  bindCategory,
1800
- authenticationRequired: bindCategory === "network" || transport.kind !== "loopback"
2591
+ remoteAccessRequired: !directLoopback
1801
2592
  };
1802
2593
  }
2594
+ function resolvePublicOrigin(value, transport) {
2595
+ if (transport.kind !== "trusted-proxy") {
2596
+ if (value) throw new RemoteTransportError("--public-url requires --trust-proxy.");
2597
+ return void 0;
2598
+ }
2599
+ if (!value) {
2600
+ throw new RemoteTransportError(
2601
+ "--trust-proxy requires an HTTPS --public-url for browser startup links."
2602
+ );
2603
+ }
2604
+ let url;
2605
+ try {
2606
+ url = new URL(value);
2607
+ } catch {
2608
+ throw new RemoteTransportError("--public-url must be a valid HTTPS origin.");
2609
+ }
2610
+ if (url.protocol !== "https:" || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
2611
+ throw new RemoteTransportError(
2612
+ "--public-url must be an HTTPS origin without credentials, path, query, or fragment."
2613
+ );
2614
+ }
2615
+ return url.origin;
2616
+ }
1803
2617
  var FORWARDED_PROTO_HEADER = "X-Forwarded-Proto";
1804
2618
  function requireProxyTls() {
1805
2619
  return async (c, next) => {
@@ -1810,7 +2624,7 @@ function requireProxyTls() {
1810
2624
  await next();
1811
2625
  };
1812
2626
  }
1813
- function createRemoteAccessToken() {
2627
+ function createAccessToken() {
1814
2628
  return randomBytes(32).toString("base64url");
1815
2629
  }
1816
2630
  function isLoopbackHostname(hostname) {
@@ -1833,12 +2647,12 @@ function requestToken(c) {
1833
2647
  const bearer = bearerToken(c);
1834
2648
  if (bearer) return bearer;
1835
2649
  if (c.req.method !== "GET") return void 0;
1836
- return c.req.query(REMOTE_ACCESS_QUERY_PARAM);
2650
+ return c.req.query(ACCESS_TOKEN_QUERY_PARAM);
1837
2651
  }
1838
- function remoteAccessAuth(expectedToken) {
2652
+ function accessTokenAuth(expectedToken) {
1839
2653
  return async (c, next) => {
1840
2654
  if (!tokenMatches(requestToken(c), expectedToken)) {
1841
- return c.json({ error: "Remote access authentication required" }, 401);
2655
+ return c.json({ error: "API access token required" }, 401);
1842
2656
  }
1843
2657
  await next();
1844
2658
  };
@@ -1912,20 +2726,21 @@ async function createServer(port, store, options = {}) {
1912
2726
  const hostname = options.hostname ?? "127.0.0.1";
1913
2727
  const transport = options.transport ?? resolveRemoteTransport({ hostname });
1914
2728
  const accessPolicy = resolveRemoteAccessPolicy(hostname, transport);
1915
- const remoteAccessToken = accessPolicy.authenticationRequired ? options.remoteAccessToken ?? (options.remoteAccess ? createRemoteAccessToken() : null) : null;
1916
- const loopbackAuthorityEnabled = !accessPolicy.authenticationRequired;
2729
+ const publicOrigin = resolvePublicOrigin(options.publicUrl, transport);
2730
+ const accessToken = options.accessToken || createAccessToken();
2731
+ const loopbackAuthorityEnabled = !accessPolicy.remoteAccessRequired;
1917
2732
  const shutdownController = new AbortController();
1918
2733
  const projectIdentityResolver = options.projectIdentityResolver ?? createProjectIdentityResolver();
1919
2734
  let actualPort = null;
1920
2735
  appLogger.info("server.access_policy", {
1921
2736
  bind_category: accessPolicy.bindCategory,
1922
2737
  transport: transport.kind,
1923
- authentication: remoteAccessToken ? "token" : "none",
2738
+ authentication: "token",
1924
2739
  loopback_authority: loopbackAuthorityEnabled ? "enabled" : "disabled"
1925
2740
  });
1926
- if (accessPolicy.authenticationRequired && !remoteAccessToken) {
2741
+ if (accessPolicy.remoteAccessRequired && !options.remoteAccess && !options.accessToken) {
1927
2742
  throw new Error(
1928
- `Refusing to expose CodeSesh on ${hostname} without authentication. Add --remote-access to continue.`
2743
+ `Refusing to expose CodeSesh on ${hostname} without explicit remote access. Add --remote-access to continue.`
1929
2744
  );
1930
2745
  }
1931
2746
  app.use(
@@ -1995,9 +2810,7 @@ async function createServer(port, store, options = {}) {
1995
2810
  if (transport.kind === "trusted-proxy") {
1996
2811
  app.use("/api/*", requireProxyTls());
1997
2812
  }
1998
- if (remoteAccessToken) {
1999
- app.use("/api/*", remoteAccessAuth(remoteAccessToken));
2000
- }
2813
+ app.use("/api/*", accessTokenAuth(accessToken));
2001
2814
  app.use("/api/*", loopbackWriteGuard());
2002
2815
  app.use(
2003
2816
  "/api/*",
@@ -2053,16 +2866,17 @@ async function createServer(port, store, options = {}) {
2053
2866
  }
2054
2867
  }
2055
2868
  const scheme = transport.kind === "tls" ? "https" : "http";
2056
- const baseUrl = accessPolicy.bindCategory === "loopback" ? `${scheme}://localhost:${actualPort}` : `${scheme}://${hostname}:${actualPort}`;
2057
- const url = remoteAccessToken ? `${baseUrl}/?${REMOTE_ACCESS_QUERY_PARAM}=${encodeURIComponent(remoteAccessToken)}` : baseUrl;
2869
+ const listenerUrl = accessPolicy.bindCategory === "loopback" ? `${scheme}://localhost:${actualPort}` : `${scheme}://${hostname}:${actualPort}`;
2870
+ const url = `${publicOrigin ?? listenerUrl}/?${ACCESS_TOKEN_QUERY_PARAM}=${encodeURIComponent(accessToken)}`;
2058
2871
  appLogger.info("server.listen", {
2059
2872
  port: actualPort,
2060
2873
  requested_port: port,
2061
2874
  hostname,
2062
- remote_access: Boolean(remoteAccessToken),
2875
+ public_origin: publicOrigin,
2876
+ remote_access: accessPolicy.remoteAccessRequired,
2063
2877
  transport: transport.kind
2064
2878
  });
2065
- if (accessPolicy.authenticationRequired) {
2879
+ if (accessPolicy.remoteAccessRequired) {
2066
2880
  appLogger.warn("server.listen.remote_access", {
2067
2881
  hostname,
2068
2882
  port: actualPort,
@@ -2090,6 +2904,7 @@ async function createServer(port, store, options = {}) {
2090
2904
  }
2091
2905
  return {
2092
2906
  url,
2907
+ listenerUrl,
2093
2908
  shutdown: async () => {
2094
2909
  appLogger.info("server.shutdown", { port: actualPort });
2095
2910
  shutdownController.abort();
@@ -2113,6 +2928,87 @@ async function createServer(port, store, options = {}) {
2113
2928
  import { existsSync as existsSync4 } from "fs";
2114
2929
  import { fileURLToPath as fileURLToPath3 } from "url";
2115
2930
 
2931
+ // src/agent-backfill-scheduler.ts
2932
+ var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2933
+ var PARTIAL_BACKFILL_RETRY_DELAY_MS = 5 * 60 * 1e3;
2934
+ var AgentBackfillScheduler = class {
2935
+ constructor(options) {
2936
+ this.options = options;
2937
+ }
2938
+ options;
2939
+ cacheIntegrityValidUntilByAgent = /* @__PURE__ */ new Map();
2940
+ isShuttingDown = false;
2941
+ needsBackfill(agent) {
2942
+ const { from, to } = this.options.startupScanOptions;
2943
+ if (from == null && to == null) return false;
2944
+ const now = Date.now();
2945
+ if ((this.cacheIntegrityValidUntilByAgent.get(agent.name) ?? 0) >= now) return false;
2946
+ const lastSync = readAgentLastFullSyncAt(agent.name);
2947
+ if (lastSync.status === "failed") {
2948
+ appLogger.warn("scan.backfill.cache_state_unavailable", {
2949
+ agent: agent.name,
2950
+ state: "last_full_sync"
2951
+ });
2952
+ return false;
2953
+ }
2954
+ const lastSyncAt = lastSync.value;
2955
+ if (lastSyncAt == null || now - lastSyncAt > BACKFILL_INTERVAL_MS) {
2956
+ return agent.isAvailable();
2957
+ }
2958
+ this.cacheIntegrityValidUntilByAgent.set(agent.name, lastSyncAt + BACKFILL_INTERVAL_MS);
2959
+ return false;
2960
+ }
2961
+ enqueue(agentName) {
2962
+ if (this.isShuttingDown || !this.options.lifecycle.enqueue(agentName)) return;
2963
+ this.options.statusReporter.publishBackfillStatus();
2964
+ this.pump();
2965
+ }
2966
+ shutdown() {
2967
+ this.isShuttingDown = true;
2968
+ this.options.lifecycle.cancelAll();
2969
+ this.cacheIntegrityValidUntilByAgent.clear();
2970
+ }
2971
+ pump() {
2972
+ if (this.isShuttingDown) return;
2973
+ const attempt = this.options.lifecycle.startNext();
2974
+ if (!attempt) return;
2975
+ this.options.statusReporter.publishBackfillStatus();
2976
+ void this.options.runAttempt(attempt).then((result) => this.complete(attempt, result)).catch((error) => this.reject(attempt, error));
2977
+ }
2978
+ complete(attempt, result) {
2979
+ if (this.isShuttingDown) return;
2980
+ const current = this.options.lifecycle.stateFor(attempt.agentName);
2981
+ if (current?.status === "running" && current.attemptId === attempt.attemptId) {
2982
+ this.options.statusReporter.flushProgressStatus(`backfill:${attempt.agentName}`);
2983
+ if (this.options.lifecycle.complete(attempt, result)) {
2984
+ if (result === "failed") {
2985
+ this.cacheIntegrityValidUntilByAgent.delete(attempt.agentName);
2986
+ } else if (result === "committed") {
2987
+ if (current.completion?.completeness === "partial") {
2988
+ this.cacheIntegrityValidUntilByAgent.delete(attempt.agentName);
2989
+ this.options.scheduleRefresh(attempt.agentName, PARTIAL_BACKFILL_RETRY_DELAY_MS);
2990
+ appLogger.info("scan.backfill.retry_scheduled", {
2991
+ agent: attempt.agentName,
2992
+ delay_ms: PARTIAL_BACKFILL_RETRY_DELAY_MS
2993
+ });
2994
+ } else {
2995
+ this.cacheIntegrityValidUntilByAgent.set(
2996
+ attempt.agentName,
2997
+ Date.now() + BACKFILL_INTERVAL_MS
2998
+ );
2999
+ }
3000
+ }
3001
+ this.options.statusReporter.publishBackfillStatus();
3002
+ }
3003
+ }
3004
+ this.pump();
3005
+ }
3006
+ reject(attempt, error) {
3007
+ appLogger.error("scan.backfill.queue_error", { agent: attempt.agentName, error });
3008
+ this.complete(attempt, "failed");
3009
+ }
3010
+ };
3011
+
2116
3012
  // src/agent-operation-scheduler.ts
2117
3013
  var PENDING_REFRESH_DELAY_MS = 100;
2118
3014
  var MAX_ADAPTIVE_REFRESH_DELAY_MS = 3e4;
@@ -2291,9 +3187,10 @@ var BackfillLifecycle = class {
2291
3187
  updateProgress(attempt, progress) {
2292
3188
  const current = this.matchingRunningAttempt(attempt);
2293
3189
  if (!current) return false;
3190
+ const phaseChanged = progress.phase != null && progress.phase !== current.progress?.phase;
2294
3191
  this.attempts.set(attempt.agentName, {
2295
3192
  ...current,
2296
- progress: { ...current.progress, ...progress }
3193
+ progress: phaseChanged ? { ...progress } : { ...current.progress, ...progress }
2297
3194
  });
2298
3195
  return true;
2299
3196
  }
@@ -2379,7 +3276,12 @@ var LiveSessionIndex = class {
2379
3276
  this.byAgent = Object.fromEntries(
2380
3277
  this.agents.flatMap((agent) => {
2381
3278
  if (this.scanFailures[agent.name] && !(agent.name in snapshot.byAgent)) return [];
2382
- return [[agent.name, sortSessions(snapshot.byAgent[agent.name] ?? [])]];
3279
+ const sessions = snapshot.byAgent[agent.name] ?? [];
3280
+ for (const session of sessions) {
3281
+ assertSessionIdentity(session, agent.name);
3282
+ assertIdentifiedSessionHead(session);
3283
+ }
3284
+ return [[agent.name, sortSessions(sessions)]];
2383
3285
  })
2384
3286
  );
2385
3287
  this.sessions = mergeSortedSessions(Object.values(this.byAgent));
@@ -2398,6 +3300,10 @@ var LiveSessionIndex = class {
2398
3300
  return this.agentsByName.get(agentName);
2399
3301
  }
2400
3302
  commitAgentSessions(agentName, nextSessions, candidateChangedIds = []) {
3303
+ for (const session of nextSessions) {
3304
+ assertSessionIdentity(session, agentName);
3305
+ assertIdentifiedSessionHead(session);
3306
+ }
2401
3307
  delete this.cacheFailures[agentName];
2402
3308
  delete this.scanFailures[agentName];
2403
3309
  const previousSessions = this.byAgent[agentName] ?? [];
@@ -2415,10 +3321,12 @@ var LiveSessionIndex = class {
2415
3321
  this.sessions = mergeSortedSessions(Object.values(this.byAgent));
2416
3322
  if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) return null;
2417
3323
  const changedSessionHeads = changes.map(({ session }) => ({
2418
- reference: { agentName, sessionId: session.id },
3324
+ reference: session.reference,
2419
3325
  session
2420
3326
  }));
2421
- const previousSessionIds = new Set(previousSessions.map((session) => session.id));
3327
+ const previousSessionIds = new Set(
3328
+ previousSessions.map((session) => session.reference.sessionId)
3329
+ );
2422
3330
  const newSessionRefs = changedSessionHeads.flatMap(
2423
3331
  ({ reference }) => previousSessionIds.has(reference.sessionId) ? [] : [reference]
2424
3332
  );
@@ -2432,14 +3340,13 @@ var LiveSessionIndex = class {
2432
3340
  return {
2433
3341
  type: "sessions-updated",
2434
3342
  changedAgents: [agentName],
2435
- newSessions: counts.new,
2436
3343
  newSessionRefs,
2437
- updatedSessions: counts.updated,
2438
- removedSessions: counts.removed,
2439
3344
  totalSessions: this.sessions.length,
2440
3345
  timestamp: Date.now(),
2441
- changedSessionHeads,
2442
- projectionRelatedSessionHeads: projectionContext.relatedSessionHeads,
3346
+ changedSessionHeads: changedSessionHeads.map(toPublicReferencedSessionHead),
3347
+ projectionRelatedSessionHeads: projectionContext.relatedSessionHeads.map(
3348
+ toPublicReferencedSessionHead
3349
+ ),
2443
3350
  projectionSessionOrder: projectionContext.sessionOrder,
2444
3351
  removedSessionRefs
2445
3352
  };
@@ -2503,6 +3410,16 @@ var PendingSearchIndexJobBatch = class {
2503
3410
  }
2504
3411
  }
2505
3412
  }
3413
+ progress(progress, onError) {
3414
+ for (const waiter of this.waiters) {
3415
+ if (!waiter.agentNames.has(progress.agentName)) continue;
3416
+ try {
3417
+ waiter.onProgress?.(progress);
3418
+ } catch (error) {
3419
+ onError(error);
3420
+ }
3421
+ }
3422
+ }
2506
3423
  merge(context, jobs, waiter) {
2507
3424
  this.context = context;
2508
3425
  this.waiters.push(waiter);
@@ -2542,10 +3459,16 @@ var PendingSearchIndexJobs = class {
2542
3459
  get changeCount() {
2543
3460
  return this.pendingBatch?.changeCount ?? 0;
2544
3461
  }
2545
- enqueue(id, context, jobs, onStarted) {
3462
+ enqueue(id, context, jobs, onStarted, onProgress) {
2546
3463
  if (jobs.length === 0) return Promise.resolve();
2547
3464
  return new Promise((resolve4, reject) => {
2548
- const waiter = { resolve: resolve4, reject, onStarted };
3465
+ const waiter = {
3466
+ resolve: resolve4,
3467
+ reject,
3468
+ onStarted,
3469
+ onProgress,
3470
+ agentNames: new Set(jobs.map((job) => job.agentName))
3471
+ };
2549
3472
  if (this.pendingBatch) {
2550
3473
  this.pendingBatch.merge(context, jobs, waiter);
2551
3474
  } else {
@@ -2597,7 +3520,7 @@ function mergeChanges(pending, job) {
2597
3520
  delete pending.meta[sessionId];
2598
3521
  }
2599
3522
  for (const change of job.changes) {
2600
- const sessionId = change.session.id;
3523
+ const sessionId = change.session.reference.sessionId;
2601
3524
  pending.removedSessionIds.delete(sessionId);
2602
3525
  pending.changesBySessionId.set(sessionId, change);
2603
3526
  if (Object.hasOwn(job.meta, sessionId)) pending.meta[sessionId] = job.meta[sessionId];
@@ -2637,17 +3560,17 @@ var SearchIndexJobRunner = class {
2637
3560
  pendingJobs = new PendingSearchIndexJobs();
2638
3561
  pendingMaintenanceJobs = new PendingSearchIndexJobs();
2639
3562
  isShuttingDown = false;
2640
- enqueue(context, jobs, onStarted) {
2641
- return this.enqueueIn(this.pendingJobs, context, jobs, onStarted);
3563
+ enqueue(context, jobs, onStarted, onProgress) {
3564
+ return this.enqueueIn(this.pendingJobs, context, jobs, onStarted, onProgress);
2642
3565
  }
2643
3566
  enqueueMaintenance(context, jobs) {
2644
3567
  return this.enqueueIn(this.pendingMaintenanceJobs, context, jobs);
2645
3568
  }
2646
- enqueueIn(queue, context, jobs, onStarted) {
3569
+ enqueueIn(queue, context, jobs, onStarted, onProgress) {
2647
3570
  if (jobs.length === 0) return Promise.resolve();
2648
3571
  if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE2));
2649
3572
  const batchId = this.nextBatchId++;
2650
- const completion = queue.enqueue(batchId, context, jobs, onStarted);
3573
+ const completion = queue.enqueue(batchId, context, jobs, onStarted, onProgress);
2651
3574
  if (this.activeBatch) {
2652
3575
  const snapshot = this.snapshot();
2653
3576
  appLogger.debug("search_index.worker_queued", {
@@ -2744,7 +3667,6 @@ var SearchIndexJobRunner = class {
2744
3667
  this.startNextBatch();
2745
3668
  return;
2746
3669
  }
2747
- worker.unref();
2748
3670
  this.worker = worker;
2749
3671
  this.activeBatch = batch;
2750
3672
  worker.on("message", (message) => {
@@ -2752,6 +3674,17 @@ var SearchIndexJobRunner = class {
2752
3674
  if (this.worker !== worker) return;
2753
3675
  const activeBatch = this.activeBatch;
2754
3676
  if (!activeBatch) return;
3677
+ if (message.type === "publication-progress") {
3678
+ const { agentName, stage } = message;
3679
+ activeBatch.progress({ agentName, stage }, (error) => {
3680
+ appLogger.error("search_index.worker_progress_listener_failed", {
3681
+ batch_id: activeBatch.id,
3682
+ context: activeBatch.context,
3683
+ error: toError2(error)
3684
+ });
3685
+ });
3686
+ return;
3687
+ }
2755
3688
  if (message.type === "sync-result") {
2756
3689
  logSearchIndexSync(message.context, message.result);
2757
3690
  return;
@@ -2923,23 +3856,36 @@ var SearchIndexMaintenanceScheduler = class {
2923
3856
  }
2924
3857
  this.remaining = pending.total;
2925
3858
  this.publishStatus();
2926
- const cached = this.cachedSessionsByAgent.get(agentName) ?? loadCachedSessions(agentName);
3859
+ let cached = this.cachedSessionsByAgent.get(agentName);
3860
+ if (!cached) {
3861
+ const outcome = readCachedSessions(agentName);
3862
+ if (outcome.status === "failed") {
3863
+ throw new Error(`Session cache read failed for ${agentName}`);
3864
+ }
3865
+ cached = outcome.value ?? void 0;
3866
+ }
2927
3867
  if (!cached) throw new Error(`Session cache is unavailable for ${agentName}`);
2928
3868
  this.cachedSessionsByAgent.set(agentName, cached);
2929
3869
  const sessionsById = new Map(
2930
- cached.sessions.map((session, sortIndex) => [session.id, { session, sortIndex }])
3870
+ cached.sessions.map((session, sortIndex) => [
3871
+ session.reference.sessionId,
3872
+ { session, sortIndex }
3873
+ ])
3874
+ );
3875
+ const changes = pending.sessionIds.flatMap(
3876
+ (sessionId) => {
3877
+ const change = sessionsById.get(sessionId);
3878
+ return change ? [change] : [];
3879
+ }
2931
3880
  );
2932
- const changes = pending.sessionIds.flatMap((sessionId) => {
2933
- const change = sessionsById.get(sessionId);
2934
- return change ? [change] : [];
2935
- });
2936
3881
  if (changes.length === 0) {
2937
3882
  throw new Error(`No cached sessions matched pending maintenance for ${agentName}`);
2938
3883
  }
2939
3884
  const meta = Object.fromEntries(
2940
3885
  changes.flatMap(({ session }) => {
2941
- const sessionMeta = cached.meta[session.id];
2942
- return sessionMeta ? [[session.id, sessionMeta]] : [];
3886
+ const sessionId = session.reference.sessionId;
3887
+ const sessionMeta = cached.meta[sessionId];
3888
+ return sessionMeta ? [[sessionId, sessionMeta]] : [];
2943
3889
  })
2944
3890
  );
2945
3891
  const job = {
@@ -3053,13 +3999,8 @@ var LatestValueThrottle = class {
3053
3999
  // src/scan-status-model.ts
3054
4000
  var ScanStatusModel = class {
3055
4001
  status = {
3056
- active: false,
3057
4002
  phase: "idle",
3058
- pendingAgents: [],
3059
- scanningAgents: [],
3060
- completedAgents: [],
3061
4003
  agentStatuses: {},
3062
- totalAgents: 0,
3063
4004
  updatedAt: Date.now(),
3064
4005
  backfill: { active: false, pendingAgents: [], completedAgents: [], failedAgents: [] },
3065
4006
  searchIndexMaintenance: {
@@ -3070,6 +4011,8 @@ var ScanStatusModel = class {
3070
4011
  }
3071
4012
  };
3072
4013
  snapshot() {
4014
+ const agentLists = this.agentLists(this.status.agentStatuses);
4015
+ const active = this.hasActiveAgents(this.status.agentStatuses);
3073
4016
  const backfill = {
3074
4017
  ...this.status.backfill,
3075
4018
  pendingAgents: [...this.status.backfill.pendingAgents],
@@ -3094,9 +4037,9 @@ var ScanStatusModel = class {
3094
4037
  return {
3095
4038
  type: "scan-status",
3096
4039
  ...this.status,
3097
- pendingAgents: [...this.status.pendingAgents],
3098
- scanningAgents: [...this.status.scanningAgents],
3099
- completedAgents: [...this.status.completedAgents],
4040
+ active,
4041
+ ...agentLists,
4042
+ totalAgents: Object.keys(this.status.agentStatuses).length,
3100
4043
  agentStatuses: Object.fromEntries(
3101
4044
  Object.entries(this.status.agentStatuses).map(([agentName, status]) => [
3102
4045
  agentName,
@@ -3129,37 +4072,26 @@ var ScanStatusModel = class {
3129
4072
  );
3130
4073
  return this.set({
3131
4074
  ...this.status,
3132
- active: uniqueAgentNames.length > 0,
3133
4075
  phase: uniqueAgentNames.length > 0 ? phase : "idle",
3134
- pendingAgents: uniqueAgentNames,
3135
- scanningAgents: [],
3136
- completedAgents: [],
3137
4076
  agentStatuses,
3138
- totalAgents: uniqueAgentNames.length,
3139
4077
  startedAt: uniqueAgentNames.length > 0 ? now : void 0,
3140
4078
  updatedAt: now,
3141
4079
  completedAt: uniqueAgentNames.length > 0 ? void 0 : now
3142
4080
  });
3143
4081
  }
3144
4082
  setPhase(phase) {
3145
- if (!this.status.active) return null;
4083
+ if (!this.hasActiveAgents(this.status.agentStatuses)) return null;
3146
4084
  return this.set({ ...this.status, phase, updatedAt: Date.now() });
3147
4085
  }
3148
4086
  beginAgent(agentName, sessionCount) {
3149
- if (!this.status.active)
4087
+ if (!this.hasActiveAgents(this.status.agentStatuses)) {
3150
4088
  this.startBatch([agentName], "scanning", { [agentName]: sessionCount });
3151
- const pendingAgents = this.status.pendingAgents.filter((agent) => agent !== agentName);
3152
- const scanningAgents = [.../* @__PURE__ */ new Set([...this.status.scanningAgents, agentName])];
3153
- const completedAgents = this.status.completedAgents.filter((agent) => agent !== agentName);
4089
+ }
3154
4090
  const existingStatus = this.status.agentStatuses[agentName];
3155
4091
  const now = Date.now();
3156
4092
  return this.set({
3157
4093
  ...this.status,
3158
- active: true,
3159
4094
  phase: this.status.phase === "initializing" ? "initializing" : "scanning",
3160
- pendingAgents,
3161
- scanningAgents,
3162
- completedAgents,
3163
4095
  agentStatuses: {
3164
4096
  ...this.status.agentStatuses,
3165
4097
  [agentName]: {
@@ -3172,7 +4104,6 @@ var ScanStatusModel = class {
3172
4104
  updatedAt: now
3173
4105
  }
3174
4106
  },
3175
- totalAgents: Math.max(this.status.totalAgents, pendingAgents.length + scanningAgents.length),
3176
4107
  updatedAt: now,
3177
4108
  completedAt: void 0
3178
4109
  });
@@ -3197,14 +4128,14 @@ var ScanStatusModel = class {
3197
4128
  };
3198
4129
  return this.set({
3199
4130
  ...this.status,
3200
- phase: this.activePhase(this.status.pendingAgents, this.status.scanningAgents, agentStatuses),
4131
+ phase: this.activePhase(agentStatuses),
3201
4132
  agentStatuses,
3202
4133
  updatedAt: now
3203
4134
  });
3204
4135
  }
3205
4136
  queueAgentPublication(agentName) {
3206
4137
  const status = this.status.agentStatuses[agentName];
3207
- if (!this.status.active || !status || status.status !== "scanning" && status.status !== "finalizing") {
4138
+ if (!this.hasActiveAgents(this.status.agentStatuses) || !status || status.status !== "scanning" && status.status !== "finalizing") {
3208
4139
  return null;
3209
4140
  }
3210
4141
  const now = Date.now();
@@ -3214,14 +4145,14 @@ var ScanStatusModel = class {
3214
4145
  };
3215
4146
  return this.set({
3216
4147
  ...this.status,
3217
- phase: this.activePhase(this.status.pendingAgents, this.status.scanningAgents, agentStatuses),
4148
+ phase: this.activePhase(agentStatuses),
3218
4149
  agentStatuses,
3219
4150
  updatedAt: now
3220
4151
  });
3221
4152
  }
3222
4153
  publishAgent(agentName) {
3223
4154
  const status = this.status.agentStatuses[agentName];
3224
- if (!this.status.active || !status || status.status !== "scanning" && status.status !== "finalizing" && status.status !== "publish-queued") {
4155
+ if (!this.hasActiveAgents(this.status.agentStatuses) || !status || status.status !== "scanning" && status.status !== "finalizing" && status.status !== "publish-queued") {
3225
4156
  return null;
3226
4157
  }
3227
4158
  const now = Date.now();
@@ -3231,16 +4162,12 @@ var ScanStatusModel = class {
3231
4162
  };
3232
4163
  return this.set({
3233
4164
  ...this.status,
3234
- phase: this.activePhase(this.status.pendingAgents, this.status.scanningAgents, agentStatuses),
4165
+ phase: this.activePhase(agentStatuses),
3235
4166
  agentStatuses,
3236
4167
  updatedAt: now
3237
4168
  });
3238
4169
  }
3239
4170
  finishAgent(agentName, sessionCount, completion = { completeness: "complete" }) {
3240
- const pendingAgents = this.status.pendingAgents.filter((agent) => agent !== agentName);
3241
- const scanningAgents = this.status.scanningAgents.filter((agent) => agent !== agentName);
3242
- const completedAgents = [.../* @__PURE__ */ new Set([...this.status.completedAgents, agentName])];
3243
- const isActive = pendingAgents.length > 0 || scanningAgents.length > 0;
3244
4171
  const now = Date.now();
3245
4172
  const previousStatus = this.status.agentStatuses[agentName];
3246
4173
  const total = previousStatus?.total ?? previousStatus?.processed;
@@ -3258,46 +4185,37 @@ var ScanStatusModel = class {
3258
4185
  completedAt: now
3259
4186
  }
3260
4187
  };
4188
+ const isActive = this.hasActiveAgents(agentStatuses);
3261
4189
  return this.set({
3262
4190
  ...this.status,
3263
- active: isActive,
3264
- phase: isActive ? this.activePhase(pendingAgents, scanningAgents, agentStatuses) : "idle",
3265
- pendingAgents,
3266
- scanningAgents,
3267
- completedAgents,
4191
+ phase: isActive ? this.activePhase(agentStatuses) : "idle",
3268
4192
  agentStatuses,
3269
4193
  updatedAt: now,
3270
4194
  completedAt: isActive ? void 0 : now
3271
4195
  });
3272
4196
  }
3273
4197
  failAgent(agentName, error, sessionCount) {
3274
- const pendingAgents = this.status.pendingAgents.filter((agent) => agent !== agentName);
3275
- const scanningAgents = this.status.scanningAgents.filter((agent) => agent !== agentName);
3276
- const completedAgents = this.status.completedAgents.filter((agent) => agent !== agentName);
3277
- const isActive = pendingAgents.length > 0 || scanningAgents.length > 0;
3278
4198
  const now = Date.now();
3279
4199
  const previousStatus = this.status.agentStatuses[agentName];
4200
+ const agentStatuses = {
4201
+ ...this.status.agentStatuses,
4202
+ [agentName]: {
4203
+ agentName,
4204
+ status: "failed",
4205
+ error,
4206
+ total: previousStatus?.total,
4207
+ processed: previousStatus?.processed,
4208
+ sessions: sessionCount ?? previousStatus?.sessions ?? 0,
4209
+ startedAt: previousStatus?.startedAt,
4210
+ updatedAt: now,
4211
+ completedAt: now
4212
+ }
4213
+ };
4214
+ const isActive = this.hasActiveAgents(agentStatuses);
3280
4215
  return this.set({
3281
4216
  ...this.status,
3282
- active: isActive,
3283
- phase: isActive ? this.activePhase(pendingAgents, scanningAgents, this.status.agentStatuses) : "idle",
3284
- pendingAgents,
3285
- scanningAgents,
3286
- completedAgents,
3287
- agentStatuses: {
3288
- ...this.status.agentStatuses,
3289
- [agentName]: {
3290
- agentName,
3291
- status: "failed",
3292
- error,
3293
- total: previousStatus?.total,
3294
- processed: previousStatus?.processed,
3295
- sessions: sessionCount ?? previousStatus?.sessions ?? 0,
3296
- startedAt: previousStatus?.startedAt,
3297
- updatedAt: now,
3298
- completedAt: now
3299
- }
3300
- },
4217
+ phase: isActive ? this.activePhase(agentStatuses) : "idle",
4218
+ agentStatuses,
3301
4219
  updatedAt: now,
3302
4220
  completedAt: isActive ? void 0 : now
3303
4221
  });
@@ -3306,10 +4224,7 @@ var ScanStatusModel = class {
3306
4224
  const now = Date.now();
3307
4225
  return this.set({
3308
4226
  ...this.status,
3309
- active: false,
3310
4227
  phase: "idle",
3311
- pendingAgents: [],
3312
- scanningAgents: [],
3313
4228
  agentStatuses: Object.fromEntries(
3314
4229
  Object.entries(this.status.agentStatuses).map(([agentName, status]) => [
3315
4230
  agentName,
@@ -3359,7 +4274,8 @@ var ScanStatusModel = class {
3359
4274
  updatedAt: Date.now()
3360
4275
  });
3361
4276
  }
3362
- activePhase(pendingAgents, scanningAgents, agentStatuses) {
4277
+ activePhase(agentStatuses) {
4278
+ const { pendingAgents, scanningAgents } = this.agentLists(agentStatuses);
3363
4279
  if (pendingAgents.length === 0 && scanningAgents.length > 0 && scanningAgents.every((agentName) => {
3364
4280
  const status = agentStatuses[agentName]?.status;
3365
4281
  return status === "finalizing" || status === "publish-queued" || status === "publishing";
@@ -3368,6 +4284,22 @@ var ScanStatusModel = class {
3368
4284
  }
3369
4285
  return this.status.phase === "initializing" ? "initializing" : "scanning";
3370
4286
  }
4287
+ agentLists(agentStatuses) {
4288
+ const pendingAgents = [];
4289
+ const scanningAgents = [];
4290
+ const completedAgents = [];
4291
+ for (const [agentName, status] of Object.entries(agentStatuses)) {
4292
+ if (status.status === "pending") pendingAgents.push(agentName);
4293
+ else if (status.status === "complete") completedAgents.push(agentName);
4294
+ else if (status.status !== "failed") scanningAgents.push(agentName);
4295
+ }
4296
+ return { pendingAgents, scanningAgents, completedAgents };
4297
+ }
4298
+ hasActiveAgents(agentStatuses) {
4299
+ return Object.values(agentStatuses).some(
4300
+ (status) => status.status !== "complete" && status.status !== "failed"
4301
+ );
4302
+ }
3371
4303
  set(status) {
3372
4304
  this.status = status;
3373
4305
  return this.snapshot();
@@ -3580,7 +4512,7 @@ var SearchIndexPublisher = class {
3580
4512
  ...job,
3581
4513
  publicationId: details.publicationId
3582
4514
  }));
3583
- await (details.onStarted ? this.options.jobs.enqueue(context, publicationJobs, details.onStarted) : this.options.jobs.enqueue(context, publicationJobs));
4515
+ await (details.onProgress ? this.options.jobs.enqueue(context, publicationJobs, details.onStarted, details.onProgress) : details.onStarted ? this.options.jobs.enqueue(context, publicationJobs, details.onStarted) : this.options.jobs.enqueue(context, publicationJobs));
3584
4516
  } catch (error) {
3585
4517
  appLogger.error("session.publication.failed", {
3586
4518
  publication_id: details.publicationId,
@@ -3599,29 +4531,109 @@ var SearchIndexPublisher = class {
3599
4531
  }
3600
4532
  };
3601
4533
 
3602
- // src/agent-sync-engine.ts
3603
- var REFRESH_DEBOUNCE_MS = 200;
3604
- var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
3605
- var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
3606
- var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3607
- var CACHE_TRUNCATION_COVERAGE = 0.5;
3608
- var SOURCE_FAILURE_SUMMARY_MAX_LENGTH = 160;
3609
- function buildPersistenceDiff(previousSessions, nextSessions, candidateChangedIds = [], completeness = "complete", explicitRemovedSessionIds = []) {
3610
- const { changes, removedSessionIds } = computeSessionDiff(
3611
- previousSessions,
3612
- nextSessions,
3613
- candidateChangedIds,
3614
- sessionSignature
3615
- );
3616
- if (completeness === "complete") {
3617
- return { changedSessions: changes, removedSessionIds };
4534
+ // src/session-publication.ts
4535
+ var SessionPublicationCoordinator = class {
4536
+ constructor(indexPublisher, sessionIndex) {
4537
+ this.indexPublisher = indexPublisher;
4538
+ this.sessionIndex = sessionIndex;
4539
+ }
4540
+ indexPublisher;
4541
+ sessionIndex;
4542
+ listeners = /* @__PURE__ */ new Set();
4543
+ isShuttingDown = false;
4544
+ subscribe(listener) {
4545
+ this.listeners.add(listener);
4546
+ return () => this.listeners.delete(listener);
4547
+ }
4548
+ markShuttingDown() {
4549
+ this.isShuttingDown = true;
4550
+ }
4551
+ async commit(publication) {
4552
+ const publicationId = this.indexPublisher.publicationId(
4553
+ publication.context,
4554
+ publication.agentName
4555
+ );
4556
+ await this.indexPublisher.commitSearchIndex(publication.context, [publication.indexJob], {
4557
+ publicationId,
4558
+ agent: publication.agentName,
4559
+ ...publication.onPublishing ? { onStarted: publication.onPublishing } : {},
4560
+ ...publication.onPublicationProgress ? { onProgress: publication.onPublicationProgress } : {}
4561
+ });
4562
+ const diffStartedAt = performance.now();
4563
+ const event = this.sessionIndex.commitAgentSessions(
4564
+ publication.agentName,
4565
+ publication.sessions,
4566
+ publication.candidateChangedIds
4567
+ );
4568
+ const diffDuration = performance.now() - diffStartedAt;
4569
+ const result = { durableCommitted: true, event, diffDuration };
4570
+ this.runPostCommit(publication, "worker_commit", () => publication.stagedRun?.commit());
4571
+ this.runPostCommit(publication, "callback", () => publication.onCommitted?.(result));
4572
+ this.runPostCommit(
4573
+ publication,
4574
+ "notification",
4575
+ () => this.emit({
4576
+ agentName: publication.agentName,
4577
+ sessions: this.sessionIndex.snapshot().byAgent[publication.agentName] ?? [],
4578
+ event
4579
+ })
4580
+ );
4581
+ this.runPostCommit(
4582
+ publication,
4583
+ "logging",
4584
+ () => appLogger.info("session.publication.published", {
4585
+ publication_id: publicationId,
4586
+ context: publication.context,
4587
+ agent: publication.agentName,
4588
+ sessions: publication.sessions.length,
4589
+ has_event: event != null
4590
+ })
4591
+ );
4592
+ return result;
4593
+ }
4594
+ runPostCommit(publication, stage, action) {
4595
+ try {
4596
+ action();
4597
+ } catch (error) {
4598
+ try {
4599
+ appLogger.error("session.publication.post_commit_error", {
4600
+ context: publication.context,
4601
+ agent: publication.agentName,
4602
+ stage,
4603
+ error
4604
+ });
4605
+ } catch {
4606
+ }
4607
+ }
4608
+ }
4609
+ emit(change) {
4610
+ if (this.isShuttingDown) return;
4611
+ for (const listener of this.listeners) {
4612
+ try {
4613
+ listener(change);
4614
+ } catch (error) {
4615
+ appLogger.error("session.publication.post_commit_error", {
4616
+ agent: change.agentName,
4617
+ error
4618
+ });
4619
+ }
4620
+ }
3618
4621
  }
3619
- const explicitRemovals = new Set(explicitRemovedSessionIds);
4622
+ };
4623
+
4624
+ // src/agent-sync-engine.ts
4625
+ function countSessionUpdates(event) {
4626
+ if (!event) return { newSessions: 0, updatedSessions: 0, removedSessions: 0 };
3620
4627
  return {
3621
- changedSessions: changes,
3622
- removedSessionIds: removedSessionIds.filter((sessionId) => explicitRemovals.has(sessionId))
4628
+ newSessions: event.newSessionRefs.length,
4629
+ updatedSessions: event.changedSessionHeads.length - event.newSessionRefs.length,
4630
+ removedSessions: event.removedSessionRefs.length
3623
4631
  };
3624
4632
  }
4633
+ var REFRESH_DEBOUNCE_MS = 200;
4634
+ var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
4635
+ var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
4636
+ var SOURCE_FAILURE_SUMMARY_MAX_LENGTH = 160;
3625
4637
  function buildScanCompletion(completeness, sourceFailures) {
3626
4638
  if (sourceFailures.length === 0) return { completeness };
3627
4639
  const firstFailure = sourceFailures[0];
@@ -3632,9 +4644,6 @@ function buildScanCompletion(completeness, sourceFailures) {
3632
4644
  sourceFailureSummary: summary.length > SOURCE_FAILURE_SUMMARY_MAX_LENGTH ? `${summary.slice(0, SOURCE_FAILURE_SUMMARY_MAX_LENGTH - 1)}\u2026` : summary
3633
4645
  };
3634
4646
  }
3635
- function restoreAgentCacheMeta(agent, cached) {
3636
- agent.setSessionMetaMap(new Map(Object.entries(cached.meta)));
3637
- }
3638
4647
  var AgentSyncEngine = class {
3639
4648
  constructor(options) {
3640
4649
  this.options = options;
@@ -3649,6 +4658,17 @@ var AgentSyncEngine = class {
3649
4658
  snapshot: () => this.sessionIndex.snapshot(),
3650
4659
  readCachedSessions: (agentName) => this.readCachedSessionsOrWarn("search.index", agentName)
3651
4660
  });
4661
+ this.sessionPublication = new SessionPublicationCoordinator(
4662
+ this.indexPublisher,
4663
+ this.sessionIndex
4664
+ );
4665
+ this.backfillScheduler = new AgentBackfillScheduler({
4666
+ lifecycle: this.backfills,
4667
+ statusReporter: this.statusReporter,
4668
+ startupScanOptions: this.startupScanOptions(),
4669
+ runAttempt: (attempt) => this.runBackfill(attempt),
4670
+ scheduleRefresh: (agentName, delayMs) => this.scheduler.schedule(agentName, delayMs)
4671
+ });
3652
4672
  this.searchIndexMaintenance = new SearchIndexMaintenanceScheduler(
3653
4673
  this.searchIndexJobs,
3654
4674
  (status) => this.statusReporter.updateSearchIndexMaintenance(status)
@@ -3659,14 +4679,13 @@ var AgentSyncEngine = class {
3659
4679
  scheduler;
3660
4680
  sessionIndex = new LiveSessionIndex();
3661
4681
  backfills = new BackfillLifecycle();
3662
- cacheIntegrityValidUntilByAgent = /* @__PURE__ */ new Map();
3663
- sessionsChangedListeners = /* @__PURE__ */ new Set();
4682
+ backfillScheduler;
3664
4683
  statusReporter;
3665
4684
  indexPublisher;
4685
+ sessionPublication;
3666
4686
  searchIndexJobs;
3667
4687
  searchIndexMaintenance;
3668
4688
  backgroundRefreshTimer = null;
3669
- isShuttingDown = false;
3670
4689
  initialize(snapshot, options = {}) {
3671
4690
  this.sessionIndex.initialize(snapshot, options);
3672
4691
  this.lastRefreshAtByAgent.clear();
@@ -3691,8 +4710,7 @@ var AgentSyncEngine = class {
3691
4710
  return this.statusReporter.status();
3692
4711
  }
3693
4712
  subscribeSessionsChanged(listener) {
3694
- this.sessionsChangedListeners.add(listener);
3695
- return () => this.sessionsChangedListeners.delete(listener);
4713
+ return this.sessionPublication.subscribe(listener);
3696
4714
  }
3697
4715
  subscribeStatusChanged(listener) {
3698
4716
  return this.statusReporter.subscribe(listener);
@@ -3726,7 +4744,7 @@ var AgentSyncEngine = class {
3726
4744
  await this.scheduler.refresh(agentName);
3727
4745
  }
3728
4746
  async shutdown() {
3729
- this.isShuttingDown = true;
4747
+ this.sessionPublication.markShuttingDown();
3730
4748
  const schedulerSnapshot = this.scheduler.snapshot();
3731
4749
  const activeOperations = {
3732
4750
  agent_operations: schedulerSnapshot.activeOperations,
@@ -3742,10 +4760,9 @@ var AgentSyncEngine = class {
3742
4760
  clearTimeout(this.backgroundRefreshTimer);
3743
4761
  this.backgroundRefreshTimer = null;
3744
4762
  }
3745
- this.backfills.cancelAll();
4763
+ this.backfillScheduler.shutdown();
3746
4764
  this.searchIndexMaintenance.stop();
3747
4765
  this.statusReporter.markShuttingDown();
3748
- this.cacheIntegrityValidUntilByAgent.clear();
3749
4766
  const searchIndexSnapshot = this.searchIndexJobs.snapshot();
3750
4767
  appLogger.info("search_index.shutdown.started", {
3751
4768
  active_batch_id: searchIndexSnapshot.activeBatchId,
@@ -3761,19 +4778,16 @@ var AgentSyncEngine = class {
3761
4778
  pending_batches: stoppedSearchIndexSnapshot.pendingBatches
3762
4779
  });
3763
4780
  }
3764
- emitSessionsChanged(change) {
3765
- if (this.isShuttingDown) return;
3766
- for (const listener of this.sessionsChangedListeners) {
3767
- try {
3768
- listener(change);
3769
- } catch (error) {
3770
- this.reportPostCommitError("session.publication", change.agentName, error);
3771
- }
3772
- }
3773
- }
3774
4781
  reportPostCommitError(operation, agentName, error) {
3775
4782
  appLogger.error(`${operation}.post_commit_error`, { agent: agentName, error });
3776
4783
  }
4784
+ commitRefreshChangeCheck(agentName, commit) {
4785
+ try {
4786
+ commit?.();
4787
+ } catch (error) {
4788
+ this.reportPostCommitError("scan.refresh", agentName, error);
4789
+ }
4790
+ }
3777
4791
  finishCommittedAgentScan(agentName, completion) {
3778
4792
  try {
3779
4793
  this.statusReporter.finishAgentScan(agentName, completion);
@@ -3789,62 +4803,50 @@ var AgentSyncEngine = class {
3789
4803
  async performRefresh(agentName) {
3790
4804
  this.statusReporter.beginAgentScan(agentName);
3791
4805
  const startedAt = performance.now();
3792
- let failed = false;
3793
- let durableCommitted = false;
3794
4806
  let cached = null;
3795
- let result = "failed";
3796
- let completion = { completeness: "complete" };
4807
+ let refresh = {
4808
+ result: "failed",
4809
+ completion: { completeness: "complete" }
4810
+ };
3797
4811
  try {
3798
4812
  if (this.findAgent(agentName))
3799
4813
  cached = this.readCachedSessionsOrWarn("scan.refresh", agentName);
3800
- const refresh = await this.runRefresh(agentName, cached, startedAt, (committedCompletion) => {
3801
- durableCommitted = true;
3802
- completion = committedCompletion;
3803
- result = "committed";
3804
- });
3805
- result = refresh.result;
3806
- completion = refresh.completion;
4814
+ refresh = await this.runRefresh(agentName, cached, startedAt);
3807
4815
  } catch (error) {
3808
- if (durableCommitted) {
3809
- this.reportPostCommitError("scan.refresh", agentName, error);
3810
- result = "committed";
4816
+ const failure = toError2(error);
4817
+ if (failure instanceof AgentUnavailableDuringScanError) {
4818
+ appLogger.warn("scan.refresh.worker_agent_unavailable", {
4819
+ agent: agentName,
4820
+ error: failure.message
4821
+ });
3811
4822
  } else {
3812
- this.options.workerRunner.discard?.(agentName);
3813
- failed = true;
3814
- const failure = toError2(error);
3815
- if (failure instanceof AgentUnavailableDuringScanError) {
3816
- appLogger.warn("scan.refresh.worker_agent_unavailable", {
3817
- agent: agentName,
3818
- error: failure.message
3819
- });
3820
- } else {
3821
- appLogger.error("scan.refresh.error", { agent: agentName, error });
3822
- }
3823
- console.error(`[${agentName}] Session refresh failed:`, error);
3824
- this.statusReporter.failAgent(agentName, failure.message);
4823
+ appLogger.error("scan.refresh.error", { agent: agentName, error });
3825
4824
  }
4825
+ console.error(`[${agentName}] Session refresh failed:`, error);
4826
+ this.statusReporter.failAgent(agentName, failure.message);
3826
4827
  }
3827
4828
  try {
3828
- if (!failed) {
3829
- if (durableCommitted) this.finishCommittedAgentScan(agentName, completion);
3830
- else this.statusReporter.finishAgentScan(agentName, completion);
4829
+ if (refresh.result !== "failed") {
4830
+ if (refresh.result === "committed")
4831
+ this.finishCommittedAgentScan(agentName, refresh.completion);
4832
+ else this.statusReporter.finishAgentScan(agentName, refresh.completion);
3831
4833
  }
3832
4834
  } catch (error) {
3833
- if (!durableCommitted) throw error;
4835
+ if (refresh.result !== "committed") throw error;
3834
4836
  this.reportPostCommitError("scan.refresh", agentName, error);
3835
4837
  }
3836
4838
  try {
3837
4839
  const agent = this.findAgent(agentName);
3838
- if (agent && this.needsBackfill(agent, cached, failed || result === "committed")) {
4840
+ if (agent && this.backfillScheduler.needsBackfill(agent)) {
3839
4841
  this.enqueueBackfill(agentName);
3840
4842
  }
3841
4843
  if (agent) this.searchIndexMaintenance.enqueue(agentName);
3842
4844
  } catch (error) {
3843
4845
  appLogger.warn("scan.refresh.backfill_probe_failed", { agent: agentName, error });
3844
4846
  }
3845
- return result;
4847
+ return refresh.result;
3846
4848
  }
3847
- async runRefresh(agentName, cached, startedAt, onDurableCommit) {
4849
+ async runRefresh(agentName, cached, startedAt) {
3848
4850
  const pendingPathCount = this.scheduler.takePendingSignalCount(agentName);
3849
4851
  const agent = this.findAgent(agentName);
3850
4852
  if (!agent) {
@@ -3854,8 +4856,8 @@ var AgentSyncEngine = class {
3854
4856
  const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
3855
4857
  const refreshBaseline = cached?.sessions ?? previousSessions;
3856
4858
  const cacheTimestamp = cached?.timestamp ?? this.lastRefreshAtByAgent.get(agentName) ?? 0;
3857
- if (cached) restoreAgentCacheMeta(agent, cached);
3858
- const durableMeta = new Map(agent.getSessionMetaMap());
4859
+ if (cached) agent.restoreSessionCacheMeta(cached.meta);
4860
+ const durableMeta = agent.snapshotSessionCacheMeta();
3859
4861
  const durableLastRefreshAt = this.lastRefreshAtByAgent.get(agentName);
3860
4862
  const initialization = readAgentCacheInitialization(agentName);
3861
4863
  if (initialization.status === "failed") {
@@ -3866,28 +4868,33 @@ var AgentSyncEngine = class {
3866
4868
  return { result: "unchanged", completion: { completeness: "complete" } };
3867
4869
  }
3868
4870
  const isInitialized = initialization.value;
3869
- const availabilityStartedAt = performance.now();
3870
- const isAvailable = agent.isAvailable();
3871
- const availabilityDuration = performance.now() - availabilityStartedAt;
4871
+ const refresh = await selectAgentRefresh(agent, {
4872
+ initialized: isInitialized,
4873
+ sinceTimestamp: cacheTimestamp,
4874
+ cachedSessions: refreshBaseline
4875
+ });
4876
+ const availabilityDuration = refresh.availabilityDurationMs;
3872
4877
  let strategyResult;
3873
- if (!isAvailable) {
4878
+ if (refresh.kind === "unavailable") {
3874
4879
  strategyResult = this.refreshUnavailableAgent(agentName);
3875
- } else if (!isInitialized) {
4880
+ } else if (refresh.kind === "initialize") {
3876
4881
  strategyResult = await this.initializeAgent(agent, previousSessions);
3877
- } else if (cached && agent instanceof FileSystemSessionSource) {
3878
- strategyResult = await this.syncAgentSources(agent, cached, startedAt);
3879
- } else if (refreshBaseline.length > 0) {
3880
- strategyResult = await this.refreshChangedAgent(
4882
+ } else if (refresh.kind === "synchronize") {
4883
+ strategyResult = await this.syncAgentSources(
3881
4884
  agent,
3882
- refreshBaseline,
3883
- cacheTimestamp,
4885
+ cached ?? {
4886
+ sessions: refreshBaseline,
4887
+ meta: durableMeta
4888
+ },
3884
4889
  startedAt
3885
4890
  );
3886
4891
  } else {
3887
- strategyResult = await this.scanAgentFully(agent, previousSessions);
4892
+ strategyResult = await this.refreshChangedAgent(agent, refresh, refreshBaseline, startedAt);
3888
4893
  }
4894
+ const stagedRun = strategyResult.workerRun;
3889
4895
  if (strategyResult.status === "unchanged") {
3890
- this.options.workerRunner.commit?.(agentName);
4896
+ stagedRun?.commit();
4897
+ this.commitRefreshChangeCheck(agentName, strategyResult.commitChangeCheck);
3891
4898
  return {
3892
4899
  result: "unchanged",
3893
4900
  completion: buildScanCompletion(strategyResult.completeness, strategyResult.sourceFailures)
@@ -3895,14 +4902,17 @@ var AgentSyncEngine = class {
3895
4902
  }
3896
4903
  const nextSessions = attachMissingProjectIdentities(strategyResult.nextSessions);
3897
4904
  const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
3898
- const persistenceDiff = strategyResult.persistenceDiff;
3899
- const publicationSessions = strategyResult.fullScanSessions ?? nextSessions;
3900
- const publicationSessionIds = new Set(publicationSessions.map((session) => session.id));
4905
+ const persistenceDiff = strategyResult.publication.kind === "changes" ? strategyResult.publication.diff : null;
4906
+ const publicationSessions = strategyResult.publication.kind === "full" ? strategyResult.publication.sessions : nextSessions;
4907
+ const explicitRemovedSessionIds = strategyResult.publication.kind === "full" ? strategyResult.publication.explicitRemovedSessionIds : [];
4908
+ const publicationSessionIds = new Set(
4909
+ publicationSessions.map((session) => session.reference.sessionId)
4910
+ );
3901
4911
  const missingBaselineSessions = refreshBaseline.reduce(
3902
- (count, session) => count + Number(!publicationSessionIds.has(session.id)),
4912
+ (count, session) => count + Number(!publicationSessionIds.has(session.reference.sessionId)),
3903
4913
  0
3904
4914
  );
3905
- const replacementDeleteCandidates = persistenceDiff ? persistenceDiff.removedSessionIds.length : strategyResult.completeness === "complete" ? missingBaselineSessions : strategyResult.explicitRemovedSessionIds.length;
4915
+ const replacementDeleteCandidates = persistenceDiff ? persistenceDiff.removedSessionIds.length : strategyResult.completeness === "complete" ? missingBaselineSessions : explicitRemovedSessionIds.length;
3906
4916
  appLogger.info("scan.refresh.persistence_candidate", {
3907
4917
  agent: agentName,
3908
4918
  scope_from: strategyResult.scope.from,
@@ -3912,7 +4922,7 @@ var AgentSyncEngine = class {
3912
4922
  payload_sessions: publicationSessions.length,
3913
4923
  delete_candidates: replacementDeleteCandidates
3914
4924
  });
3915
- const changedSessionIds = persistenceDiff ? new Set(persistenceDiff.changedSessions.map(({ session }) => session.id)) : void 0;
4925
+ const changedSessionIds = persistenceDiff ? new Set(persistenceDiff.changedSessions.map(({ session }) => session.reference.sessionId)) : void 0;
3916
4926
  const persistStartedAt = performance.now();
3917
4927
  const persistentJob = persistenceDiff ? {
3918
4928
  kind: "changes",
@@ -3929,7 +4939,7 @@ var AgentSyncEngine = class {
3929
4939
  sessions: publicationSessions,
3930
4940
  meta: buildAgentCacheMeta(agent),
3931
4941
  completeness: strategyResult.completeness,
3932
- removedSessionIds: strategyResult.explicitRemovedSessionIds,
4942
+ removedSessionIds: explicitRemovedSessionIds,
3933
4943
  saveCache: true,
3934
4944
  ...searchIndexOptions ? { searchIndexOptions } : {}
3935
4945
  };
@@ -3938,59 +4948,59 @@ var AgentSyncEngine = class {
3938
4948
  strategyResult.sourceFailures
3939
4949
  );
3940
4950
  this.statusReporter.queueAgentPublication(agentName);
3941
- let publicationCommitted = false;
3942
4951
  let publication;
3943
4952
  try {
3944
- publication = await this.commitSessionPublication({
4953
+ publication = await this.sessionPublication.commit({
3945
4954
  context: "scan.refresh",
3946
4955
  agentName,
3947
4956
  sessions: nextSessions,
3948
- candidateChangedIds: strategyResult.preciseChangedIds ?? [],
4957
+ candidateChangedIds: strategyResult.publication.kind === "changes" ? strategyResult.publication.candidateChangedIds : [],
3949
4958
  indexJob: persistentJob,
3950
- onPublishing: () => this.statusReporter.beginAgentPublishing(agentName),
3951
- onCommitted: () => {
3952
- publicationCommitted = true;
3953
- onDurableCommit(completion);
3954
- }
4959
+ stagedRun,
4960
+ onPublishing: () => this.statusReporter.beginAgentPublishing(agentName)
3955
4961
  });
3956
4962
  } catch (error) {
3957
- if (!publicationCommitted) {
3958
- agent.setSessionMetaMap(durableMeta);
3959
- if (durableLastRefreshAt == null) this.lastRefreshAtByAgent.delete(agentName);
3960
- else this.lastRefreshAtByAgent.set(agentName, durableLastRefreshAt);
3961
- }
4963
+ stagedRun?.discard();
4964
+ agent.restoreSessionCacheMeta(durableMeta);
4965
+ if (durableLastRefreshAt == null) this.lastRefreshAtByAgent.delete(agentName);
4966
+ else this.lastRefreshAtByAgent.set(agentName, durableLastRefreshAt);
3962
4967
  throw error;
3963
4968
  }
3964
- this.options.workerRunner.commit?.(agentName);
4969
+ this.commitRefreshChangeCheck(agentName, strategyResult.commitChangeCheck);
3965
4970
  const persistDuration = performance.now() - persistStartedAt;
3966
- logSearchIndexSync("scan.refresh", null, { pending_paths: pendingPathCount });
3967
4971
  const totalDurationMs = performance.now() - startedAt;
3968
- this.scheduler.recordRefreshDuration(agentName, totalDurationMs);
3969
- appLogger.info(
3970
- strategyResult.sourceFailures.length > 0 ? "scan.refresh.partial" : "scan.refresh.done",
3971
- {
3972
- agent: agentName,
3973
- duration_ms: Math.round(totalDurationMs),
3974
- sessions: nextSessions.length,
3975
- new_sessions: publication.event?.newSessions ?? 0,
3976
- updated_sessions: publication.event?.updatedSessions ?? 0,
3977
- removed_sessions: publication.event?.removedSessions ?? 0,
3978
- pending_paths: pendingPathCount,
3979
- availability_ms: Math.round(availabilityDuration),
3980
- check_ms: Math.round(strategyResult.checkDuration),
3981
- scan_ms: Math.round(strategyResult.scanDuration),
3982
- diff_ms: Math.round(publication.diffDuration),
3983
- persist_ms: Math.round(persistDuration),
3984
- search_index_ms: Math.round(persistDuration),
3985
- persistent_index_worker_job: persistentJob.kind,
3986
- failed_sources: strategyResult.sourceFailures.length
4972
+ try {
4973
+ logSearchIndexSync("scan.refresh", null, { pending_paths: pendingPathCount });
4974
+ this.scheduler.recordRefreshDuration(agentName, totalDurationMs);
4975
+ const sessionUpdateCounts = countSessionUpdates(publication.event);
4976
+ appLogger.info(
4977
+ strategyResult.sourceFailures.length > 0 ? "scan.refresh.partial" : "scan.refresh.done",
4978
+ {
4979
+ agent: agentName,
4980
+ duration_ms: Math.round(totalDurationMs),
4981
+ sessions: nextSessions.length,
4982
+ new_sessions: sessionUpdateCounts.newSessions,
4983
+ updated_sessions: sessionUpdateCounts.updatedSessions,
4984
+ removed_sessions: sessionUpdateCounts.removedSessions,
4985
+ pending_paths: pendingPathCount,
4986
+ availability_ms: Math.round(availabilityDuration),
4987
+ check_ms: Math.round(strategyResult.checkDuration),
4988
+ scan_ms: Math.round(strategyResult.scanDuration),
4989
+ diff_ms: Math.round(publication.diffDuration),
4990
+ persist_ms: Math.round(persistDuration),
4991
+ search_index_ms: Math.round(persistDuration),
4992
+ persistent_index_worker_job: persistentJob.kind,
4993
+ failed_sources: strategyResult.sourceFailures.length
4994
+ }
4995
+ );
4996
+ if (strategyResult.sourceFailures.length > 0) {
4997
+ appLogger.warn("scan.refresh.source_failures", {
4998
+ agent: agentName,
4999
+ failures: strategyResult.sourceFailures
5000
+ });
3987
5001
  }
3988
- );
3989
- if (strategyResult.sourceFailures.length > 0) {
3990
- appLogger.warn("scan.refresh.source_failures", {
3991
- agent: agentName,
3992
- failures: strategyResult.sourceFailures
3993
- });
5002
+ } catch (error) {
5003
+ this.reportPostCommitError("scan.refresh", agentName, error);
3994
5004
  }
3995
5005
  return { result: "committed", completion };
3996
5006
  }
@@ -4001,184 +5011,242 @@ var AgentSyncEngine = class {
4001
5011
  * empty database.
4002
5012
  */
4003
5013
  refreshUnavailableAgent(agentName) {
4004
- this.lastRefreshAtByAgent.set(agentName, Date.now());
4005
5014
  const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
4006
5015
  if (previousSessions.length > 0) {
4007
5016
  appLogger.warn("scan.refresh.agent_unavailable", {
4008
5017
  agent: agentName,
4009
5018
  retained_sessions: previousSessions.length
4010
5019
  });
4011
- return this.refreshStrategyResult(previousSessions, "partial", {}, { status: "unchanged" });
5020
+ return {
5021
+ ...this.refreshStrategyBase(previousSessions, "partial", {}),
5022
+ status: "unchanged"
5023
+ };
4012
5024
  }
4013
- return this.refreshStrategyResult([], "partial", {});
5025
+ return {
5026
+ ...this.refreshStrategyBase([], "partial", {}),
5027
+ status: "continue",
5028
+ publication: { kind: "full", sessions: [], explicitRemovedSessionIds: [] }
5029
+ };
4014
5030
  }
4015
5031
  async initializeAgent(agent, previousSessions) {
4016
5032
  this.statusReporter.setScanPhase("initializing");
4017
5033
  const scanStartedAt = performance.now();
4018
5034
  const scope = this.startupScanOptions();
4019
- const result = await this.runWorker(agent, previousSessions, { kind: "full-scan" }, scope);
4020
- agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
4021
- const sessions = attachMissingProjectIdentities(result.sessions);
4022
- this.lastRefreshAtByAgent.set(agent.name, Date.now());
4023
- return this.refreshStrategyResult(sessions, result.completeness, scope, {
4024
- fullScanSessions: sessions,
4025
- explicitRemovedSessionIds: result.explicitRemovedSessionIds,
4026
- scanDuration: performance.now() - scanStartedAt,
4027
- sourceFailures: result.sourceFailures ?? []
4028
- });
5035
+ const workerRun = await this.runWorker(agent, previousSessions, { kind: "full-scan" }, scope);
5036
+ try {
5037
+ const { result } = workerRun;
5038
+ agent.restoreSessionCacheMeta(result.meta);
5039
+ const sessions = attachMissingProjectIdentities(result.sessions);
5040
+ this.lastRefreshAtByAgent.set(agent.name, Date.now());
5041
+ return {
5042
+ ...this.refreshStrategyBase(sessions, result.completeness, scope, {
5043
+ scanDuration: performance.now() - scanStartedAt,
5044
+ sourceFailures: result.sourceFailures ?? [],
5045
+ workerRun
5046
+ }),
5047
+ status: "continue",
5048
+ publication: {
5049
+ kind: "full",
5050
+ sessions,
5051
+ explicitRemovedSessionIds: result.explicitRemovedSessionIds
5052
+ }
5053
+ };
5054
+ } catch (error) {
5055
+ workerRun.discard();
5056
+ throw error;
5057
+ }
4029
5058
  }
4030
- async syncAgentSources(agent, cached, refreshStartedAt) {
5059
+ async syncAgentSources(agent, baseline, refreshStartedAt) {
4031
5060
  const scanStartedAt = performance.now();
4032
5061
  const scope = this.startupScanOptions();
4033
- const result = await this.runWorker(agent, cached.sessions, { kind: "source-refresh" }, scope, {
4034
- meta: cached.meta
4035
- });
4036
- agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
4037
- const sessions = attachMissingProjectIdentities(result.sessions);
4038
- const preciseChangedIds = result.changedIds ?? [];
4039
- const persistenceDiff = buildPersistenceDiff(cached.sessions, sessions, preciseChangedIds);
4040
- this.lastRefreshAtByAgent.set(agent.name, Date.now());
4041
- if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0 && (result.sourceFailures?.length ?? 0) === 0) {
4042
- this.logUnchangedRefresh(agent.name, refreshStartedAt);
4043
- return this.refreshStrategyResult(sessions, result.completeness, scope, {
4044
- status: "unchanged",
4045
- scanDuration: performance.now() - scanStartedAt
5062
+ const workerRun = await this.runWorker(
5063
+ agent,
5064
+ baseline.sessions,
5065
+ { kind: "source-refresh" },
5066
+ scope,
5067
+ {
5068
+ meta: baseline.meta
5069
+ }
5070
+ );
5071
+ try {
5072
+ const { result } = workerRun;
5073
+ agent.restoreSessionCacheMeta(result.meta);
5074
+ const sessions = attachMissingProjectIdentities(result.sessions);
5075
+ const preciseChangedIds = result.changedIds ?? [];
5076
+ const persistenceDiff = buildSessionPersistenceDiff(baseline.sessions, sessions, {
5077
+ candidateChangedIds: preciseChangedIds
4046
5078
  });
5079
+ this.lastRefreshAtByAgent.set(agent.name, Date.now());
5080
+ if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0 && (result.sourceFailures?.length ?? 0) === 0) {
5081
+ this.logUnchangedRefresh(agent.name, refreshStartedAt);
5082
+ return {
5083
+ ...this.refreshStrategyBase(sessions, result.completeness, scope, {
5084
+ scanDuration: performance.now() - scanStartedAt,
5085
+ workerRun
5086
+ }),
5087
+ status: "unchanged"
5088
+ };
5089
+ }
5090
+ return {
5091
+ ...this.refreshStrategyBase(sessions, result.completeness, scope, {
5092
+ scanDuration: performance.now() - scanStartedAt,
5093
+ sourceFailures: result.sourceFailures ?? [],
5094
+ workerRun
5095
+ }),
5096
+ status: "continue",
5097
+ publication: {
5098
+ kind: "changes",
5099
+ diff: persistenceDiff,
5100
+ candidateChangedIds: preciseChangedIds
5101
+ }
5102
+ };
5103
+ } catch (error) {
5104
+ workerRun.discard();
5105
+ throw error;
4047
5106
  }
4048
- return this.refreshStrategyResult(sessions, result.completeness, scope, {
4049
- preciseChangedIds,
4050
- persistenceDiff,
4051
- scanDuration: performance.now() - scanStartedAt,
4052
- sourceFailures: result.sourceFailures ?? []
4053
- });
4054
5107
  }
4055
- async refreshChangedAgent(agent, baseline, cacheTimestamp, refreshStartedAt) {
5108
+ async refreshChangedAgent(agent, refresh, baseline, refreshStartedAt) {
4056
5109
  const scope = this.startupScanOptions();
4057
- const checkStartedAt = performance.now();
4058
- const checkResult = await Promise.resolve(agent.checkForChanges(cacheTimestamp, baseline));
4059
- const checkDuration = performance.now() - checkStartedAt;
4060
- if (checkResult.status === "failed") {
5110
+ if (refresh.kind === "failed") {
4061
5111
  appLogger.warn("scan.refresh.change_check_failed", {
4062
5112
  agent: agent.name,
4063
- source_path: checkResult.failure.sourcePath,
4064
- error_class: checkResult.failure.errorClass,
4065
- message: checkResult.failure.message
5113
+ source_path: refresh.failure.sourcePath,
5114
+ error_class: refresh.failure.errorClass,
5115
+ message: refresh.failure.message
4066
5116
  });
4067
- return this.refreshStrategyResult(
4068
- baseline,
4069
- "partial",
4070
- {},
4071
- { status: "unchanged", checkDuration }
4072
- );
4073
- }
4074
- if (!checkResult.hasChanges) {
4075
- const scanStartedAt2 = performance.now();
4076
- const result = await this.runWorker(agent, baseline, { kind: "recompute-derived" }, {});
4077
- agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
4078
- const sessions2 = attachMissingProjectIdentities(result.sessions);
4079
- agent.commitChangeCheck();
4080
- this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
4081
- const persistenceDiff = buildPersistenceDiff(baseline, sessions2);
4082
- if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0) {
4083
- this.logUnchangedRefresh(agent.name, refreshStartedAt);
4084
- return this.refreshStrategyResult(
4085
- sessions2,
4086
- result.completeness,
5117
+ return {
5118
+ ...this.refreshStrategyBase(
5119
+ baseline,
5120
+ "partial",
4087
5121
  {},
4088
5122
  {
4089
- status: "unchanged",
4090
- checkDuration,
4091
- scanDuration: performance.now() - scanStartedAt2
5123
+ checkDuration: refresh.checkDurationMs
4092
5124
  }
4093
- );
4094
- }
4095
- return this.refreshStrategyResult(
4096
- sessions2,
4097
- result.completeness,
4098
- {},
4099
- {
4100
- persistenceDiff,
4101
- checkDuration,
4102
- scanDuration: performance.now() - scanStartedAt2,
4103
- sourceFailures: result.sourceFailures ?? []
5125
+ ),
5126
+ status: "unchanged"
5127
+ };
5128
+ }
5129
+ const { check: checkResult, source } = refresh;
5130
+ const checkDuration = refresh.checkDurationMs;
5131
+ if (refresh.kind === "recompute-derived") {
5132
+ const scanStartedAt2 = performance.now();
5133
+ const workerRun = await this.runWorker(agent, baseline, { kind: "recompute-derived" }, {});
5134
+ try {
5135
+ const { result } = workerRun;
5136
+ agent.restoreSessionCacheMeta(result.meta);
5137
+ const sessions2 = attachMissingProjectIdentities(result.sessions);
5138
+ this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
5139
+ const persistenceDiff = buildSessionPersistenceDiff(baseline, sessions2);
5140
+ if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0) {
5141
+ this.logUnchangedRefresh(agent.name, refreshStartedAt);
5142
+ return {
5143
+ ...this.refreshStrategyBase(
5144
+ sessions2,
5145
+ result.completeness,
5146
+ {},
5147
+ {
5148
+ checkDuration,
5149
+ scanDuration: performance.now() - scanStartedAt2,
5150
+ workerRun,
5151
+ commitChangeCheck: () => commitAgentRefreshCheck(refresh)
5152
+ }
5153
+ ),
5154
+ status: "unchanged"
5155
+ };
4104
5156
  }
4105
- );
5157
+ return {
5158
+ ...this.refreshStrategyBase(
5159
+ sessions2,
5160
+ result.completeness,
5161
+ {},
5162
+ {
5163
+ checkDuration,
5164
+ scanDuration: performance.now() - scanStartedAt2,
5165
+ sourceFailures: result.sourceFailures ?? [],
5166
+ workerRun,
5167
+ commitChangeCheck: () => commitAgentRefreshCheck(refresh)
5168
+ }
5169
+ ),
5170
+ status: "continue",
5171
+ publication: { kind: "changes", diff: persistenceDiff, candidateChangedIds: [] }
5172
+ };
5173
+ } catch (error) {
5174
+ workerRun.discard();
5175
+ throw error;
5176
+ }
4106
5177
  }
4107
- const preciseChangedIds = checkResult.changedIds ?? null;
4108
5178
  const scanStartedAt = performance.now();
4109
- if (preciseChangedIds === null) {
4110
- const result = await this.runWorker(agent, baseline, { kind: "full-scan" }, scope);
4111
- agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
4112
- const sessions2 = attachMissingProjectIdentities(result.sessions);
4113
- agent.commitChangeCheck();
4114
- this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
4115
- return this.refreshStrategyResult(sessions2, result.completeness, scope, {
4116
- // Meta-only changes (e.g. a pricing capture epoch bump) leave the head
4117
- // signature intact; without the worker-reported ids they would never
4118
- // persist and checkForChanges would rescan on every startup.
4119
- persistenceDiff: buildPersistenceDiff(
4120
- baseline,
4121
- sessions2,
4122
- result.changedIds ?? [],
4123
- result.completeness,
4124
- result.explicitRemovedSessionIds
4125
- ),
4126
- checkDuration,
4127
- scanDuration: performance.now() - scanStartedAt,
4128
- sourceFailures: result.sourceFailures ?? []
4129
- });
5179
+ if (refresh.kind === "full-scan") {
5180
+ const workerRun = await this.runWorker(agent, baseline, { kind: "full-scan" }, scope);
5181
+ try {
5182
+ const { result } = workerRun;
5183
+ agent.restoreSessionCacheMeta(result.meta);
5184
+ const sessions2 = attachMissingProjectIdentities(result.sessions);
5185
+ this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
5186
+ return {
5187
+ ...this.refreshStrategyBase(sessions2, result.completeness, scope, {
5188
+ checkDuration,
5189
+ scanDuration: performance.now() - scanStartedAt,
5190
+ sourceFailures: result.sourceFailures ?? [],
5191
+ workerRun,
5192
+ commitChangeCheck: () => commitAgentRefreshCheck(refresh)
5193
+ }),
5194
+ status: "continue",
5195
+ publication: {
5196
+ kind: "changes",
5197
+ // Meta-only changes (e.g. a pricing capture epoch bump) leave the head
5198
+ // signature intact; without the worker-reported ids they would never
5199
+ // persist and checkForChanges would rescan on every startup.
5200
+ diff: buildSessionPersistenceDiff(baseline, sessions2, {
5201
+ candidateChangedIds: result.changedIds ?? [],
5202
+ completeness: result.completeness,
5203
+ explicitRemovedSessionIds: result.explicitRemovedSessionIds
5204
+ }),
5205
+ candidateChangedIds: []
5206
+ }
5207
+ };
5208
+ } catch (error) {
5209
+ workerRun.discard();
5210
+ throw error;
5211
+ }
4130
5212
  }
4131
- this.options.workerRunner.discard?.(agent.name);
5213
+ const preciseChangedIds = refresh.check.changedIds;
5214
+ this.options.workerRunner.reset(agent.name);
4132
5215
  const sessions = attachMissingProjectIdentities(
4133
5216
  await Promise.resolve(
4134
- agent.incrementalScan(baseline, preciseChangedIds, checkResult.refs, scope)
5217
+ source.incrementalScan(baseline, preciseChangedIds, checkResult.refs, scope)
4135
5218
  )
4136
5219
  );
4137
- agent.commitChangeCheck();
4138
5220
  this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
4139
5221
  const sourceFailures = checkResult.sourceFailures ?? [];
4140
- const completeness = scope.from == null && scope.to == null && sourceFailures.length === 0 ? "complete" : "partial";
4141
- return this.refreshStrategyResult(sessions, completeness, scope, {
4142
- preciseChangedIds,
4143
- persistenceDiff: buildPersistenceDiff(
4144
- baseline,
4145
- sessions,
4146
- preciseChangedIds,
4147
- completeness,
4148
- preciseChangedIds
4149
- ),
4150
- checkDuration,
4151
- scanDuration: performance.now() - scanStartedAt,
4152
- sourceFailures
4153
- });
4154
- }
4155
- async scanAgentFully(agent, previousSessions) {
4156
- const scanStartedAt = performance.now();
4157
- const scope = this.startupScanOptions();
4158
- const result = await this.runWorker(agent, previousSessions, { kind: "full-scan" }, scope);
4159
- agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
4160
- const sessions = attachMissingProjectIdentities(result.sessions);
4161
- this.lastRefreshAtByAgent.set(agent.name, Date.now());
4162
- return this.refreshStrategyResult(sessions, result.completeness, scope, {
4163
- fullScanSessions: sessions,
4164
- explicitRemovedSessionIds: result.explicitRemovedSessionIds,
4165
- scanDuration: performance.now() - scanStartedAt,
4166
- sourceFailures: result.sourceFailures ?? []
4167
- });
4168
- }
4169
- refreshStrategyResult(nextSessions, completeness, scope, overrides = {}) {
5222
+ const completeness = resolveSessionSnapshotCompleteness(scope, sourceFailures);
4170
5223
  return {
5224
+ ...this.refreshStrategyBase(sessions, completeness, scope, {
5225
+ checkDuration,
5226
+ scanDuration: performance.now() - scanStartedAt,
5227
+ sourceFailures,
5228
+ commitChangeCheck: () => commitAgentRefreshCheck(refresh)
5229
+ }),
4171
5230
  status: "continue",
5231
+ publication: {
5232
+ kind: "changes",
5233
+ diff: buildSessionPersistenceDiff(baseline, sessions, {
5234
+ candidateChangedIds: preciseChangedIds,
5235
+ completeness,
5236
+ explicitRemovedSessionIds: preciseChangedIds
5237
+ }),
5238
+ candidateChangedIds: preciseChangedIds
5239
+ }
5240
+ };
5241
+ }
5242
+ refreshStrategyBase(nextSessions, completeness, scope, overrides = {}) {
5243
+ return {
4172
5244
  nextSessions,
4173
- fullScanSessions: null,
4174
- preciseChangedIds: null,
4175
- persistenceDiff: null,
4176
5245
  checkDuration: 0,
4177
5246
  scanDuration: 0,
4178
5247
  sourceFailures: [],
4179
5248
  completeness,
4180
5249
  scope,
4181
- explicitRemovedSessionIds: [],
4182
5250
  ...overrides
4183
5251
  };
4184
5252
  }
@@ -4222,85 +5290,8 @@ var AgentSyncEngine = class {
4222
5290
  }
4223
5291
  return outcome.value;
4224
5292
  }
4225
- needsBackfill(agent, cached, reloadCached = false) {
4226
- const startupScanOptions = this.startupScanOptions();
4227
- if (startupScanOptions.from == null && startupScanOptions.to == null) return false;
4228
- const now = Date.now();
4229
- if ((this.cacheIntegrityValidUntilByAgent.get(agent.name) ?? 0) >= now) return false;
4230
- const lastSync = readAgentLastFullSyncAt(agent.name);
4231
- if (lastSync.status === "failed") {
4232
- appLogger.warn("scan.backfill.cache_state_unavailable", {
4233
- agent: agent.name,
4234
- state: "last_full_sync"
4235
- });
4236
- return false;
4237
- }
4238
- const lastSyncAt = lastSync.value;
4239
- if (lastSyncAt == null || now - lastSyncAt > BACKFILL_INTERVAL_MS) {
4240
- return agent.isAvailable();
4241
- }
4242
- if (!(agent instanceof FileSystemSessionSource)) return false;
4243
- if (!agent.isAvailable()) return false;
4244
- let cachedSessions = cached;
4245
- if (reloadCached || cached === void 0) {
4246
- const outcome = readCachedSessions(agent.name);
4247
- if (outcome.status === "failed") {
4248
- appLogger.warn("scan.backfill.cache_state_unavailable", {
4249
- agent: agent.name,
4250
- state: "cached_sessions"
4251
- });
4252
- return false;
4253
- }
4254
- cachedSessions = outcome.value;
4255
- }
4256
- const sourceCount = agent.listSessionSources().length;
4257
- const cachedCount = cachedSessions?.sessions.length ?? 0;
4258
- this.cacheIntegrityValidUntilByAgent.set(agent.name, lastSyncAt + BACKFILL_INTERVAL_MS);
4259
- if (sourceCount > 0 && cachedCount / sourceCount < CACHE_TRUNCATION_COVERAGE) {
4260
- appLogger.warn("scan.backfill.cache_truncated", {
4261
- agent: agent.name,
4262
- cached_sessions: cachedCount,
4263
- source_files: sourceCount,
4264
- last_sync_at: lastSyncAt
4265
- });
4266
- return true;
4267
- }
4268
- return false;
4269
- }
4270
5293
  enqueueBackfill(agentName) {
4271
- if (this.isShuttingDown || !this.backfills.enqueue(agentName)) return;
4272
- this.statusReporter.publishBackfillStatus();
4273
- this.pumpBackfillQueue();
4274
- }
4275
- pumpBackfillQueue() {
4276
- if (this.isShuttingDown) return;
4277
- const attempt = this.backfills.startNext();
4278
- if (!attempt) return;
4279
- this.statusReporter.publishBackfillStatus();
4280
- void this.runBackfill(attempt).then((result) => this.completeBackfillAttempt(attempt, result)).catch((error) => this.rejectBackfillAttempt(attempt, error));
4281
- }
4282
- completeBackfillAttempt(attempt, result) {
4283
- if (this.isShuttingDown) return;
4284
- const current = this.backfills.stateFor(attempt.agentName);
4285
- if (current?.status === "running" && current.attemptId === attempt.attemptId) {
4286
- this.statusReporter.flushProgressStatus(`backfill:${attempt.agentName}`);
4287
- if (this.backfills.complete(attempt, result)) {
4288
- if (result === "failed") {
4289
- this.cacheIntegrityValidUntilByAgent.delete(attempt.agentName);
4290
- } else if (result === "committed") {
4291
- this.cacheIntegrityValidUntilByAgent.set(
4292
- attempt.agentName,
4293
- Date.now() + BACKFILL_INTERVAL_MS
4294
- );
4295
- }
4296
- this.statusReporter.publishBackfillStatus();
4297
- }
4298
- }
4299
- this.pumpBackfillQueue();
4300
- }
4301
- rejectBackfillAttempt(attempt, error) {
4302
- appLogger.error("scan.backfill.queue_error", { agent: attempt.agentName, error });
4303
- this.completeBackfillAttempt(attempt, "failed");
5294
+ this.backfillScheduler.enqueue(agentName);
4304
5295
  }
4305
5296
  runBackfill(attempt) {
4306
5297
  return this.scheduler.run(attempt.agentName, "backfill", () => this.performBackfill(attempt));
@@ -4315,8 +5306,8 @@ var AgentSyncEngine = class {
4315
5306
  const baseline = cached?.sessions ?? snapshot.byAgent[agentName] ?? [];
4316
5307
  const meta = cached?.meta ?? buildAgentCacheMeta(agent);
4317
5308
  const backfillCursor = getAgentFullSyncCursor(agentName);
4318
- if (cached) restoreAgentCacheMeta(agent, cached);
4319
- let durableCommitted = false;
5309
+ if (cached) agent.restoreSessionCacheMeta(cached.meta);
5310
+ let workerRun;
4320
5311
  try {
4321
5312
  if (!markAgentFullSyncStarted(agentName)) {
4322
5313
  appLogger.warn("scan.backfill.cache_state_unavailable", {
@@ -4324,13 +5315,17 @@ var AgentSyncEngine = class {
4324
5315
  state: "full_sync_started"
4325
5316
  });
4326
5317
  }
4327
- const operation = agent instanceof FileSystemSessionSource ? { kind: "source-backfill", cursor: backfillCursor, checkpoint: "durable" } : { kind: "full-backfill", cursor: backfillCursor, checkpoint: "durable" };
5318
+ const operation = {
5319
+ kind: "backfill",
5320
+ cursor: backfillCursor,
5321
+ checkpoint: "durable"
5322
+ };
4328
5323
  appLogger.info("scan.backfill.started", {
4329
5324
  agent: agentName,
4330
5325
  cursor: backfillCursor ?? void 0,
4331
5326
  durable_checkpoints: operation.checkpoint === "durable"
4332
5327
  });
4333
- const result = await this.runWorker(
5328
+ workerRun = await this.runWorker(
4334
5329
  agent,
4335
5330
  baseline,
4336
5331
  operation,
@@ -4341,7 +5336,8 @@ var AgentSyncEngine = class {
4341
5336
  onCheckpoint: (checkpoint) => this.handleBackfillCheckpoint(agentName, checkpoint)
4342
5337
  }
4343
5338
  );
4344
- agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
5339
+ const { result } = workerRun;
5340
+ agent.restoreSessionCacheMeta(result.meta);
4345
5341
  const fullSessions = attachMissingProjectIdentities(result.sessions);
4346
5342
  const completion = buildScanCompletion(result.completeness, result.sourceFailures ?? []);
4347
5343
  this.statusReporter.flushProgressStatus(`backfill:${agentName}`);
@@ -4354,7 +5350,7 @@ var AgentSyncEngine = class {
4354
5350
  }
4355
5351
  };
4356
5352
  updatePublicationPhase("publish-queued");
4357
- const publication = await this.commitSessionPublication({
5353
+ await this.sessionPublication.commit({
4358
5354
  context: "scan.backfill",
4359
5355
  agentName,
4360
5356
  sessions: fullSessions,
@@ -4369,81 +5365,50 @@ var AgentSyncEngine = class {
4369
5365
  removedSessionIds: result.explicitRemovedSessionIds,
4370
5366
  saveCache: true
4371
5367
  },
5368
+ stagedRun: workerRun,
4372
5369
  onPublishing: () => updatePublicationPhase("publishing"),
5370
+ onPublicationProgress: ({ stage }) => {
5371
+ if (stage === "prepared") updatePublicationPhase("indexing");
5372
+ if (stage === "search_staged") updatePublicationPhase("committing");
5373
+ },
4373
5374
  onCommitted: () => {
4374
- durableCommitted = true;
4375
5375
  if (completion.completeness === "partial") {
4376
5376
  this.backfills.recordCompletion(attempt, completion);
4377
5377
  }
4378
5378
  }
4379
5379
  });
4380
- durableCommitted = publication.durableCommitted;
4381
- this.options.workerRunner.commit?.(agentName);
4382
- if (!markAgentFullSyncCompleted(agentName)) {
4383
- appLogger.warn("scan.backfill.completion_not_durable", { agent: agentName });
4384
- }
4385
- appLogger.info(
4386
- result.sourceFailures?.length ? "scan.backfill.partial" : "scan.backfill.done",
4387
- {
4388
- agent: agentName,
4389
- duration_ms: Math.round(performance.now() - startedAt),
4390
- sessions: fullSessions.length,
4391
- changed: result.changedIds?.length ?? 0,
4392
- failed_sources: result.sourceFailures?.length ?? 0
5380
+ try {
5381
+ if (completion.completeness === "complete" && !markAgentFullSyncCompleted(agentName)) {
5382
+ appLogger.warn("scan.backfill.completion_not_durable", { agent: agentName });
4393
5383
  }
4394
- );
4395
- if (result.sourceFailures?.length) {
4396
- appLogger.warn("scan.backfill.source_failures", {
4397
- agent: agentName,
4398
- failures: result.sourceFailures
4399
- });
5384
+ appLogger.info(
5385
+ completion.completeness === "partial" ? "scan.backfill.partial" : "scan.backfill.done",
5386
+ {
5387
+ agent: agentName,
5388
+ duration_ms: Math.round(performance.now() - startedAt),
5389
+ sessions: fullSessions.length,
5390
+ changed: result.changedIds?.length ?? 0,
5391
+ failed_sources: result.sourceFailures?.length ?? 0
5392
+ }
5393
+ );
5394
+ if (result.sourceFailures?.length) {
5395
+ appLogger.warn("scan.backfill.source_failures", {
5396
+ agent: agentName,
5397
+ failures: result.sourceFailures
5398
+ });
5399
+ }
5400
+ } catch (error) {
5401
+ this.reportPostCommitError("scan.backfill", agentName, error);
4400
5402
  }
4401
5403
  return "committed";
4402
5404
  } catch (error) {
4403
- if (durableCommitted) {
4404
- this.reportPostCommitError("scan.backfill", agentName, error);
4405
- return "committed";
4406
- }
4407
- agent.setSessionMetaMap(new Map(Object.entries(meta)));
4408
- this.options.workerRunner.discard?.(agentName);
5405
+ agent.restoreSessionCacheMeta(meta);
5406
+ workerRun?.discard();
4409
5407
  appLogger.error("scan.backfill.error", { agent: agentName, error });
4410
5408
  console.error(`[${agentName}] Backfill failed:`, error);
4411
5409
  return "failed";
4412
5410
  }
4413
5411
  }
4414
- async commitSessionPublication(publication) {
4415
- const publicationId = this.indexPublisher.publicationId(
4416
- publication.context,
4417
- publication.agentName
4418
- );
4419
- await this.indexPublisher.commitSearchIndex(publication.context, [publication.indexJob], {
4420
- publicationId,
4421
- agent: publication.agentName,
4422
- ...publication.onPublishing ? { onStarted: publication.onPublishing } : {}
4423
- });
4424
- const diffStartedAt = performance.now();
4425
- const event = this.sessionIndex.commitAgentSessions(
4426
- publication.agentName,
4427
- publication.sessions,
4428
- publication.candidateChangedIds
4429
- );
4430
- const diffDuration = performance.now() - diffStartedAt;
4431
- const result = { durableCommitted: true, event, diffDuration };
4432
- publication.onCommitted?.(result);
4433
- this.emitSessionsChanged({
4434
- agentName: publication.agentName,
4435
- sessions: this.sessionIndex.snapshot().byAgent[publication.agentName] ?? [],
4436
- event
4437
- });
4438
- appLogger.info("session.publication.published", {
4439
- publication_id: publicationId,
4440
- context: publication.context,
4441
- agent: publication.agentName,
4442
- sessions: publication.sessions.length,
4443
- has_event: event != null
4444
- });
4445
- return result;
4446
- }
4447
5412
  findAgent(agentName) {
4448
5413
  return this.sessionIndex.findAgent(agentName);
4449
5414
  }
@@ -4460,7 +5425,7 @@ var AgentSyncEngine = class {
4460
5425
 
4461
5426
  // src/session-watcher.ts
4462
5427
  import { existsSync as existsSync3, readdirSync, statSync, watch } from "fs";
4463
- import { dirname as dirname2, isAbsolute, join, relative, resolve as resolve2 } from "path";
5428
+ import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
4464
5429
  var WRITE_STABILITY_THRESHOLD_MS = 250;
4465
5430
  var WRITE_STABILITY_POLL_MS = 100;
4466
5431
  function toAbsolutePath(path) {
@@ -4484,11 +5449,11 @@ function getWatchRoot(path) {
4484
5449
  const stat = statSync(path);
4485
5450
  return stat.isDirectory() ? path : dirname2(path);
4486
5451
  }
4487
- function isRecursiveWatchSupported(platform = process.platform, nodeVersion = process.versions.node) {
4488
- if (platform === "darwin" || platform === "win32") {
5452
+ function isRecursiveWatchSupported(platform2 = process.platform, nodeVersion = process.versions.node) {
5453
+ if (platform2 === "darwin" || platform2 === "win32") {
4489
5454
  return true;
4490
5455
  }
4491
- if (platform !== "linux" && platform !== "aix" && platform !== "ibmi") {
5456
+ if (platform2 !== "linux" && platform2 !== "aix" && platform2 !== "ibmi") {
4492
5457
  return false;
4493
5458
  }
4494
5459
  const [major = 0, minor = 0] = nodeVersion.split(".").map((part) => Number(part));
@@ -4522,7 +5487,7 @@ function resolveWatchEventPath(watchPath, filename) {
4522
5487
  if (!filenameText) {
4523
5488
  return watchPath;
4524
5489
  }
4525
- return isAbsolute(filenameText) ? filenameText : join(watchPath, filenameText);
5490
+ return isAbsolute(filenameText) ? filenameText : join2(watchPath, filenameText);
4526
5491
  }
4527
5492
  var SessionWatcher = class {
4528
5493
  watchers = [];
@@ -4640,7 +5605,7 @@ var SessionWatcher = class {
4640
5605
  try {
4641
5606
  for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
4642
5607
  if (entry.isDirectory()) {
4643
- pending.push(join(dirPath, entry.name));
5608
+ pending.push(join2(dirPath, entry.name));
4644
5609
  }
4645
5610
  }
4646
5611
  } catch (error) {
@@ -4768,8 +5733,10 @@ var SHUTDOWN_ERROR_MESSAGE3 = "Scan refresh worker shut down";
4768
5733
  function applySessionChanges2(previousSessions, changes, removedSessionIds) {
4769
5734
  if (changes.length === 0 && removedSessionIds.length === 0) return previousSessions;
4770
5735
  const replacedIds = new Set(removedSessionIds);
4771
- for (const { session } of changes) replacedIds.add(session.id);
4772
- const retained = previousSessions.filter((session) => !replacedIds.has(session.id));
5736
+ for (const { session } of changes) replacedIds.add(session.reference.sessionId);
5737
+ const retained = previousSessions.filter(
5738
+ (session) => !replacedIds.has(session.reference.sessionId)
5739
+ );
4773
5740
  const next = Array.from({
4774
5741
  length: retained.length + changes.length
4775
5742
  });
@@ -4857,24 +5824,7 @@ var ThreadWorkerRunner = class {
4857
5824
  }
4858
5825
  });
4859
5826
  }
4860
- commit(agentName) {
4861
- const slot = this.workers.get(agentName);
4862
- const awaiting = slot?.awaitingCommit;
4863
- if (!slot || !awaiting) return;
4864
- const request = {
4865
- type: "commit",
4866
- requestId: awaiting.requestId,
4867
- generation: awaiting.generation
4868
- };
4869
- try {
4870
- slot.worker.postMessage(request);
4871
- slot.awaitingCommit = null;
4872
- slot.generation += 1;
4873
- } catch {
4874
- this.invalidateWorker(agentName, slot);
4875
- }
4876
- }
4877
- discard(agentName) {
5827
+ reset(agentName) {
4878
5828
  const slot = this.workers.get(agentName);
4879
5829
  if (slot) this.invalidateWorker(agentName, slot);
4880
5830
  }
@@ -4900,7 +5850,6 @@ var ThreadWorkerRunner = class {
4900
5850
  awaitingCommit: null,
4901
5851
  closed: false
4902
5852
  };
4903
- worker.unref();
4904
5853
  this.workers.set(agentName, slot);
4905
5854
  worker.on("message", (message) => {
4906
5855
  if (appLogger.consumeWorkerMessage(message)) return;
@@ -4953,7 +5902,7 @@ var ThreadWorkerRunner = class {
4953
5902
  this.invalidateWorker(slot.agentName, slot);
4954
5903
  return;
4955
5904
  }
4956
- const changedIds = message.changes.map(({ session }) => session.id);
5905
+ const changedIds = message.changes.map(({ session }) => session.reference.sessionId);
4957
5906
  const replacedSessionIds = [...changedIds, ...message.removedSessionIds];
4958
5907
  const removedMetaIds = [...message.removedSessionIds, ...message.removedMetaIds];
4959
5908
  try {
@@ -4973,12 +5922,52 @@ var ThreadWorkerRunner = class {
4973
5922
  requestId: message.requestId,
4974
5923
  generation: message.generation
4975
5924
  };
4976
- pending.resolve(result);
5925
+ pending.resolve(this.createStagedRun(slot, message.requestId, message.generation, result));
4977
5926
  } catch (error) {
4978
5927
  pending.reject(toError2(error));
4979
5928
  this.invalidateWorker(slot.agentName, slot);
4980
5929
  }
4981
5930
  }
5931
+ createStagedRun(slot, requestId, generation, result) {
5932
+ let finalized = false;
5933
+ return {
5934
+ result,
5935
+ commit: () => {
5936
+ if (finalized) return;
5937
+ finalized = true;
5938
+ this.commitStagedRun(slot, requestId, generation);
5939
+ },
5940
+ discard: () => {
5941
+ if (finalized) return;
5942
+ finalized = true;
5943
+ this.discardStagedRun(slot, requestId, generation);
5944
+ }
5945
+ };
5946
+ }
5947
+ commitStagedRun(slot, requestId, generation) {
5948
+ const awaiting = slot.awaitingCommit;
5949
+ if (this.workers.get(slot.agentName) !== slot || awaiting?.requestId !== requestId || awaiting.generation !== generation) {
5950
+ return;
5951
+ }
5952
+ const request = {
5953
+ type: "commit",
5954
+ requestId,
5955
+ generation
5956
+ };
5957
+ try {
5958
+ slot.worker.postMessage(request);
5959
+ slot.awaitingCommit = null;
5960
+ slot.generation += 1;
5961
+ } catch {
5962
+ this.invalidateWorker(slot.agentName, slot);
5963
+ }
5964
+ }
5965
+ discardStagedRun(slot, requestId, generation) {
5966
+ const awaiting = slot.awaitingCommit;
5967
+ if (this.workers.get(slot.agentName) === slot && awaiting?.requestId === requestId && awaiting.generation === generation) {
5968
+ this.invalidateWorker(slot.agentName, slot);
5969
+ }
5970
+ }
4982
5971
  closeWorker(agentName, slot, error) {
4983
5972
  if (slot.closed) return;
4984
5973
  slot.closed = true;
@@ -5038,7 +6027,6 @@ var LiveScanStore = class {
5038
6027
  const initialResult = await scanSessions({
5039
6028
  ...this.scanOptions,
5040
6029
  useCache: this.scanOptions.useCache ?? true,
5041
- smartRefresh: false,
5042
6030
  cacheOnly: this.deferInitialRefresh,
5043
6031
  writeCache: this.deferInitialRefresh ? false : this.scanOptions.writeCache,
5044
6032
  smartTagWorkerUrl: this.getSmartTagWorkerUrl() ?? void 0,
@@ -5074,7 +6062,13 @@ var LiveScanStore = class {
5074
6062
  check_changes_ms: timing.checkChanges != null ? Math.round(timing.checkChanges) : void 0,
5075
6063
  scan_ms: timing.scan != null ? Math.round(timing.scan) : void 0,
5076
6064
  identity_ms: timing.identity != null ? Math.round(timing.identity) : void 0,
5077
- tags_ms: timing.tags != null ? Math.round(timing.tags) : void 0
6065
+ tags_ms: timing.tags != null ? Math.round(timing.tags) : void 0,
6066
+ source_enumeration_ms: timing.sourceEnumeration != null ? Math.round(timing.sourceEnumeration) : void 0,
6067
+ source_diff_ms: timing.sourceDiff != null ? Math.round(timing.sourceDiff) : void 0,
6068
+ source_parse_ms: timing.sourceParse != null ? Math.round(timing.sourceParse) : void 0,
6069
+ enumerated_sources: timing.enumeratedSources,
6070
+ changed_sources: timing.changedSources,
6071
+ processed_sources: timing.processedSources
5078
6072
  }
5079
6073
  ])
5080
6074
  ) : void 0
@@ -5120,7 +6114,7 @@ var LiveScanStore = class {
5120
6114
  }
5121
6115
  emit(event) {
5122
6116
  if (this.shuttingDown) return;
5123
- if (this.pendingEvent || event.newSessions > 0) {
6117
+ if (this.pendingEvent || event.newSessionRefs.length > 0) {
5124
6118
  this.queueEvent(event);
5125
6119
  return;
5126
6120
  }
@@ -5195,7 +6189,7 @@ function formatScanFailureDiagnostics(snapshot) {
5195
6189
  }
5196
6190
  function formatCacheFailureDiagnostics(snapshot) {
5197
6191
  return Object.values(snapshot.cacheFailures ?? {}).map(
5198
- (failure) => `[${failure.agentName}] Cache persistence failed; serving in-memory results without advancing the durable baseline`
6192
+ (failure) => failure.operation === "read" ? `[${failure.agentName}] Cache read failed; durable baseline is unavailable` : `[${failure.agentName}] Cache persistence failed; serving in-memory results without advancing the durable baseline`
5199
6193
  );
5200
6194
  }
5201
6195
  function buildSessionIndexOutput(snapshot, window = {}) {
@@ -5331,6 +6325,10 @@ var main = defineCommand({
5331
6325
  description: "A reverse proxy in front of CodeSesh terminates TLS",
5332
6326
  default: false
5333
6327
  },
6328
+ "public-url": {
6329
+ type: "string",
6330
+ description: "Public HTTPS origin served by the trusted reverse proxy"
6331
+ },
5334
6332
  agent: {
5335
6333
  type: "string",
5336
6334
  alias: "a",
@@ -5398,6 +6396,7 @@ var main = defineCommand({
5398
6396
  const hostname = args.host;
5399
6397
  const remoteAccess = args["remote-access"];
5400
6398
  let transport;
6399
+ let publicOrigin;
5401
6400
  try {
5402
6401
  transport = resolveRemoteTransport({
5403
6402
  hostname,
@@ -5405,13 +6404,14 @@ var main = defineCommand({
5405
6404
  tlsKeyPath: args["tls-key"],
5406
6405
  trustProxy: args["trust-proxy"]
5407
6406
  });
6407
+ publicOrigin = resolvePublicOrigin(args["public-url"], transport);
5408
6408
  } catch (error) {
5409
6409
  console.error(error instanceof Error ? error.message : String(error));
5410
6410
  process.exit(1);
5411
6411
  }
5412
- if (resolveRemoteAccessPolicy(hostname, transport).authenticationRequired && !remoteAccess) {
6412
+ if (resolveRemoteAccessPolicy(hostname, transport).remoteAccessRequired && !remoteAccess) {
5413
6413
  console.error(
5414
- `Refusing to expose CodeSesh on ${hostname} without authentication. Add --remote-access to continue.`
6414
+ `Refusing to expose CodeSesh on ${hostname} without explicit remote access. Add --remote-access to continue.`
5415
6415
  );
5416
6416
  process.exit(1);
5417
6417
  }
@@ -5423,6 +6423,7 @@ var main = defineCommand({
5423
6423
  port,
5424
6424
  host: hostname,
5425
6425
  transport: transport.kind,
6426
+ public_url: publicOrigin,
5426
6427
  remote_access: remoteAccess,
5427
6428
  json: jsonOnly,
5428
6429
  no_open: noOpen,
@@ -5432,7 +6433,7 @@ var main = defineCommand({
5432
6433
  log_path: appLogger.getLogPath()
5433
6434
  });
5434
6435
  if (clearCache) {
5435
- const { clearCache: clear } = await import("./dist-RHDY2EZX.js");
6436
+ const { clearCache: clear } = await import("./discovery-K3GLISI6.js");
5436
6437
  clear();
5437
6438
  appLogger.info("cache.clear");
5438
6439
  console.log("Cache cleared.");
@@ -5484,18 +6485,21 @@ var main = defineCommand({
5484
6485
  await pricingRefresh.cancel();
5485
6486
  const scanFailureDiagnostics = formatScanFailureDiagnostics(result);
5486
6487
  if (scanFailureDiagnostics.length > 0) {
5487
- await store.shutdown();
5488
6488
  for (const diagnostic of scanFailureDiagnostics) console.error(diagnostic);
5489
6489
  process.exitCode = 1;
5490
- return;
6490
+ } else {
6491
+ for (const diagnostic of formatCacheFailureDiagnostics(result)) console.error(diagnostic);
6492
+ const output = buildSessionIndexOutput(result, {
6493
+ from: listDefaultFrom,
6494
+ to: listDefaultTo
6495
+ });
6496
+ appLogger.info("cli.json_output", {
6497
+ sessions: output.sessions.length,
6498
+ duration_ms: Math.round(performance.now() - startedAt)
6499
+ });
6500
+ console.log(JSON.stringify(output, null, 2));
5491
6501
  }
5492
- for (const diagnostic of formatCacheFailureDiagnostics(result)) console.error(diagnostic);
5493
- const output = buildSessionIndexOutput(result, { from: listDefaultFrom, to: listDefaultTo });
5494
- appLogger.info("cli.json_output", {
5495
- sessions: output.sessions.length,
5496
- duration_ms: Math.round(performance.now() - startedAt)
5497
- });
5498
- console.log(JSON.stringify(output, null, 2));
6502
+ await store.shutdown();
5499
6503
  return;
5500
6504
  }
5501
6505
  const agents = createRegisteredAgents();
@@ -5509,7 +6513,8 @@ var main = defineCommand({
5509
6513
  portFallbackAttempts: explicitPort ? 1 : DEFAULT_PORT_FALLBACK_ATTEMPTS,
5510
6514
  hostname,
5511
6515
  remoteAccess,
5512
- transport
6516
+ transport,
6517
+ publicUrl: publicOrigin
5513
6518
  });
5514
6519
  } catch (error) {
5515
6520
  console.error(getServerStartupErrorMessage(error, port));