codesesh 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,29 +2,27 @@
2
2
  import {
3
3
  appLogger,
4
4
  logSearchIndexSync
5
- } from "./chunk-EWC6IODK.js";
5
+ } from "./chunk-A4U2SMJJ.js";
6
6
  import {
7
7
  FileSystemSessionSource,
8
+ SessionAliasValidationError,
8
9
  StateStorageUnavailableError,
9
10
  attachMissingProjectIdentities,
11
+ attachProjectMetrics,
10
12
  buildAgentCacheMeta,
11
13
  buildDashboard,
12
- classifySessionTags,
13
- computeIdentity,
14
14
  computeSessionDiff,
15
15
  createProjectScopeMatcher,
16
16
  createRegisteredAgents,
17
17
  deleteBookmark,
18
18
  deleteSessionAlias,
19
19
  executeSessionSearch,
20
- extractSessionFileActivity,
20
+ filterSessionSearchCandidates,
21
+ formatSessionReference,
21
22
  getAgentInfoMap,
22
23
  getAgentLastFullSyncAt,
23
- getCursorDataPath,
24
24
  getSessionActivityTime,
25
- getSessionAgentName,
26
- getSmartTagSourceTimestamp,
27
- getTotalTokens,
25
+ getSessionAgentKey,
28
26
  importBookmarks,
29
27
  isAgentCacheInitialized,
30
28
  isProjectIdentityKind,
@@ -32,25 +30,23 @@ import {
32
30
  listCachedProjectGroups,
33
31
  listFileActivity,
34
32
  listSessionAliases,
35
- listSessionFileActivity,
36
- loadCachedSessionDataEntry,
37
33
  loadCachedSessions,
38
34
  markAgentFullSyncCompleted,
39
35
  matchesProjectIdentity,
40
36
  matchesProjectScope,
41
- matchesSessionSearchFilters,
37
+ materializeSessionDetailResponse,
42
38
  mergeSearchQueryOptions,
39
+ mergeSortedSessions,
40
+ normalizeSessionReference,
43
41
  perf,
44
- realFs,
45
42
  refreshPricingCache,
46
- resolveProviderRoots,
47
43
  scanSessions,
48
44
  sessionSignature,
49
45
  sortSessions,
50
46
  startOfLocalDay,
51
47
  upsertBookmark,
52
48
  upsertSessionAlias
53
- } from "./chunk-MWSJTNOW.js";
49
+ } from "./chunk-7APNDHQ6.js";
54
50
 
55
51
  // src/index.ts
56
52
  import { defineCommand, runMain } from "citty";
@@ -131,26 +127,98 @@ function elapsedDays(from, to) {
131
127
  return Math.max(1, Math.ceil((to - from) / DAY_MS));
132
128
  }
133
129
 
134
- // src/api/handlers.ts
135
- function cacheMatchesCurrentSource(cachedMeta, currentMeta) {
136
- const currentFingerprint = currentMeta?.sourceFingerprint;
137
- if (typeof currentFingerprint !== "string") return true;
138
- return cachedMeta?.sourceFingerprint === currentFingerprint;
130
+ // src/api/query-params.ts
131
+ var SMART_TAGS = [
132
+ "bugfix",
133
+ "refactoring",
134
+ "feature-dev",
135
+ "testing",
136
+ "docs",
137
+ "git-ops",
138
+ "build-deploy",
139
+ "exploration",
140
+ "planning"
141
+ ];
142
+ var SEARCH_LIMIT_MAX = 100;
143
+ var SEARCH_LIMIT_DEFAULT = 50;
144
+ function searchParams(c) {
145
+ return new URL(c.req.url ?? "http://localhost/", "http://localhost/").searchParams;
139
146
  }
140
- function getSessionAliasKey(agentKey, sessionId) {
141
- return `${agentKey.toLowerCase()}\0${sessionId}`;
147
+ function queryValues(params, ...names) {
148
+ return names.flatMap(
149
+ (name) => params.getAll(name).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean)
150
+ );
142
151
  }
143
- function getSessionAgentKey(session) {
144
- return session.slug.split("/")[0]?.toLowerCase() ?? "";
152
+ function optionalQueryValue(value) {
153
+ const normalized = value?.trim();
154
+ return normalized ? normalized : void 0;
145
155
  }
146
- function loadSessionAliasMap() {
156
+ function parseDateParam(value, fallback) {
157
+ if (value == null) return fallback;
158
+ const ts = new Date(value).getTime();
159
+ return Number.isNaN(ts) ? fallback : ts;
160
+ }
161
+ function parseNumberParam(value) {
162
+ if (value == null || !value.trim()) return void 0;
163
+ const number = Number(value);
164
+ return Number.isFinite(number) ? number : void 0;
165
+ }
166
+ function parseSmartTags(values) {
167
+ const tags = values.map((value) => value.toLowerCase()).filter((value) => SMART_TAGS.includes(value));
168
+ return tags.length > 0 ? [...new Set(tags)] : void 0;
169
+ }
170
+ function parseFileActivityKind(value) {
171
+ if (value === "read" || value === "edit" || value === "write" || value === "delete") {
172
+ return value;
173
+ }
174
+ return void 0;
175
+ }
176
+ function parseProjectIdentityFilter(kindValue, keyValue) {
177
+ const kind = optionalQueryValue(kindValue);
178
+ const key = optionalQueryValue(keyValue);
179
+ if (!kind && !key) return void 0;
180
+ if (!kind || !key || !isProjectIdentityKind(kind)) return null;
181
+ return { kind, key };
182
+ }
183
+ function parseSearchOptions(c, defaults, projectIdentity) {
184
+ const params = searchParams(c);
185
+ const limitValue = parseNumberParam(params.get("limit") ?? void 0);
186
+ return {
187
+ agent: optionalQueryValue(params.get("agent") ?? void 0),
188
+ project: optionalQueryValue(params.get("project") ?? void 0),
189
+ projectKind: projectIdentity?.kind,
190
+ projectKey: projectIdentity?.key,
191
+ cwd: optionalQueryValue(params.get("cwd") ?? void 0),
192
+ tags: parseSmartTags(queryValues(params, "tag", "tags", "signal")),
193
+ tools: queryValues(params, "tool", "tools").map((tool) => tool.toLowerCase()),
194
+ file: optionalQueryValue(params.get("file") ?? params.get("path") ?? void 0),
195
+ fileKind: parseFileActivityKind(
196
+ optionalQueryValue(params.get("fileKind") ?? params.get("fileActivity") ?? void 0)
197
+ ),
198
+ costMin: parseNumberParam(params.get("costMin") ?? void 0),
199
+ costMax: parseNumberParam(params.get("costMax") ?? void 0),
200
+ from: parseDateParam(params.get("from") ?? void 0, defaults.from),
201
+ to: parseDateParam(params.get("to") ?? void 0, defaults.to),
202
+ limit: limitValue && limitValue > 0 ? Math.min(limitValue, SEARCH_LIMIT_MAX) : SEARCH_LIMIT_DEFAULT
203
+ };
204
+ }
205
+ function filterSessionsByActivityWindow(sessions, from, to) {
206
+ if (from == null && to == null) return sessions;
207
+ return sessions.filter((session) => {
208
+ const activity = getSessionActivityTime(session);
209
+ if (from != null && activity < from) return false;
210
+ if (to != null && activity > to) return false;
211
+ return true;
212
+ });
213
+ }
214
+
215
+ // src/api/session-aliases-view.ts
216
+ function aliasKey(reference) {
217
+ return `${reference.agentName.toLowerCase()}\0${reference.sessionId}`;
218
+ }
219
+ function loadAliasMap() {
147
220
  try {
148
- return new Map(
149
- listSessionAliases().map((alias) => [
150
- getSessionAliasKey(alias.agentKey, alias.sessionId),
151
- alias.alias
152
- ])
153
- );
221
+ return new Map(listSessionAliases().map((alias) => [aliasKey(alias.reference), alias]));
154
222
  } catch (error) {
155
223
  if (!(error instanceof StateStorageUnavailableError)) {
156
224
  appLogger.warn("api.session_aliases.load_failed", {
@@ -160,52 +228,97 @@ function loadSessionAliasMap() {
160
228
  return /* @__PURE__ */ new Map();
161
229
  }
162
230
  }
163
- function isStateStorageUnavailable(error) {
164
- return error instanceof StateStorageUnavailableError;
231
+ function buildAliasView() {
232
+ const aliases = loadAliasMap();
233
+ return {
234
+ size: aliases.size,
235
+ get: (reference) => aliases.get(aliasKey(reference))?.alias,
236
+ decorate(record, reference) {
237
+ const alias = aliases.get(aliasKey(reference))?.alias;
238
+ return alias ? { ...record, display_title: alias } : record;
239
+ },
240
+ entries: () => aliases.values()
241
+ };
242
+ }
243
+ var cachedView = null;
244
+ function loadAliasView() {
245
+ return cachedView ??= buildAliasView();
165
246
  }
166
- function withDisplayTitle(session, agentKey, aliases) {
167
- const alias = aliases.get(getSessionAliasKey(agentKey, session.id));
168
- return alias ? { ...session, display_title: alias } : session;
247
+ function invalidateAliasView() {
248
+ cachedView = null;
169
249
  }
170
- function withBookmarkDisplayTitle(bookmark, aliases) {
171
- const alias = aliases.get(getSessionAliasKey(bookmark.agentKey, bookmark.sessionId));
172
- return alias ? { ...bookmark, display_title: alias } : bookmark;
250
+ function decorateBookmark(bookmark, aliases) {
251
+ return {
252
+ ...bookmark,
253
+ session: aliases.decorate(bookmark.session, bookmark.reference)
254
+ };
173
255
  }
174
- function withFileActivityDisplayTitle(activity, aliases) {
256
+ function decorateFileActivity(activity, aliases) {
175
257
  return {
176
258
  ...activity,
177
- session: withDisplayTitle(activity.session, activity.agent_name, aliases)
259
+ session: aliases.decorate(activity.session, activity.reference)
178
260
  };
179
261
  }
180
- function findSessionByAliasKey(scanResult, aliasKey) {
181
- const separatorIndex = aliasKey.indexOf("\0");
182
- const agentName = aliasKey.slice(0, separatorIndex);
183
- const sessionId = aliasKey.slice(separatorIndex + 1);
184
- return scanResult.byAgent[agentName]?.find((session) => session.id === sessionId);
185
- }
186
262
  function findAliasSearchResults(query, options, scanResult, aliases) {
187
263
  const search = mergeSearchQueryOptions(query, options);
188
264
  const needle = search.text.trim().toLowerCase();
189
265
  if (!needle || aliases.size === 0) return [];
190
- const projectScope = search.options.cwd ? createProjectScopeMatcher(search.options.cwd) : null;
266
+ const sessionsByAgent = /* @__PURE__ */ new Map();
267
+ const lookupSession = (agentName, sessionId) => {
268
+ let byId = sessionsByAgent.get(agentName);
269
+ if (!byId) {
270
+ byId = new Map((scanResult.byAgent[agentName] ?? []).map((item) => [item.id, item]));
271
+ sessionsByAgent.set(agentName, byId);
272
+ }
273
+ return byId.get(sessionId);
274
+ };
191
275
  const results = [];
192
- for (const [aliasKey, alias] of aliases) {
193
- if (!alias.toLowerCase().includes(needle)) continue;
194
- const session = findSessionByAliasKey(scanResult, aliasKey);
276
+ for (const alias of aliases.entries()) {
277
+ if (!alias.alias.toLowerCase().includes(needle)) continue;
278
+ const { agentName, sessionId } = alias.reference;
279
+ const session = lookupSession(agentName, sessionId);
195
280
  if (!session) continue;
196
- const agentName = aliasKey.slice(0, aliasKey.indexOf("\0"));
197
- if (!matchesSessionSearchFilters(agentName, session, search.options, projectScope)) continue;
198
281
  results.push({
199
- agentName,
200
- session: withDisplayTitle(session, agentName, aliases),
282
+ reference: alias.reference,
283
+ session: aliases.decorate(session, alias.reference),
201
284
  snippet: `Alias \xB7 ${session.directory}`,
202
285
  matchType: "title"
203
286
  });
204
287
  }
205
- return results.sort(
288
+ return filterSessionSearchCandidates(results, search.options).sort(
206
289
  (a, b) => getSessionActivityTime(b.session) - getSessionActivityTime(a.session)
207
290
  );
208
291
  }
292
+
293
+ // src/api/handlers.ts
294
+ var SNAPSHOT_AGGREGATION_CACHE_LIMIT = 64;
295
+ var snapshotAggregationCaches = /* @__PURE__ */ new WeakMap();
296
+ function getSnapshotAggregation(source, sessions, key, build) {
297
+ let cache = snapshotAggregationCaches.get(source);
298
+ if (!cache || cache.sessions !== sessions) {
299
+ cache = { sessions, values: /* @__PURE__ */ new Map() };
300
+ snapshotAggregationCaches.set(source, cache);
301
+ }
302
+ const cacheKey = JSON.stringify(key);
303
+ if (cache.values.has(cacheKey)) return cache.values.get(cacheKey);
304
+ const value = build();
305
+ if (cache.values.size >= SNAPSHOT_AGGREGATION_CACHE_LIMIT) {
306
+ const oldestKey = cache.values.keys().next().value;
307
+ if (oldestKey != null) cache.values.delete(oldestKey);
308
+ }
309
+ cache.values.set(cacheKey, value);
310
+ return value;
311
+ }
312
+ function withStorageErrors(handler, onUnavailable) {
313
+ try {
314
+ return handler();
315
+ } catch (error) {
316
+ if (error instanceof StateStorageUnavailableError) {
317
+ return onUnavailable();
318
+ }
319
+ throw error;
320
+ }
321
+ }
209
322
  function isRecord(value) {
210
323
  return typeof value === "object" && value !== null;
211
324
  }
@@ -213,15 +326,14 @@ function isSessionStats(value) {
213
326
  if (!isRecord(value)) return false;
214
327
  return typeof value.message_count === "number" && typeof value.total_input_tokens === "number" && typeof value.total_output_tokens === "number" && typeof value.total_cost === "number" && (value.total_tokens == null || typeof value.total_tokens === "number");
215
328
  }
216
- function parseBookmarkPayload(value) {
329
+ function parseBookmarkSession(value, reference) {
217
330
  if (!isRecord(value)) return null;
218
- if (typeof value.agentKey !== "string" || typeof value.sessionId !== "string" || typeof value.fullPath !== "string" || typeof value.title !== "string" || typeof value.directory !== "string" || typeof value.time_created !== "number" || value.time_updated != null && typeof value.time_updated !== "number" || !isSessionStats(value.stats)) {
331
+ if (value.id !== reference.sessionId || typeof value.slug !== "string" || typeof value.title !== "string" || typeof value.directory !== "string" || typeof value.time_created !== "number" || value.time_updated != null && typeof value.time_updated !== "number" || !isSessionStats(value.stats)) {
219
332
  return null;
220
333
  }
221
334
  return {
222
- agentKey: value.agentKey,
223
- sessionId: value.sessionId,
224
- fullPath: value.fullPath,
335
+ id: reference.sessionId,
336
+ slug: formatSessionReference(reference),
225
337
  title: value.title,
226
338
  directory: value.directory,
227
339
  time_created: value.time_created,
@@ -229,70 +341,38 @@ function parseBookmarkPayload(value) {
229
341
  stats: value.stats
230
342
  };
231
343
  }
232
- function parseDateParam(value, fallback) {
233
- if (value == null) return fallback;
234
- const ts = new Date(value).getTime();
235
- return Number.isNaN(ts) ? fallback : ts;
236
- }
237
- function parseNumberParam(value) {
238
- if (value == null || !value.trim()) return void 0;
239
- const number = Number(value);
240
- return Number.isFinite(number) ? number : void 0;
241
- }
242
- function searchParams(c) {
243
- return new URL(c.req.url ?? "http://localhost/", "http://localhost/").searchParams;
244
- }
245
- function queryValues(params, ...names) {
246
- return names.flatMap(
247
- (name) => params.getAll(name).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean)
248
- );
249
- }
250
- function parseSmartTags(values) {
251
- const tags = values.map((value) => value.toLowerCase()).filter(
252
- (value) => [
253
- "bugfix",
254
- "refactoring",
255
- "feature-dev",
256
- "testing",
257
- "docs",
258
- "git-ops",
259
- "build-deploy",
260
- "exploration",
261
- "planning"
262
- ].includes(value)
263
- );
264
- return tags.length > 0 ? [...new Set(tags)] : void 0;
265
- }
266
- function parseSearchOptions(c, defaults, projectIdentity) {
267
- const params = searchParams(c);
268
- const limitValue = parseNumberParam(params.get("limit") ?? void 0);
269
- return {
270
- agent: optionalQueryValue(params.get("agent") ?? void 0),
271
- project: optionalQueryValue(params.get("project") ?? void 0),
272
- projectKind: projectIdentity?.kind,
273
- projectKey: projectIdentity?.key,
274
- cwd: optionalQueryValue(params.get("cwd") ?? void 0),
275
- tags: parseSmartTags(queryValues(params, "tag", "tags", "signal")),
276
- tools: queryValues(params, "tool", "tools").map((tool) => tool.toLowerCase()),
277
- file: optionalQueryValue(params.get("file") ?? params.get("path") ?? void 0),
278
- fileKind: parseFileActivityKind(
279
- optionalQueryValue(params.get("fileKind") ?? params.get("fileActivity") ?? void 0)
280
- ),
281
- costMin: parseNumberParam(params.get("costMin") ?? void 0),
282
- costMax: parseNumberParam(params.get("costMax") ?? void 0),
283
- from: parseDateParam(params.get("from") ?? void 0, defaults.from),
284
- to: parseDateParam(params.get("to") ?? void 0, defaults.to),
285
- limit: limitValue && limitValue > 0 ? Math.min(limitValue, 100) : 50
286
- };
344
+ function parseSessionReferencePayload(value) {
345
+ if (!isRecord(value) || typeof value.agentName !== "string" || typeof value.sessionId !== "string" || !value.agentName.trim() || !value.sessionId) {
346
+ return null;
347
+ }
348
+ return normalizeSessionReference({
349
+ agentName: value.agentName.trim().toLowerCase(),
350
+ sessionId: value.sessionId
351
+ });
287
352
  }
288
- function filterSessionsByActivityWindow(sessions, from, to) {
289
- if (from == null && to == null) return sessions;
290
- return sessions.filter((session) => {
291
- const activity = getSessionActivityTime(session);
292
- if (from != null && activity < from) return false;
293
- if (to != null && activity > to) return false;
294
- return true;
353
+ function parseBookmarkPayload(value) {
354
+ if (!isRecord(value)) return null;
355
+ const reference = parseSessionReferencePayload(value.reference);
356
+ if (reference) {
357
+ const session2 = parseBookmarkSession(value.session, reference);
358
+ return session2 ? { reference, session: session2 } : null;
359
+ }
360
+ if (typeof value.agentKey !== "string" || typeof value.sessionId !== "string" || typeof value.fullPath !== "string" || !value.agentKey.trim() || !value.sessionId) {
361
+ return null;
362
+ }
363
+ const legacyReference = normalizeSessionReference({
364
+ agentName: value.agentKey.trim().toLowerCase(),
365
+ sessionId: value.sessionId
295
366
  });
367
+ const session = parseBookmarkSession(
368
+ {
369
+ ...value,
370
+ id: value.sessionId,
371
+ slug: value.fullPath
372
+ },
373
+ legacyReference
374
+ );
375
+ return session ? { reference: legacyReference, session } : null;
296
376
  }
297
377
  function sanitizeClientLogData(value) {
298
378
  if (!isRecord(value)) return {};
@@ -306,60 +386,47 @@ function sanitizeClientLogData(value) {
306
386
  })
307
387
  );
308
388
  }
309
- function getProjectGroupKey(identityKind, identityKey) {
310
- return `${identityKind}:${identityKey}`;
311
- }
312
- function attachProjectMetrics(projects, sessions) {
313
- const metrics = /* @__PURE__ */ new Map();
314
- for (const session of sessions) {
315
- const identity = session.project_identity;
316
- if (!identity) continue;
317
- const key = getProjectGroupKey(identity.kind, identity.key);
318
- let current = metrics.get(key);
319
- if (!current) {
320
- current = {
321
- messages: 0,
322
- tokens: 0,
323
- cost: 0,
324
- hasEstimatedCost: false,
325
- agentStats: /* @__PURE__ */ new Map()
326
- };
327
- metrics.set(key, current);
328
- }
329
- const tokens = getTotalTokens(session.stats);
330
- const cost = session.stats.total_cost ?? 0;
331
- current.messages += session.stats.message_count;
332
- current.tokens += tokens;
333
- current.cost += cost;
334
- if (session.stats.cost_source === "estimated") current.hasEstimatedCost = true;
335
- const agentName = getSessionAgentName(session);
336
- const agent = current.agentStats.get(agentName);
337
- if (agent) {
338
- agent.sessions += 1;
339
- agent.messages += session.stats.message_count;
340
- agent.tokens += tokens;
341
- agent.cost += cost;
342
- } else {
343
- current.agentStats.set(agentName, {
344
- name: agentName,
345
- sessions: 1,
346
- messages: session.stats.message_count,
347
- tokens,
348
- cost
349
- });
350
- }
351
- }
352
- return projects.map((project) => {
353
- const metric = metrics.get(getProjectGroupKey(project.identityKind, project.identityKey));
354
- return {
355
- ...project,
356
- messages: metric?.messages ?? 0,
357
- tokens: metric?.tokens ?? 0,
358
- cost: metric?.cost ?? 0,
359
- cost_source: metric && metric.cost > 0 ? metric.hasEstimatedCost ? "estimated" : "recorded" : void 0,
360
- agentStats: [...metric?.agentStats.values() ?? []].sort((a, b) => b.sessions - a.sessions)
361
- };
362
- });
389
+ function toSessionListItem(session) {
390
+ if (!session.model_usage) return session;
391
+ const item = { ...session };
392
+ delete item.model_usage;
393
+ return item;
394
+ }
395
+ function getSessionHeadReference(session) {
396
+ return {
397
+ agentName: getSessionAgentKey(session),
398
+ sessionId: session.id
399
+ };
400
+ }
401
+ function createSessionDetailJsonResponse(data, messages) {
402
+ const encoder = new TextEncoder();
403
+ const headerJson = JSON.stringify(data);
404
+ const iterator = messages[Symbol.iterator]();
405
+ let wroteHeader = false;
406
+ let wroteMessage = false;
407
+ return new Response(
408
+ new ReadableStream({
409
+ pull(controller) {
410
+ if (!wroteHeader) {
411
+ controller.enqueue(encoder.encode(`${headerJson.slice(0, -1)},"messages":[`));
412
+ wroteHeader = true;
413
+ return;
414
+ }
415
+ const next = iterator.next();
416
+ if (!next.done) {
417
+ controller.enqueue(encoder.encode(`${wroteMessage ? "," : ""}${next.value}`));
418
+ wroteMessage = true;
419
+ return;
420
+ }
421
+ controller.enqueue(encoder.encode("]}"));
422
+ controller.close();
423
+ },
424
+ cancel() {
425
+ iterator.return?.();
426
+ }
427
+ }),
428
+ { headers: { "Content-Type": "application/json; charset=UTF-8" } }
429
+ );
363
430
  }
364
431
  function handleGetConfig(c, defaults) {
365
432
  const payload = {
@@ -378,22 +445,38 @@ function handleGetAgents(c, scanSource, defaults = {}) {
378
445
  const scanResult = scanSource.getSnapshot();
379
446
  const from = parseDateParam(c.req.query("from"), defaults.from);
380
447
  const to = parseDateParam(c.req.query("to"), defaults.to);
381
- const counts = Object.fromEntries(
382
- Object.entries(scanResult.byAgent).map(([agentName, sessions]) => [
383
- agentName,
384
- filterSessionsByActivityWindow(sessions, from, to).length
385
- ])
448
+ const agents = getSnapshotAggregation(
449
+ scanSource,
450
+ scanResult.sessions,
451
+ ["agents", from, to],
452
+ () => {
453
+ const counts = Object.fromEntries(
454
+ Object.entries(scanResult.byAgent).map(([agentName, sessions]) => [
455
+ agentName,
456
+ filterSessionsByActivityWindow(sessions, from, to).length
457
+ ])
458
+ );
459
+ return getAgentInfoMap(counts);
460
+ }
386
461
  );
387
- return c.json(getAgentInfoMap(counts));
462
+ return c.json(agents);
388
463
  }
389
464
  function handleGetProjects(c, scanSource, defaults = {}) {
390
465
  const scanResult = scanSource.getSnapshot();
391
466
  const from = parseDateParam(c.req.query("from"), defaults.from);
392
467
  const to = parseDateParam(c.req.query("to"), defaults.to);
393
- const sessions = filterSessionsByActivityWindow(scanResult.sessions, from, to);
394
- return c.json({
395
- projects: attachProjectMetrics(listCachedProjectGroups(sessions), sessions)
396
- });
468
+ const projects = getSnapshotAggregation(
469
+ scanSource,
470
+ scanResult.sessions,
471
+ ["projects", from, to],
472
+ () => {
473
+ const sessions = filterSessionsByActivityWindow(scanResult.sessions, from, to);
474
+ return {
475
+ projects: attachProjectMetrics(listCachedProjectGroups(sessions), sessions)
476
+ };
477
+ }
478
+ );
479
+ return c.json(projects);
397
480
  }
398
481
  function handleGetSessions(c, scanSource, defaults = {}) {
399
482
  const scanResult = scanSource.getSnapshot();
@@ -428,16 +511,16 @@ function handleGetSessions(c, scanSource, defaults = {}) {
428
511
  if (tag) {
429
512
  sessions = sessions.filter((s) => s.smart_tags?.includes(tag));
430
513
  }
431
- const aliases = loadSessionAliasMap();
514
+ const aliases = loadAliasView();
432
515
  if (q) {
433
516
  sessions = sessions.filter((session) => {
434
- const alias = aliases.get(getSessionAliasKey(getSessionAgentKey(session), session.id));
517
+ const alias = aliases.get(getSessionHeadReference(session));
435
518
  return session.title.toLowerCase().includes(q) || alias?.toLowerCase().includes(q);
436
519
  });
437
520
  }
438
521
  return c.json({
439
522
  sessions: sessions.map(
440
- (session) => withDisplayTitle(session, getSessionAgentKey(session), aliases)
523
+ (session) => toSessionListItem(aliases.decorate(session, getSessionHeadReference(session)))
441
524
  )
442
525
  });
443
526
  }
@@ -452,35 +535,18 @@ function handleSearchSessions(c, scanSource, defaults = {}) {
452
535
  return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
453
536
  }
454
537
  const searchOptions = parseSearchOptions(c, defaults, projectIdentity);
455
- const aliases = loadSessionAliasMap();
538
+ const aliases = loadAliasView();
456
539
  const results = executeSessionSearch(query, searchOptions, scanResult).map((result) => ({
457
540
  ...result,
458
- session: withDisplayTitle(result.session, result.agentName, aliases)
541
+ session: aliases.decorate(result.session, result.reference)
459
542
  }));
460
543
  const aliasResults = findAliasSearchResults(query, searchOptions, scanResult, aliases);
461
544
  const deduped = /* @__PURE__ */ new Map();
462
545
  for (const result of [...aliasResults, ...results]) {
463
- deduped.set(`${result.agentName}\0${result.session.id}`, result);
546
+ deduped.set(`${result.reference.agentName}\0${result.reference.sessionId}`, result);
464
547
  }
465
548
  return c.json({ results: [...deduped.values()].slice(0, searchOptions.limit ?? 50) });
466
549
  }
467
- function parseFileActivityKind(value) {
468
- if (value === "read" || value === "edit" || value === "write" || value === "delete") {
469
- return value;
470
- }
471
- return void 0;
472
- }
473
- function optionalQueryValue(value) {
474
- const normalized = value?.trim();
475
- return normalized ? normalized : void 0;
476
- }
477
- function parseProjectIdentityFilter(kindValue, keyValue) {
478
- const kind = optionalQueryValue(kindValue);
479
- const key = optionalQueryValue(keyValue);
480
- if (!kind && !key) return void 0;
481
- if (!kind || !key || !isProjectIdentityKind(kind)) return null;
482
- return { kind, key };
483
- }
484
550
  function handleGetFileActivity(c, defaults = {}) {
485
551
  const limitValue = Number(c.req.query("limit"));
486
552
  const limit = Number.isFinite(limitValue) && limitValue > 0 ? Math.min(limitValue, 200) : 50;
@@ -491,7 +557,7 @@ function handleGetFileActivity(c, defaults = {}) {
491
557
  if (projectIdentity === null) {
492
558
  return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
493
559
  }
494
- const aliases = loadSessionAliasMap();
560
+ const aliases = loadAliasView();
495
561
  return c.json({
496
562
  activity: listFileActivity({
497
563
  agent: optionalQueryValue(c.req.query("agent")),
@@ -505,12 +571,11 @@ function handleGetFileActivity(c, defaults = {}) {
505
571
  from: parseDateParam(c.req.query("from"), defaults.from),
506
572
  to: parseDateParam(c.req.query("to"), defaults.to),
507
573
  limit
508
- }).map((activity) => withFileActivityDisplayTitle(activity, aliases))
574
+ }).map((activity) => decorateFileActivity(activity, aliases))
509
575
  });
510
576
  }
511
577
  async function handleGetSessionData(c, scanSource) {
512
578
  const startedAt = performance.now();
513
- const scanResult = scanSource.getSnapshot();
514
579
  const agentName = c.req.param("agent");
515
580
  const sessionId = c.req.param("id");
516
581
  if (!agentName) {
@@ -519,21 +584,15 @@ async function handleGetSessionData(c, scanSource) {
519
584
  if (!sessionId) {
520
585
  return c.json({ error: "Missing session ID" }, 400);
521
586
  }
522
- const agent = scanResult.agents.find((a) => a.name === agentName);
523
- if (!agent) {
524
- return c.json({ error: `Unknown agent: ${agentName}` }, 404);
525
- }
526
587
  try {
527
- const head = scanResult.byAgent[agentName]?.find((item) => item.id === sessionId);
528
- const loadStartedAt = performance.now();
529
- const cachedEntry = loadCachedSessionDataEntry(agentName, sessionId);
530
- const cachedData = cachedEntry?.data ?? null;
531
- const cachedMessageCount = cachedData?.stats.message_count ?? 0;
532
- const currentMeta = head ? agent.getSessionMetaMap().get(sessionId) : void 0;
533
- const cacheHasExpectedMessages = cachedData !== null && cacheMatchesCurrentSource(cachedEntry?.meta ?? null, currentMeta) && (cachedData.messages.length > 0 || cachedMessageCount === 0);
534
- const data = cacheHasExpectedMessages ? cachedData : head ? agent.getSessionData(sessionId) : null;
535
- const loadDuration = performance.now() - loadStartedAt;
536
- if (!data) {
588
+ const result = materializeSessionDetailResponse(scanSource.getSnapshot(), {
589
+ agentName,
590
+ sessionId
591
+ });
592
+ if (result.status === "unknown-agent") {
593
+ return c.json({ error: `Unknown agent: ${agentName}` }, 404);
594
+ }
595
+ if (result.status === "not-ready") {
537
596
  appLogger.warn("api.session_data.cache_miss", {
538
597
  agent: agentName,
539
598
  session_id: sessionId,
@@ -541,27 +600,20 @@ async function handleGetSessionData(c, scanSource) {
541
600
  });
542
601
  return c.json({ error: "Session cache not ready" }, 404);
543
602
  }
544
- const tagStartedAt = performance.now();
545
- const smartTags = data.smart_tags ?? classifySessionTags(data);
546
- const tagDuration = performance.now() - tagStartedAt;
547
- const projectIdentity = data.project_identity ?? head?.project_identity ?? computeIdentity(data.directory, realFs);
548
- const fileActivity = data.file_activity ?? (cacheHasExpectedMessages && cachedData ? listSessionFileActivity(agentName, sessionId) : extractSessionFileActivity(agentName, sessionId, projectIdentity.key, data.messages));
549
603
  appLogger.info("api.session_data", {
550
604
  agent: agentName,
551
605
  session_id: sessionId,
552
- messages: data.messages.length,
553
- load_duration_ms: Math.round(loadDuration),
554
- tag_duration_ms: Math.round(tagDuration),
606
+ messages: result.status === "found-json" ? result.messageCount : result.data.messages.length,
555
607
  duration_ms: Math.round(performance.now() - startedAt)
556
608
  });
557
- const aliases = loadSessionAliasMap();
558
- return c.json({
559
- ...withDisplayTitle(data, agentName, aliases),
560
- project_identity: projectIdentity,
561
- smart_tags: smartTags,
562
- smart_tags_source_updated_at: getSmartTagSourceTimestamp(data),
563
- file_activity: fileActivity
564
- });
609
+ const aliases = loadAliasView();
610
+ if (result.status === "found-json") {
611
+ return createSessionDetailJsonResponse(
612
+ aliases.decorate(result.data, result.data.reference),
613
+ result.messages
614
+ );
615
+ }
616
+ return c.json(aliases.decorate(result.data, result.data.reference));
565
617
  } catch (err) {
566
618
  const message = err instanceof Error ? err.message : "Failed to load session";
567
619
  appLogger.error("api.session_data.error", {
@@ -584,32 +636,26 @@ async function handlePostClientLog(c) {
584
636
  return c.json({ ok: true });
585
637
  }
586
638
  function handleGetBookmarks(c) {
587
- try {
588
- const aliases = loadSessionAliasMap();
589
- return c.json({
590
- bookmarks: listBookmarks().map((bookmark) => withBookmarkDisplayTitle(bookmark, aliases)),
591
- storageAvailable: true
592
- });
593
- } catch (error) {
594
- if (error instanceof StateStorageUnavailableError) {
595
- return c.json({ bookmarks: [], storageAvailable: false });
596
- }
597
- throw error;
598
- }
639
+ return withStorageErrors(
640
+ () => {
641
+ const aliases = loadAliasView();
642
+ return c.json({
643
+ bookmarks: listBookmarks().map((bookmark) => decorateBookmark(bookmark, aliases)),
644
+ storageAvailable: true
645
+ });
646
+ },
647
+ () => c.json({ bookmarks: [], storageAvailable: false })
648
+ );
599
649
  }
600
650
  async function handlePutBookmark(c) {
601
651
  const payload = parseBookmarkPayload(await c.req.json().catch(() => null));
602
652
  if (!payload) {
603
653
  return c.json({ error: "Invalid bookmark payload" }, 400);
604
654
  }
605
- try {
606
- return c.json({ bookmark: upsertBookmark(payload), storageAvailable: true });
607
- } catch (error) {
608
- if (error instanceof StateStorageUnavailableError) {
609
- return c.json({ error: "Bookmark storage is unavailable" }, 503);
610
- }
611
- throw error;
612
- }
655
+ return withStorageErrors(
656
+ () => c.json({ bookmark: upsertBookmark(payload), storageAvailable: true }),
657
+ () => c.json({ error: "Bookmark storage is unavailable" }, 503)
658
+ );
613
659
  }
614
660
  async function handleImportBookmarks(c) {
615
661
  const payload = await c.req.json().catch(() => null);
@@ -620,14 +666,10 @@ async function handleImportBookmarks(c) {
620
666
  if (bookmarks.length !== payload.length) {
621
667
  return c.json({ error: "Invalid bookmark payload" }, 400);
622
668
  }
623
- try {
624
- return c.json({ bookmarks: importBookmarks(bookmarks), storageAvailable: true });
625
- } catch (error) {
626
- if (error instanceof StateStorageUnavailableError) {
627
- return c.json({ error: "Bookmark storage is unavailable" }, 503);
628
- }
629
- throw error;
630
- }
669
+ return withStorageErrors(
670
+ () => c.json({ bookmarks: importBookmarks(bookmarks), storageAvailable: true }),
671
+ () => c.json({ error: "Bookmark storage is unavailable" }, 503)
672
+ );
631
673
  }
632
674
  function handleDeleteBookmark(c) {
633
675
  const agentKey = c.req.param("agent");
@@ -635,32 +677,35 @@ function handleDeleteBookmark(c) {
635
677
  if (!agentKey || !sessionId) {
636
678
  return c.json({ error: "Missing bookmark identifier" }, 400);
637
679
  }
638
- try {
639
- deleteBookmark(agentKey, sessionId);
640
- return c.json({ ok: true, storageAvailable: true });
641
- } catch (error) {
642
- if (error instanceof StateStorageUnavailableError) {
643
- return c.json({ error: "Bookmark storage is unavailable" }, 503);
644
- }
645
- throw error;
646
- }
680
+ return withStorageErrors(
681
+ () => {
682
+ deleteBookmark({ agentName: agentKey, sessionId });
683
+ return c.json({ ok: true, storageAvailable: true });
684
+ },
685
+ () => c.json({ error: "Bookmark storage is unavailable" }, 503)
686
+ );
647
687
  }
648
688
  async function handlePutSessionAlias(c) {
649
689
  const agentKey = c.req.param("agent");
650
690
  const sessionId = c.req.param("id");
651
691
  const payload = await c.req.json().catch(() => null);
652
- if (!agentKey || !sessionId || typeof payload?.alias !== "string") {
692
+ const aliasValue = payload?.alias;
693
+ if (!agentKey || !sessionId || typeof aliasValue !== "string") {
653
694
  return c.json({ error: "Invalid session alias payload" }, 400);
654
695
  }
655
696
  try {
656
- return c.json({ alias: upsertSessionAlias(agentKey, sessionId, payload.alias) });
697
+ return withStorageErrors(
698
+ () => {
699
+ const alias = upsertSessionAlias({ agentName: agentKey, sessionId }, aliasValue);
700
+ invalidateAliasView();
701
+ return c.json({ alias });
702
+ },
703
+ () => c.json({ error: "Session alias storage is unavailable" }, 503)
704
+ );
657
705
  } catch (error) {
658
- if (error instanceof TypeError) {
706
+ if (error instanceof SessionAliasValidationError) {
659
707
  return c.json({ error: "Session alias must be non-empty and at most 160 characters" }, 400);
660
708
  }
661
- if (isStateStorageUnavailable(error)) {
662
- return c.json({ error: "Session alias storage is unavailable" }, 503);
663
- }
664
709
  throw error;
665
710
  }
666
711
  }
@@ -670,15 +715,14 @@ function handleDeleteSessionAlias(c) {
670
715
  if (!agentKey || !sessionId) {
671
716
  return c.json({ error: "Missing session alias identifier" }, 400);
672
717
  }
673
- try {
674
- deleteSessionAlias(agentKey, sessionId);
675
- return c.json({ ok: true });
676
- } catch (error) {
677
- if (isStateStorageUnavailable(error)) {
678
- return c.json({ error: "Session alias storage is unavailable" }, 503);
679
- }
680
- throw error;
681
- }
718
+ return withStorageErrors(
719
+ () => {
720
+ deleteSessionAlias({ agentName: agentKey, sessionId });
721
+ invalidateAliasView();
722
+ return c.json({ ok: true });
723
+ },
724
+ () => c.json({ error: "Session alias storage is unavailable" }, 503)
725
+ );
682
726
  }
683
727
  function handleGetDashboard(c, scanSource, defaults = {}) {
684
728
  const scanResult = scanSource.getSnapshot();
@@ -703,15 +747,30 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
703
747
  projectKind: projectIdentity?.kind,
704
748
  projectKey: projectIdentity?.key
705
749
  };
706
- const agentInfo = getAgentInfoMap({});
707
- const agentInfoMap = new Map(agentInfo.map((a) => [a.name, a]));
708
- const aggregate = buildDashboard(scanResult.sessions, {
709
- byAgentNames: Object.keys(scanResult.byAgent),
710
- scope,
711
- from,
712
- to,
713
- agentInfoMap
714
- });
750
+ const fixedTo = parseDateParam(c.req.query("to"), defaults.to);
751
+ const aggregate = getSnapshotAggregation(
752
+ scanSource,
753
+ scanResult.sessions,
754
+ [
755
+ "dashboard",
756
+ scope.agent,
757
+ scope.projectKind,
758
+ scope.projectKey,
759
+ from,
760
+ fixedTo ?? startOfLocalDay(to)
761
+ ],
762
+ () => {
763
+ const agentInfo = getAgentInfoMap({});
764
+ const agentInfoMap = new Map(agentInfo.map((agent) => [agent.name, agent]));
765
+ return buildDashboard(scanResult.sessions, {
766
+ byAgentNames: Object.keys(scanResult.byAgent),
767
+ scope,
768
+ from,
769
+ to,
770
+ agentInfoMap
771
+ });
772
+ }
773
+ );
715
774
  const data = {
716
775
  ...aggregate,
717
776
  recentFileActivities: listFileActivity({
@@ -724,14 +783,15 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
724
783
  }),
725
784
  window: { from, to, days }
726
785
  };
727
- const aliases = loadSessionAliasMap();
786
+ const aliases = loadAliasView();
728
787
  return c.json({
729
788
  ...data,
730
- recentSessions: data.recentSessions.map(
731
- (session) => withDisplayTitle(session, session.agentName, aliases)
732
- ),
789
+ recentSessions: data.recentSessions.map((item) => ({
790
+ ...item,
791
+ session: aliases.decorate(item.session, item.reference)
792
+ })),
733
793
  recentFileActivities: data.recentFileActivities.map(
734
- (activity) => withFileActivityDisplayTitle(activity, aliases)
794
+ (activity) => decorateFileActivity(activity, aliases)
735
795
  )
736
796
  });
737
797
  }
@@ -1016,6 +1076,227 @@ async function createServer(port, store, options = {}) {
1016
1076
  import { existsSync as existsSync4 } from "fs";
1017
1077
  import { fileURLToPath as fileURLToPath3 } from "url";
1018
1078
 
1079
+ // src/agent-operation-scheduler.ts
1080
+ var PENDING_REFRESH_DELAY_MS = 100;
1081
+ var MAX_ADAPTIVE_REFRESH_DELAY_MS = 3e4;
1082
+ var ADAPTIVE_REFRESH_DELAY_MULTIPLIER = 4;
1083
+ var AgentOperationScheduler = class {
1084
+ constructor(runRefresh) {
1085
+ this.runRefresh = runRefresh;
1086
+ }
1087
+ runRefresh;
1088
+ states = /* @__PURE__ */ new Map();
1089
+ operationGenerations = /* @__PURE__ */ new Map();
1090
+ operationTails = /* @__PURE__ */ new Map();
1091
+ isStopped = false;
1092
+ notify(agentName, delayMs) {
1093
+ if (this.isStopped) return;
1094
+ this.state(agentName).pendingSignalCount += 1;
1095
+ this.schedule(agentName, delayMs);
1096
+ }
1097
+ schedule(agentName, delayMs) {
1098
+ if (this.isStopped) return;
1099
+ const state = this.state(agentName);
1100
+ const adaptiveDelayMs = Math.min(
1101
+ state.lastRefreshDurationMs * ADAPTIVE_REFRESH_DELAY_MULTIPLIER,
1102
+ MAX_ADAPTIVE_REFRESH_DELAY_MS
1103
+ );
1104
+ const effectiveDelayMs = Math.max(delayMs, adaptiveDelayMs);
1105
+ const deadline = Date.now() + effectiveDelayMs;
1106
+ if (state.timer) {
1107
+ if (deadline >= state.timerDeadline) return;
1108
+ clearTimeout(state.timer);
1109
+ }
1110
+ appLogger.debug("scan.refresh.schedule", { agent: agentName, delay_ms: effectiveDelayMs });
1111
+ state.timerDeadline = deadline;
1112
+ state.timer = setTimeout(() => {
1113
+ state.timer = null;
1114
+ state.timerDeadline = 0;
1115
+ void this.refresh(agentName);
1116
+ }, effectiveDelayMs);
1117
+ }
1118
+ async refresh(agentName) {
1119
+ if (this.isStopped) return;
1120
+ const state = this.state(agentName);
1121
+ if (state.isRefreshRunning) {
1122
+ appLogger.debug("scan.refresh.pending", { agent: agentName });
1123
+ state.hasPendingRefresh = true;
1124
+ return;
1125
+ }
1126
+ state.isRefreshRunning = true;
1127
+ try {
1128
+ await this.run(agentName, "refresh", () => this.runRefresh(agentName));
1129
+ } finally {
1130
+ state.isRefreshRunning = false;
1131
+ if (state.hasPendingRefresh && !this.isStopped) {
1132
+ state.hasPendingRefresh = false;
1133
+ this.schedule(agentName, PENDING_REFRESH_DELAY_MS);
1134
+ }
1135
+ }
1136
+ }
1137
+ run(agentName, kind, operation) {
1138
+ const previous = this.operationTails.get(agentName) ?? Promise.resolve();
1139
+ const run = previous.then(async () => {
1140
+ if (this.isStopped) return "skipped";
1141
+ const lifecycle = this.beginOperation(agentName, kind);
1142
+ try {
1143
+ const result = await operation();
1144
+ this.completeOperation(lifecycle, result);
1145
+ return result;
1146
+ } catch (error) {
1147
+ this.completeOperation(lifecycle, "failed");
1148
+ throw error;
1149
+ }
1150
+ });
1151
+ const tail = run.then(
1152
+ () => void 0,
1153
+ () => void 0
1154
+ );
1155
+ this.operationTails.set(agentName, tail);
1156
+ void tail.finally(() => {
1157
+ if (this.operationTails.get(agentName) === tail) this.operationTails.delete(agentName);
1158
+ });
1159
+ return run;
1160
+ }
1161
+ takePendingSignalCount(agentName) {
1162
+ const state = this.state(agentName);
1163
+ const count = state.pendingSignalCount;
1164
+ state.pendingSignalCount = 0;
1165
+ return count;
1166
+ }
1167
+ recordRefreshDuration(agentName, durationMs) {
1168
+ this.state(agentName).lastRefreshDurationMs = durationMs;
1169
+ }
1170
+ snapshot() {
1171
+ return {
1172
+ activeOperations: this.operationTails.size,
1173
+ activeRefreshes: [...this.states.values()].filter((state) => state.isRefreshRunning).length
1174
+ };
1175
+ }
1176
+ stop() {
1177
+ this.isStopped = true;
1178
+ for (const state of this.states.values()) {
1179
+ if (state.timer) clearTimeout(state.timer);
1180
+ state.timer = null;
1181
+ state.timerDeadline = 0;
1182
+ state.hasPendingRefresh = false;
1183
+ state.pendingSignalCount = 0;
1184
+ }
1185
+ }
1186
+ async waitForIdle() {
1187
+ await Promise.allSettled(this.operationTails.values());
1188
+ }
1189
+ state(agentName) {
1190
+ const existing = this.states.get(agentName);
1191
+ if (existing) return existing;
1192
+ const state = {
1193
+ timer: null,
1194
+ timerDeadline: 0,
1195
+ isRefreshRunning: false,
1196
+ hasPendingRefresh: false,
1197
+ lastRefreshDurationMs: 0,
1198
+ pendingSignalCount: 0
1199
+ };
1200
+ this.states.set(agentName, state);
1201
+ return state;
1202
+ }
1203
+ beginOperation(agentName, kind) {
1204
+ const generation = (this.operationGenerations.get(agentName) ?? 0) + 1;
1205
+ const startedAt = Date.now();
1206
+ this.operationGenerations.set(agentName, generation);
1207
+ appLogger.info("scan.agent_operation.started", {
1208
+ agent: agentName,
1209
+ operation: kind,
1210
+ generation,
1211
+ started_at: startedAt
1212
+ });
1213
+ return { agentName, kind, generation, startedAt };
1214
+ }
1215
+ completeOperation(lifecycle, result) {
1216
+ const completedAt = Date.now();
1217
+ appLogger.info("scan.agent_operation.completed", {
1218
+ agent: lifecycle.agentName,
1219
+ operation: lifecycle.kind,
1220
+ generation: lifecycle.generation,
1221
+ started_at: lifecycle.startedAt,
1222
+ completed_at: completedAt,
1223
+ duration_ms: completedAt - lifecycle.startedAt,
1224
+ result
1225
+ });
1226
+ }
1227
+ };
1228
+
1229
+ // src/live-session-index.ts
1230
+ var LiveSessionIndex = class {
1231
+ agents = [];
1232
+ agentsByName = /* @__PURE__ */ new Map();
1233
+ byAgent = {};
1234
+ sessions = [];
1235
+ signatureCaches = /* @__PURE__ */ new Map();
1236
+ initialize(snapshot, options = {}) {
1237
+ const agentMap = /* @__PURE__ */ new Map();
1238
+ for (const agent of snapshot.agents) agentMap.set(agent.name, agent);
1239
+ for (const agent of options.registeredAgents ?? []) {
1240
+ if (!agentMap.has(agent.name)) agentMap.set(agent.name, agent);
1241
+ }
1242
+ this.agents = [...agentMap.values()].filter(
1243
+ (agent) => !options.allowedAgents || options.allowedAgents.has(agent.name.toLowerCase())
1244
+ );
1245
+ this.agentsByName = new Map(this.agents.map((agent) => [agent.name, agent]));
1246
+ this.byAgent = Object.fromEntries(
1247
+ this.agents.map((agent) => [agent.name, sortSessions(snapshot.byAgent[agent.name] ?? [])])
1248
+ );
1249
+ this.sessions = mergeSortedSessions(Object.values(this.byAgent));
1250
+ this.signatureCaches.clear();
1251
+ }
1252
+ snapshot() {
1253
+ return {
1254
+ agents: this.agents,
1255
+ byAgent: this.byAgent,
1256
+ sessions: this.sessions
1257
+ };
1258
+ }
1259
+ findAgent(agentName) {
1260
+ return this.agentsByName.get(agentName);
1261
+ }
1262
+ commitAgentSessions(agentName, nextSessions, candidateChangedIds = []) {
1263
+ const previousSessions = this.byAgent[agentName] ?? [];
1264
+ const signatureCache = this.signatureCache(agentName);
1265
+ const { changes, removedSessionIds, counts } = computeSessionDiff(
1266
+ previousSessions,
1267
+ nextSessions,
1268
+ candidateChangedIds,
1269
+ sessionSignature,
1270
+ signatureCache
1271
+ );
1272
+ for (const removedId of removedSessionIds) signatureCache.delete(removedId);
1273
+ this.byAgent[agentName] = sortSessions(nextSessions);
1274
+ this.sessions = mergeSortedSessions(Object.values(this.byAgent));
1275
+ if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) return null;
1276
+ return {
1277
+ type: "sessions-updated",
1278
+ changedAgents: [agentName],
1279
+ newSessions: counts.new,
1280
+ updatedSessions: counts.updated,
1281
+ removedSessions: counts.removed,
1282
+ totalSessions: this.sessions.length,
1283
+ timestamp: Date.now(),
1284
+ changedSessionHeads: changes.map(({ session }) => ({
1285
+ reference: { agentName, sessionId: session.id },
1286
+ session
1287
+ })),
1288
+ removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
1289
+ };
1290
+ }
1291
+ signatureCache(agentName) {
1292
+ const existing = this.signatureCaches.get(agentName);
1293
+ if (existing) return existing;
1294
+ const cache = /* @__PURE__ */ new Map();
1295
+ this.signatureCaches.set(agentName, cache);
1296
+ return cache;
1297
+ }
1298
+ };
1299
+
1019
1300
  // src/search-index-job-runner.ts
1020
1301
  import { existsSync as existsSync2 } from "fs";
1021
1302
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -1168,7 +1449,6 @@ var SearchIndexJobRunner = class {
1168
1449
  nextBatchId = 1;
1169
1450
  pendingJobs = new PendingSearchIndexJobs();
1170
1451
  isShuttingDown = false;
1171
- hasCheckedFtsIntegrity = false;
1172
1452
  enqueue(context, jobs) {
1173
1453
  if (jobs.length === 0) return Promise.resolve();
1174
1454
  if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE));
@@ -1241,8 +1521,7 @@ var SearchIndexJobRunner = class {
1241
1521
  jobs: batch.jobs,
1242
1522
  agentNames: [],
1243
1523
  sessionsByAgent: {},
1244
- metaByAgent: {},
1245
- skipFtsIntegrityCheck: this.hasCheckedFtsIntegrity
1524
+ metaByAgent: {}
1246
1525
  }
1247
1526
  });
1248
1527
  worker.unref();
@@ -1258,7 +1537,6 @@ var SearchIndexJobRunner = class {
1258
1537
  duration_ms: Math.round(message.durationMs),
1259
1538
  sessions: message.sessions
1260
1539
  });
1261
- this.hasCheckedFtsIntegrity = true;
1262
1540
  this.settle(batch);
1263
1541
  });
1264
1542
  worker.on("error", (error) => {
@@ -1414,6 +1692,21 @@ var ScanStatusModel = class {
1414
1692
  updatedAt: now
1415
1693
  });
1416
1694
  }
1695
+ indexAgent(agentName) {
1696
+ const status = this.status.agentStatuses[agentName];
1697
+ if (!this.status.active || !status || status.status !== "scanning") return null;
1698
+ const now = Date.now();
1699
+ const agentStatuses = {
1700
+ ...this.status.agentStatuses,
1701
+ [agentName]: { ...status, status: "indexing", updatedAt: now }
1702
+ };
1703
+ return this.set({
1704
+ ...this.status,
1705
+ phase: this.activePhase(this.status.pendingAgents, this.status.scanningAgents, agentStatuses),
1706
+ agentStatuses,
1707
+ updatedAt: now
1708
+ });
1709
+ }
1417
1710
  finishAgent(agentName, sessionCount) {
1418
1711
  const pendingAgents = this.status.pendingAgents.filter((agent) => agent !== agentName);
1419
1712
  const scanningAgents = this.status.scanningAgents.filter((agent) => agent !== agentName);
@@ -1422,26 +1715,27 @@ var ScanStatusModel = class {
1422
1715
  const now = Date.now();
1423
1716
  const previousStatus = this.status.agentStatuses[agentName];
1424
1717
  const total = previousStatus?.total ?? previousStatus?.processed;
1718
+ const agentStatuses = {
1719
+ ...this.status.agentStatuses,
1720
+ [agentName]: {
1721
+ agentName,
1722
+ status: "complete",
1723
+ total,
1724
+ processed: total,
1725
+ sessions: sessionCount ?? previousStatus?.sessions ?? 0,
1726
+ startedAt: previousStatus?.startedAt,
1727
+ updatedAt: now,
1728
+ completedAt: now
1729
+ }
1730
+ };
1425
1731
  return this.set({
1426
1732
  ...this.status,
1427
1733
  active: isActive,
1428
- phase: isActive ? "scanning" : "idle",
1734
+ phase: isActive ? this.activePhase(pendingAgents, scanningAgents, agentStatuses) : "idle",
1429
1735
  pendingAgents,
1430
1736
  scanningAgents,
1431
1737
  completedAgents,
1432
- agentStatuses: {
1433
- ...this.status.agentStatuses,
1434
- [agentName]: {
1435
- agentName,
1436
- status: "complete",
1437
- total,
1438
- processed: total,
1439
- sessions: sessionCount ?? previousStatus?.sessions ?? 0,
1440
- startedAt: previousStatus?.startedAt,
1441
- updatedAt: now,
1442
- completedAt: now
1443
- }
1444
- },
1738
+ agentStatuses,
1445
1739
  updatedAt: now,
1446
1740
  completedAt: isActive ? void 0 : now
1447
1741
  });
@@ -1471,6 +1765,12 @@ var ScanStatusModel = class {
1471
1765
  updatedAt: Date.now()
1472
1766
  });
1473
1767
  }
1768
+ activePhase(pendingAgents, scanningAgents, agentStatuses) {
1769
+ if (pendingAgents.length === 0 && scanningAgents.length > 0 && scanningAgents.every((agentName) => agentStatuses[agentName]?.status === "indexing")) {
1770
+ return "indexing";
1771
+ }
1772
+ return this.status.phase === "initializing" ? "initializing" : "scanning";
1773
+ }
1474
1774
  set(status) {
1475
1775
  this.status = status;
1476
1776
  return this.snapshot();
@@ -1480,38 +1780,16 @@ var ScanStatusModel = class {
1480
1780
  // src/agent-sync-engine.ts
1481
1781
  var REFRESH_DEBOUNCE_MS = 200;
1482
1782
  var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
1483
- var PENDING_REFRESH_DELAY_MS = 100;
1484
- var MAX_ADAPTIVE_REFRESH_DELAY_MS = 3e4;
1485
- var ADAPTIVE_REFRESH_DELAY_MULTIPLIER = 4;
1486
1783
  var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
1487
1784
  var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1488
- function buildRefreshDiff(agentName, previousSessions, nextSessions, candidateChangedIds = [], signatureCache) {
1489
- const { changes, removedSessionIds, counts } = computeSessionDiff(
1785
+ function buildPersistenceDiff(previousSessions, nextSessions, candidateChangedIds = []) {
1786
+ const { changes, removedSessionIds } = computeSessionDiff(
1490
1787
  previousSessions,
1491
1788
  nextSessions,
1492
1789
  candidateChangedIds,
1493
- sessionSignature,
1494
- signatureCache
1790
+ sessionSignature
1495
1791
  );
1496
- for (const removedId of removedSessionIds) signatureCache?.delete(removedId);
1497
- if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) {
1498
- return { event: null, changedSessions: changes, removedSessionIds };
1499
- }
1500
- return {
1501
- changedSessions: changes,
1502
- removedSessionIds,
1503
- event: {
1504
- type: "sessions-updated",
1505
- changedAgents: [agentName],
1506
- newSessions: counts.new,
1507
- updatedSessions: counts.updated,
1508
- removedSessions: counts.removed,
1509
- totalSessions: nextSessions.length,
1510
- timestamp: Date.now(),
1511
- changedSessionHeads: changes.map(({ session }) => ({ agentName, session })),
1512
- removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
1513
- }
1514
- };
1792
+ return { changedSessions: changes, removedSessionIds };
1515
1793
  }
1516
1794
  function restoreAgentCacheMeta(agent, cached) {
1517
1795
  agent.setSessionMetaMap(new Map(Object.entries(cached.meta)));
@@ -1519,11 +1797,12 @@ function restoreAgentCacheMeta(agent, cached) {
1519
1797
  var AgentSyncEngine = class {
1520
1798
  constructor(options) {
1521
1799
  this.options = options;
1800
+ this.scheduler = new AgentOperationScheduler((agentName) => this.performRefresh(agentName));
1522
1801
  }
1523
1802
  options;
1524
- refreshStates = /* @__PURE__ */ new Map();
1525
- operationGenerations = /* @__PURE__ */ new Map();
1526
- operationTails = /* @__PURE__ */ new Map();
1803
+ lastRefreshAtByAgent = /* @__PURE__ */ new Map();
1804
+ scheduler;
1805
+ sessionIndex = new LiveSessionIndex();
1527
1806
  backfillQueue = [];
1528
1807
  currentBackfillAgent;
1529
1808
  completedBackfillAgents = [];
@@ -1532,13 +1811,22 @@ var AgentSyncEngine = class {
1532
1811
  statusChangedListeners = /* @__PURE__ */ new Set();
1533
1812
  scanStatus = new ScanStatusModel();
1534
1813
  searchIndexJobs = new SearchIndexJobRunner();
1814
+ nextPublicationId = 1;
1535
1815
  backgroundRefreshTimer = null;
1536
1816
  isShuttingDown = false;
1537
- initialize(cacheTimestamps = {}) {
1538
- for (const agent of this.options.snapshot().agents) {
1539
- this.state(agent.name).lastRefreshAt = cacheTimestamps[agent.name] ?? Date.now();
1817
+ initialize(snapshot, options = {}) {
1818
+ this.sessionIndex.initialize(snapshot, options);
1819
+ this.lastRefreshAtByAgent.clear();
1820
+ for (const agent of this.sessionIndex.snapshot().agents) {
1821
+ this.lastRefreshAtByAgent.set(
1822
+ agent.name,
1823
+ options.cacheTimestamps?.[agent.name] ?? Date.now()
1824
+ );
1540
1825
  }
1541
1826
  }
1827
+ snapshot() {
1828
+ return this.sessionIndex.snapshot();
1829
+ }
1542
1830
  status() {
1543
1831
  return this.scanStatus.snapshot();
1544
1832
  }
@@ -1551,49 +1839,45 @@ var AgentSyncEngine = class {
1551
1839
  return () => this.statusChangedListeners.delete(listener);
1552
1840
  }
1553
1841
  async syncInitialIndex() {
1554
- await this.searchIndexJobs.enqueue(
1555
- "scan.initial",
1556
- this.buildFullSearchIndexJobs("scan.initial")
1557
- );
1842
+ const jobs = this.buildFullSearchIndexJobs("scan.initial");
1843
+ await this.commitSearchIndex("scan.initial", jobs, {
1844
+ publicationId: this.publicationId("scan.initial"),
1845
+ agents: jobs.map((job) => job.agentName)
1846
+ });
1558
1847
  }
1559
1848
  handleAgentsChanged(agentNames) {
1560
- const snapshot = this.options.snapshot();
1849
+ const snapshot = this.sessionIndex.snapshot();
1561
1850
  for (const agentName of agentNames) {
1562
- this.state(agentName).pendingPathCount += 1;
1563
1851
  const delayMs = (snapshot.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
1564
- this.scheduleRefresh(agentName, delayMs);
1852
+ this.scheduler.notify(agentName, delayMs);
1565
1853
  }
1566
1854
  }
1567
1855
  startBackgroundRefresh() {
1568
1856
  if (this.backgroundRefreshTimer) return;
1569
- const agentNames = this.options.snapshot().agents.map((agent) => agent.name);
1857
+ const agentNames = this.sessionIndex.snapshot().agents.map((agent) => agent.name);
1570
1858
  this.startScanBatch(agentNames, "scanning");
1571
1859
  this.backgroundRefreshTimer = setTimeout(() => {
1572
1860
  this.backgroundRefreshTimer = null;
1573
- for (const agentName of agentNames) this.scheduleRefresh(agentName, 0);
1861
+ for (const agentName of agentNames) this.scheduler.schedule(agentName, 0);
1574
1862
  if (agentNames.length === 0) this.finishScanBatch();
1575
1863
  }, 0);
1576
1864
  }
1577
1865
  async refresh(agentName) {
1578
- await this.runCoalescedRefresh(agentName);
1866
+ await this.scheduler.refresh(agentName);
1579
1867
  }
1580
1868
  async shutdown() {
1581
1869
  this.isShuttingDown = true;
1870
+ const schedulerSnapshot = this.scheduler.snapshot();
1582
1871
  const activeOperations = {
1583
- agent_operations: this.operationTails.size,
1584
- refreshes: [...this.refreshStates.values()].filter((state) => state.isRunning).length,
1872
+ agent_operations: schedulerSnapshot.activeOperations,
1873
+ refreshes: schedulerSnapshot.activeRefreshes,
1585
1874
  backfill_running: this.currentBackfillAgent != null || void 0,
1586
1875
  scan_workers: this.options.workerRunner.activeCount
1587
1876
  };
1588
1877
  if (activeOperations.agent_operations > 0 || activeOperations.scan_workers > 0) {
1589
1878
  appLogger.warn("scan.shutdown.active_operations", activeOperations);
1590
1879
  }
1591
- for (const state of this.refreshStates.values()) {
1592
- if (!state.timer) continue;
1593
- clearTimeout(state.timer);
1594
- state.timer = null;
1595
- state.timerDeadline = 0;
1596
- }
1880
+ this.scheduler.stop();
1597
1881
  if (this.backgroundRefreshTimer) {
1598
1882
  clearTimeout(this.backgroundRefreshTimer);
1599
1883
  this.backgroundRefreshTimer = null;
@@ -1607,7 +1891,7 @@ var AgentSyncEngine = class {
1607
1891
  });
1608
1892
  await this.searchIndexJobs.shutdown();
1609
1893
  await this.options.workerRunner.shutdown();
1610
- await Promise.allSettled(this.operationTails.values());
1894
+ await this.scheduler.waitForIdle();
1611
1895
  const stoppedSearchIndexSnapshot = this.searchIndexJobs.snapshot();
1612
1896
  appLogger.info("search_index.shutdown.completed", {
1613
1897
  active_batch_id: searchIndexSnapshot.activeBatchId,
@@ -1615,7 +1899,7 @@ var AgentSyncEngine = class {
1615
1899
  });
1616
1900
  }
1617
1901
  startScanBatch(agentNames, phase) {
1618
- const snapshot = this.options.snapshot();
1902
+ const snapshot = this.sessionIndex.snapshot();
1619
1903
  const sessionCounts = Object.fromEntries(
1620
1904
  agentNames.map((agentName) => [agentName, snapshot.byAgent[agentName]?.length ?? 0])
1621
1905
  );
@@ -1625,7 +1909,7 @@ var AgentSyncEngine = class {
1625
1909
  this.publishStatus(this.scanStatus.setPhase(phase));
1626
1910
  }
1627
1911
  beginAgentScan(agentName) {
1628
- const snapshot = this.options.snapshot();
1912
+ const snapshot = this.sessionIndex.snapshot();
1629
1913
  if (!this.scanStatus.snapshot().active) this.startScanBatch([agentName], "scanning");
1630
1914
  this.publishStatus(
1631
1915
  this.scanStatus.beginAgent(agentName, snapshot.byAgent[agentName]?.length ?? 0)
@@ -1634,8 +1918,11 @@ var AgentSyncEngine = class {
1634
1918
  updateAgentScanProgress(agentName, progress) {
1635
1919
  this.publishStatus(this.scanStatus.updateAgent(agentName, progress));
1636
1920
  }
1921
+ beginAgentIndexing(agentName) {
1922
+ this.publishStatus(this.scanStatus.indexAgent(agentName));
1923
+ }
1637
1924
  finishAgentScan(agentName) {
1638
- const count = this.options.snapshot().byAgent[agentName]?.length;
1925
+ const count = this.sessionIndex.snapshot().byAgent[agentName]?.length;
1639
1926
  this.publishStatus(this.scanStatus.finishAgent(agentName, count));
1640
1927
  }
1641
1928
  finishScanBatch() {
@@ -1652,44 +1939,6 @@ var AgentSyncEngine = class {
1652
1939
  if (this.isShuttingDown) return;
1653
1940
  for (const listener of this.sessionsChangedListeners) listener(change);
1654
1941
  }
1655
- scheduleRefresh(agentName, delayMs) {
1656
- if (this.isShuttingDown) return;
1657
- const state = this.state(agentName);
1658
- const adaptiveDelayMs = Math.min(
1659
- state.lastRefreshDurationMs * ADAPTIVE_REFRESH_DELAY_MULTIPLIER,
1660
- MAX_ADAPTIVE_REFRESH_DELAY_MS
1661
- );
1662
- const effectiveDelayMs = Math.max(delayMs, adaptiveDelayMs);
1663
- const deadline = Date.now() + effectiveDelayMs;
1664
- if (state.timer) {
1665
- if (deadline >= state.timerDeadline) return;
1666
- clearTimeout(state.timer);
1667
- }
1668
- appLogger.debug("scan.refresh.schedule", { agent: agentName, delay_ms: effectiveDelayMs });
1669
- state.timerDeadline = deadline;
1670
- state.timer = setTimeout(() => {
1671
- state.timer = null;
1672
- void this.runCoalescedRefresh(agentName);
1673
- }, effectiveDelayMs);
1674
- }
1675
- async runCoalescedRefresh(agentName) {
1676
- const state = this.state(agentName);
1677
- if (state.isRunning) {
1678
- appLogger.debug("scan.refresh.pending", { agent: agentName });
1679
- state.hasPendingRerun = true;
1680
- return;
1681
- }
1682
- state.isRunning = true;
1683
- try {
1684
- await this.serialize(agentName, "refresh", () => this.performRefresh(agentName));
1685
- } finally {
1686
- state.isRunning = false;
1687
- if (state.hasPendingRerun && !this.isShuttingDown) {
1688
- state.hasPendingRerun = false;
1689
- this.scheduleRefresh(agentName, PENDING_REFRESH_DELAY_MS);
1690
- }
1691
- }
1692
- }
1693
1942
  async performRefresh(agentName) {
1694
1943
  this.beginAgentScan(agentName);
1695
1944
  try {
@@ -1706,18 +1955,16 @@ var AgentSyncEngine = class {
1706
1955
  }
1707
1956
  async runRefresh(agentName) {
1708
1957
  const startedAt = performance.now();
1709
- const state = this.state(agentName);
1710
- const pendingPathCount = state.pendingPathCount;
1711
- state.pendingPathCount = 0;
1958
+ const pendingPathCount = this.scheduler.takePendingSignalCount(agentName);
1712
1959
  const agent = this.findAgent(agentName);
1713
1960
  if (!agent) {
1714
1961
  appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
1715
1962
  return "skipped";
1716
1963
  }
1717
- const previousSessions = this.options.snapshot().byAgent[agentName] ?? [];
1964
+ const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
1718
1965
  const cached = loadCachedSessions(agentName);
1719
1966
  const refreshBaseline = cached?.sessions ?? previousSessions;
1720
- const cacheTimestamp = cached?.timestamp ?? state.lastRefreshAt;
1967
+ const cacheTimestamp = cached?.timestamp ?? this.lastRefreshAtByAgent.get(agentName) ?? 0;
1721
1968
  if (cached) restoreAgentCacheMeta(agent, cached);
1722
1969
  const isInitialized = isAgentCacheInitialized(agentName);
1723
1970
  const availabilityStartedAt = performance.now();
@@ -1742,74 +1989,59 @@ var AgentSyncEngine = class {
1742
1989
  }
1743
1990
  if (strategyResult.status === "unchanged") return "unchanged";
1744
1991
  const nextSessions = attachMissingProjectIdentities(strategyResult.nextSessions);
1745
- const diffStartedAt = performance.now();
1746
- const diff = buildRefreshDiff(
1747
- agentName,
1748
- previousSessions,
1749
- nextSessions,
1750
- strategyResult.preciseChangedIds ?? [],
1751
- state.signatureCache
1752
- );
1753
- const diffDuration = performance.now() - diffStartedAt;
1754
1992
  const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
1755
- const persistentChanges = strategyResult.persistenceDiff?.changedSessions ?? diff.changedSessions;
1756
- const persistentRemovedSessionIds = strategyResult.persistenceDiff?.removedSessionIds ?? diff.removedSessionIds;
1757
- const changedSessionIds = strategyResult.usedIncrementalScan ? new Set(persistentChanges.map(({ session }) => session.id)) : void 0;
1993
+ const persistenceDiff = strategyResult.persistenceDiff;
1994
+ const changedSessionIds = persistenceDiff ? new Set(persistenceDiff.changedSessions.map(({ session }) => session.id)) : void 0;
1758
1995
  const persistStartedAt = performance.now();
1759
- const persistentJob = strategyResult.usedIncrementalScan ? {
1996
+ const persistentJob = persistenceDiff ? {
1760
1997
  kind: "changes",
1761
1998
  context: "scan.refresh",
1762
1999
  agentName,
1763
- changes: persistentChanges,
1764
- removedSessionIds: persistentRemovedSessionIds,
2000
+ changes: persistenceDiff.changedSessions,
2001
+ removedSessionIds: persistenceDiff.removedSessionIds,
1765
2002
  meta: buildAgentCacheMeta(agent, changedSessionIds),
1766
2003
  ...searchIndexOptions ? { searchIndexOptions } : {}
1767
- } : strategyResult.fullScanSessions ? {
2004
+ } : {
1768
2005
  kind: "full",
1769
2006
  context: "scan.refresh",
1770
2007
  agentName,
1771
- sessions: strategyResult.fullScanSessions,
2008
+ sessions: strategyResult.fullScanSessions ?? nextSessions,
1772
2009
  meta: buildAgentCacheMeta(agent),
1773
2010
  saveCache: true,
1774
2011
  ...searchIndexOptions ? { searchIndexOptions } : {}
1775
- } : null;
1776
- if (persistentJob) {
1777
- const persist = this.searchIndexJobs.enqueue("scan.refresh", [persistentJob]);
1778
- if (!isInitialized && persistentJob.kind === "full") {
1779
- await persist;
1780
- } else {
1781
- void persist.catch((error) => {
1782
- appLogger.error("scan.refresh.persist.error", { agent: agentName, error });
1783
- console.error(`[${agentName}] Session persistence failed:`, error);
1784
- });
1785
- }
1786
- }
2012
+ };
2013
+ this.beginAgentIndexing(agentName);
2014
+ const publication = await this.commitSessionPublication({
2015
+ context: "scan.refresh",
2016
+ agentName,
2017
+ sessions: nextSessions,
2018
+ candidateChangedIds: strategyResult.preciseChangedIds ?? [],
2019
+ indexJob: persistentJob
2020
+ });
1787
2021
  const persistDuration = performance.now() - persistStartedAt;
1788
2022
  logSearchIndexSync("scan.refresh", null, { pending_paths: pendingPathCount });
1789
- this.emitSessionsChanged({ agentName, sessions: nextSessions, event: diff.event });
1790
2023
  const totalDurationMs = performance.now() - startedAt;
1791
- state.lastRefreshDurationMs = totalDurationMs;
2024
+ this.scheduler.recordRefreshDuration(agentName, totalDurationMs);
1792
2025
  appLogger.info("scan.refresh.done", {
1793
2026
  agent: agentName,
1794
2027
  duration_ms: Math.round(totalDurationMs),
1795
2028
  sessions: nextSessions.length,
1796
- new_sessions: diff.event?.newSessions ?? 0,
1797
- updated_sessions: diff.event?.updatedSessions ?? 0,
1798
- removed_sessions: diff.event?.removedSessions ?? 0,
2029
+ new_sessions: publication.event?.newSessions ?? 0,
2030
+ updated_sessions: publication.event?.updatedSessions ?? 0,
2031
+ removed_sessions: publication.event?.removedSessions ?? 0,
1799
2032
  pending_paths: pendingPathCount,
1800
2033
  availability_ms: Math.round(availabilityDuration),
1801
2034
  check_ms: Math.round(strategyResult.checkDuration),
1802
2035
  scan_ms: Math.round(strategyResult.scanDuration),
1803
- diff_ms: Math.round(diffDuration),
2036
+ diff_ms: Math.round(publication.diffDuration),
1804
2037
  persist_ms: Math.round(persistDuration),
1805
- search_index_ms: 0,
1806
- persistent_index_worker_job: persistentJob?.kind,
1807
- persistent_index_skipped: !persistentJob || void 0
2038
+ search_index_ms: Math.round(persistDuration),
2039
+ persistent_index_worker_job: persistentJob.kind
1808
2040
  });
1809
2041
  return "committed";
1810
2042
  }
1811
2043
  refreshUnavailableAgent(agentName) {
1812
- this.state(agentName).lastRefreshAt = Date.now();
2044
+ this.lastRefreshAtByAgent.set(agentName, Date.now());
1813
2045
  return this.refreshStrategyResult([]);
1814
2046
  }
1815
2047
  async initializeAgent(agent, previousSessions) {
@@ -1818,7 +2050,7 @@ var AgentSyncEngine = class {
1818
2050
  const result = await this.runWorker(agent, previousSessions, null, this.startupScanOptions());
1819
2051
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
1820
2052
  const sessions = attachMissingProjectIdentities(result.sessions);
1821
- this.state(agent.name).lastRefreshAt = Date.now();
2053
+ this.lastRefreshAtByAgent.set(agent.name, Date.now());
1822
2054
  return this.refreshStrategyResult(sessions, {
1823
2055
  fullScanSessions: sessions,
1824
2056
  scanDuration: performance.now() - scanStartedAt
@@ -1833,17 +2065,17 @@ var AgentSyncEngine = class {
1833
2065
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
1834
2066
  const sessions = attachMissingProjectIdentities(result.sessions);
1835
2067
  const preciseChangedIds = result.changedIds ?? [];
1836
- const persistenceDiff = buildRefreshDiff(
1837
- agent.name,
1838
- cached.sessions,
1839
- sessions,
1840
- preciseChangedIds
1841
- );
1842
- this.state(agent.name).lastRefreshAt = Date.now();
1843
- if (preciseChangedIds.length === 0) this.logUnchangedRefresh(agent.name, refreshStartedAt);
2068
+ const persistenceDiff = buildPersistenceDiff(cached.sessions, sessions, preciseChangedIds);
2069
+ this.lastRefreshAtByAgent.set(agent.name, Date.now());
2070
+ if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0) {
2071
+ this.logUnchangedRefresh(agent.name, refreshStartedAt);
2072
+ return this.refreshStrategyResult(sessions, {
2073
+ status: "unchanged",
2074
+ scanDuration: performance.now() - scanStartedAt
2075
+ });
2076
+ }
1844
2077
  return this.refreshStrategyResult(sessions, {
1845
2078
  preciseChangedIds,
1846
- usedIncrementalScan: true,
1847
2079
  persistenceDiff,
1848
2080
  scanDuration: performance.now() - scanStartedAt
1849
2081
  });
@@ -1852,22 +2084,29 @@ var AgentSyncEngine = class {
1852
2084
  const checkStartedAt = performance.now();
1853
2085
  const checkResult = await Promise.resolve(agent.checkForChanges(cacheTimestamp, baseline));
1854
2086
  const checkDuration = performance.now() - checkStartedAt;
1855
- this.state(agent.name).lastRefreshAt = checkResult.timestamp;
2087
+ this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
1856
2088
  if (!checkResult.hasChanges) {
1857
2089
  this.logUnchangedRefresh(agent.name, refreshStartedAt);
1858
2090
  return this.refreshStrategyResult(baseline, { status: "unchanged", checkDuration });
1859
2091
  }
1860
2092
  const preciseChangedIds = checkResult.changedIds ?? null;
1861
2093
  const scanStartedAt = performance.now();
2094
+ if (preciseChangedIds === null) {
2095
+ const result = await this.runWorker(agent, baseline, null, {});
2096
+ agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
2097
+ const sessions2 = attachMissingProjectIdentities(result.sessions);
2098
+ return this.refreshStrategyResult(sessions2, {
2099
+ persistenceDiff: buildPersistenceDiff(baseline, sessions2),
2100
+ checkDuration,
2101
+ scanDuration: performance.now() - scanStartedAt
2102
+ });
2103
+ }
1862
2104
  const sessions = attachMissingProjectIdentities(
1863
- await Promise.resolve(
1864
- agent.incrementalScan(baseline, checkResult.changedIds ?? [], checkResult.refs)
1865
- )
2105
+ await Promise.resolve(agent.incrementalScan(baseline, preciseChangedIds, checkResult.refs))
1866
2106
  );
1867
2107
  return this.refreshStrategyResult(sessions, {
1868
2108
  preciseChangedIds,
1869
- usedIncrementalScan: Array.isArray(checkResult.changedIds),
1870
- persistenceDiff: buildRefreshDiff(agent.name, baseline, sessions, preciseChangedIds ?? []),
2109
+ persistenceDiff: buildPersistenceDiff(baseline, sessions, preciseChangedIds),
1871
2110
  checkDuration,
1872
2111
  scanDuration: performance.now() - scanStartedAt
1873
2112
  });
@@ -1877,7 +2116,7 @@ var AgentSyncEngine = class {
1877
2116
  const result = await this.runWorker(agent, previousSessions, null, {});
1878
2117
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
1879
2118
  const sessions = attachMissingProjectIdentities(result.sessions);
1880
- this.state(agent.name).lastRefreshAt = Date.now();
2119
+ this.lastRefreshAtByAgent.set(agent.name, Date.now());
1881
2120
  return this.refreshStrategyResult(sessions, {
1882
2121
  fullScanSessions: sessions,
1883
2122
  scanDuration: performance.now() - scanStartedAt
@@ -1889,7 +2128,6 @@ var AgentSyncEngine = class {
1889
2128
  nextSessions,
1890
2129
  fullScanSessions: null,
1891
2130
  preciseChangedIds: null,
1892
- usedIncrementalScan: false,
1893
2131
  persistenceDiff: null,
1894
2132
  checkDuration: 0,
1895
2133
  scanDuration: 0,
@@ -1927,30 +2165,31 @@ var AgentSyncEngine = class {
1927
2165
  if (!agentName) return;
1928
2166
  this.currentBackfillAgent = agentName;
1929
2167
  this.publishBackfillStatus();
1930
- void this.serialize(agentName, "backfill", () => this.performBackfill(agentName)).then(
1931
- (result) => {
1932
- if (this.isShuttingDown) return;
1933
- this.currentBackfillAgent = void 0;
1934
- if (result === "committed") {
1935
- if (!this.completedBackfillAgents.includes(agentName)) {
1936
- this.completedBackfillAgents.push(agentName);
1937
- }
1938
- this.failedBackfillAgents = this.failedBackfillAgents.filter(
1939
- (failedAgent) => failedAgent !== agentName
1940
- );
1941
- } else if (!this.failedBackfillAgents.includes(agentName)) {
1942
- this.failedBackfillAgents.push(agentName);
2168
+ void this.runBackfill(agentName).then((result) => {
2169
+ if (this.isShuttingDown) return;
2170
+ this.currentBackfillAgent = void 0;
2171
+ if (result === "committed") {
2172
+ if (!this.completedBackfillAgents.includes(agentName)) {
2173
+ this.completedBackfillAgents.push(agentName);
1943
2174
  }
1944
- this.publishBackfillStatus();
1945
- this.pumpBackfillQueue();
2175
+ this.failedBackfillAgents = this.failedBackfillAgents.filter(
2176
+ (failedAgent) => failedAgent !== agentName
2177
+ );
2178
+ } else if (!this.failedBackfillAgents.includes(agentName)) {
2179
+ this.failedBackfillAgents.push(agentName);
1946
2180
  }
1947
- );
2181
+ this.publishBackfillStatus();
2182
+ this.pumpBackfillQueue();
2183
+ });
2184
+ }
2185
+ runBackfill(agentName) {
2186
+ return this.scheduler.run(agentName, "backfill", () => this.performBackfill(agentName));
1948
2187
  }
1949
2188
  async performBackfill(agentName) {
1950
2189
  const startedAt = performance.now();
1951
2190
  const agent = this.findAgent(agentName);
1952
2191
  if (!agent || !agent.isAvailable()) return "skipped";
1953
- const snapshot = this.options.snapshot();
2192
+ const snapshot = this.sessionIndex.snapshot();
1954
2193
  const cached = loadCachedSessions(agentName);
1955
2194
  const baseline = cached?.sessions ?? snapshot.byAgent[agentName] ?? [];
1956
2195
  const meta = cached?.meta ?? buildAgentCacheMeta(agent);
@@ -1968,14 +2207,12 @@ var AgentSyncEngine = class {
1968
2207
  );
1969
2208
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
1970
2209
  const fullSessions = attachMissingProjectIdentities(result.sessions);
1971
- const diff = buildRefreshDiff(
2210
+ await this.commitSessionPublication({
2211
+ context: "scan.backfill",
1972
2212
  agentName,
1973
- snapshot.byAgent[agentName] ?? [],
1974
- fullSessions,
1975
- result.changedIds ?? []
1976
- );
1977
- await this.searchIndexJobs.enqueue("scan.backfill", [
1978
- {
2213
+ sessions: fullSessions,
2214
+ candidateChangedIds: result.changedIds ?? [],
2215
+ indexJob: {
1979
2216
  kind: "full",
1980
2217
  context: "scan.backfill",
1981
2218
  agentName,
@@ -1983,9 +2220,8 @@ var AgentSyncEngine = class {
1983
2220
  meta: buildAgentCacheMeta(agent),
1984
2221
  saveCache: true
1985
2222
  }
1986
- ]);
2223
+ });
1987
2224
  markAgentFullSyncCompleted(agentName);
1988
- this.emitSessionsChanged({ agentName, sessions: fullSessions, event: diff.event });
1989
2225
  appLogger.info("scan.backfill.done", {
1990
2226
  agent: agentName,
1991
2227
  duration_ms: Math.round(performance.now() - startedAt),
@@ -1999,70 +2235,6 @@ var AgentSyncEngine = class {
1999
2235
  return "failed";
2000
2236
  }
2001
2237
  }
2002
- serialize(agentName, kind, operation) {
2003
- const previous = this.operationTails.get(agentName) ?? Promise.resolve();
2004
- const run = previous.then(async () => {
2005
- if (this.isShuttingDown) return "skipped";
2006
- const lifecycle = this.beginOperation(agentName, kind);
2007
- try {
2008
- const result = await operation();
2009
- this.completeOperation(lifecycle, result);
2010
- return result;
2011
- } catch (error) {
2012
- this.completeOperation(lifecycle, "failed");
2013
- throw error;
2014
- }
2015
- });
2016
- const tail = run.then(
2017
- () => void 0,
2018
- () => void 0
2019
- );
2020
- this.operationTails.set(agentName, tail);
2021
- void tail.finally(() => {
2022
- if (this.operationTails.get(agentName) === tail) this.operationTails.delete(agentName);
2023
- });
2024
- return run;
2025
- }
2026
- state(agentName) {
2027
- const existing = this.refreshStates.get(agentName);
2028
- if (existing) return existing;
2029
- const state = {
2030
- timer: null,
2031
- timerDeadline: 0,
2032
- isRunning: false,
2033
- hasPendingRerun: false,
2034
- lastRefreshAt: 0,
2035
- lastRefreshDurationMs: 0,
2036
- pendingPathCount: 0,
2037
- signatureCache: /* @__PURE__ */ new Map()
2038
- };
2039
- this.refreshStates.set(agentName, state);
2040
- return state;
2041
- }
2042
- beginOperation(agentName, kind) {
2043
- const generation = (this.operationGenerations.get(agentName) ?? 0) + 1;
2044
- const startedAt = Date.now();
2045
- this.operationGenerations.set(agentName, generation);
2046
- appLogger.info("scan.agent_operation.started", {
2047
- agent: agentName,
2048
- operation: kind,
2049
- generation,
2050
- started_at: startedAt
2051
- });
2052
- return { agentName, kind, generation, startedAt };
2053
- }
2054
- completeOperation(lifecycle, result) {
2055
- const completedAt = Date.now();
2056
- appLogger.info("scan.agent_operation.completed", {
2057
- agent: lifecycle.agentName,
2058
- operation: lifecycle.kind,
2059
- generation: lifecycle.generation,
2060
- started_at: lifecycle.startedAt,
2061
- completed_at: completedAt,
2062
- duration_ms: completedAt - lifecycle.startedAt,
2063
- result
2064
- });
2065
- }
2066
2238
  backfillStatus() {
2067
2239
  return {
2068
2240
  active: this.currentBackfillAgent != null || this.backfillQueue.length > 0,
@@ -2073,7 +2245,7 @@ var AgentSyncEngine = class {
2073
2245
  };
2074
2246
  }
2075
2247
  buildFullSearchIndexJobs(context) {
2076
- const snapshot = this.options.snapshot();
2248
+ const snapshot = this.sessionIndex.snapshot();
2077
2249
  return snapshot.agents.map((agent) => {
2078
2250
  const cached = loadCachedSessions(agent.name);
2079
2251
  return cached ? {
@@ -2091,8 +2263,65 @@ var AgentSyncEngine = class {
2091
2263
  };
2092
2264
  });
2093
2265
  }
2266
+ publicationId(context, agentName) {
2267
+ const id = this.nextPublicationId++;
2268
+ return agentName ? `${context}:${agentName}:${id}` : `${context}:${id}`;
2269
+ }
2270
+ async commitSearchIndex(context, jobs, details) {
2271
+ appLogger.info("session.publication.prepared", {
2272
+ publication_id: details.publicationId,
2273
+ context,
2274
+ agent: details.agent,
2275
+ agents: details.agents,
2276
+ jobs: jobs.length
2277
+ });
2278
+ try {
2279
+ await this.searchIndexJobs.enqueue(context, jobs);
2280
+ } catch (error) {
2281
+ appLogger.error("session.publication.failed", {
2282
+ publication_id: details.publicationId,
2283
+ context,
2284
+ agent: details.agent,
2285
+ stage: "search_index",
2286
+ error
2287
+ });
2288
+ throw error;
2289
+ }
2290
+ appLogger.info("session.publication.index_committed", {
2291
+ publication_id: details.publicationId,
2292
+ context,
2293
+ agent: details.agent
2294
+ });
2295
+ }
2296
+ async commitSessionPublication(publication) {
2297
+ const publicationId = this.publicationId(publication.context, publication.agentName);
2298
+ await this.commitSearchIndex(publication.context, [publication.indexJob], {
2299
+ publicationId,
2300
+ agent: publication.agentName
2301
+ });
2302
+ const diffStartedAt = performance.now();
2303
+ const event = this.sessionIndex.commitAgentSessions(
2304
+ publication.agentName,
2305
+ publication.sessions,
2306
+ publication.candidateChangedIds
2307
+ );
2308
+ const diffDuration = performance.now() - diffStartedAt;
2309
+ this.emitSessionsChanged({
2310
+ agentName: publication.agentName,
2311
+ sessions: this.sessionIndex.snapshot().byAgent[publication.agentName] ?? [],
2312
+ event
2313
+ });
2314
+ appLogger.info("session.publication.published", {
2315
+ publication_id: publicationId,
2316
+ context: publication.context,
2317
+ agent: publication.agentName,
2318
+ sessions: publication.sessions.length,
2319
+ has_event: event != null
2320
+ });
2321
+ return { event, diffDuration };
2322
+ }
2094
2323
  findAgent(agentName) {
2095
- return this.options.snapshot().agents.find((agent) => agent.name === agentName);
2324
+ return this.sessionIndex.findAgent(agentName);
2096
2325
  }
2097
2326
  startupScanOptions() {
2098
2327
  return this.options.startupScanOptions ?? {};
@@ -2167,49 +2396,6 @@ function resolveWatchEventPath(watchPath, filename) {
2167
2396
  }
2168
2397
  return isAbsolute(filenameText) ? filenameText : join(watchPath, filenameText);
2169
2398
  }
2170
- function resolveAgentWatchTargets(agentName) {
2171
- const roots = resolveProviderRoots();
2172
- const cursorDataPath = getCursorDataPath();
2173
- switch (agentName) {
2174
- case "claudecode":
2175
- return [
2176
- { root: roots.claudeRoot, path: join(roots.claudeRoot, "projects") },
2177
- { path: "data/claudecode" }
2178
- ];
2179
- case "codex":
2180
- return [
2181
- { path: join(roots.codexRoot, "sessions") },
2182
- { path: join(roots.codexRoot, "session_index.jsonl") }
2183
- ];
2184
- case "pi":
2185
- return [
2186
- { root: roots.piRoot, path: join(roots.piRoot, "agent", "sessions") },
2187
- { root: "data/pi", path: "data/pi" }
2188
- ];
2189
- case "cursor":
2190
- return cursorDataPath ? [
2191
- {
2192
- root: cursorDataPath,
2193
- path: join(cursorDataPath, "globalStorage", "state.vscdb")
2194
- },
2195
- { root: cursorDataPath, path: join(cursorDataPath, "workspaceStorage") }
2196
- ] : [];
2197
- case "kimi":
2198
- return [
2199
- { root: roots.kimiRoot, path: join(roots.kimiRoot, "sessions") },
2200
- { path: "data/kimi" }
2201
- ];
2202
- case "opencode":
2203
- return [
2204
- { root: roots.opencodeRoot, path: join(roots.opencodeRoot, "opencode.db") },
2205
- { root: "data/opencode", path: "data/opencode/opencode.db" }
2206
- ];
2207
- case "zcode":
2208
- return roots.zcodeRoot ? [{ root: roots.zcodeRoot, path: join(roots.zcodeRoot, "cli", "db", "db.sqlite") }] : [];
2209
- default:
2210
- return [];
2211
- }
2212
- }
2213
2399
  var SessionWatcher = class {
2214
2400
  watchers = [];
2215
2401
  fallbackWatchScopes = /* @__PURE__ */ new Map();
@@ -2222,16 +2408,24 @@ var SessionWatcher = class {
2222
2408
  this.listeners.delete(cb);
2223
2409
  };
2224
2410
  }
2225
- /** Begin watching the given agent names' data directories. */
2226
- start(agentNames) {
2411
+ /** Begin watching the session sources declared by each agent adapter. */
2412
+ start(agents) {
2227
2413
  const scopesByRoot = /* @__PURE__ */ new Map();
2228
- for (const agentName of agentNames) {
2229
- const watchTargets = resolveAgentWatchTargets(agentName);
2230
- if (watchTargets.length === 0) {
2231
- appLogger.debug("watch.skip", { agent: agentName });
2414
+ for (const agent of agents) {
2415
+ const plan = agent.getSessionWatchPlan();
2416
+ if (plan.status !== "supported") {
2417
+ appLogger.debug("watch.skip", {
2418
+ agent: agent.name,
2419
+ status: plan.status,
2420
+ reason: plan.reason
2421
+ });
2422
+ continue;
2423
+ }
2424
+ if (plan.targets.length === 0) {
2425
+ appLogger.debug("watch.skip", { agent: agent.name, status: plan.status });
2232
2426
  continue;
2233
2427
  }
2234
- for (const target of watchTargets) {
2428
+ for (const target of plan.targets) {
2235
2429
  const watchRootPath = closestWatchablePath(target.root ?? target.path);
2236
2430
  if (!watchRootPath) continue;
2237
2431
  let rootPath;
@@ -2243,17 +2437,17 @@ var SessionWatcher = class {
2243
2437
  }
2244
2438
  const targetPath = toAbsolutePath(target.path);
2245
2439
  const scopes = scopesByRoot.get(rootPath) ?? [];
2246
- if (!scopes.some((scope) => scope.agentName === agentName && scope.targetPath === targetPath)) {
2247
- scopes.push({ agentName, targetPath });
2440
+ if (!scopes.some((scope) => scope.agentName === agent.name && scope.targetPath === targetPath)) {
2441
+ scopes.push({ agentName: agent.name, targetPath });
2248
2442
  }
2249
2443
  scopesByRoot.set(rootPath, scopes);
2250
2444
  }
2251
2445
  }
2252
2446
  for (const [rootPath, scopes] of scopesByRoot.entries()) {
2253
- const agents = Array.from(new Set(scopes.map((scope) => scope.agentName)));
2447
+ const agents2 = Array.from(new Set(scopes.map((scope) => scope.agentName)));
2254
2448
  appLogger.info("watch.start", {
2255
2449
  root: rootPath,
2256
- agents,
2450
+ agents: agents2,
2257
2451
  targets: scopes.map((scope) => ({
2258
2452
  agent: scope.agentName,
2259
2453
  path: scope.targetPath
@@ -2425,71 +2619,154 @@ var SessionWatcher = class {
2425
2619
 
2426
2620
  // src/worker-runner.ts
2427
2621
  import { Worker as Worker2 } from "worker_threads";
2622
+ var SHUTDOWN_ERROR_MESSAGE2 = "Scan refresh worker shut down";
2623
+ function applySessionChanges2(previousSessions, changes, removedSessionIds) {
2624
+ const replacedIds = new Set(removedSessionIds);
2625
+ for (const { session } of changes) replacedIds.add(session.id);
2626
+ const retained = previousSessions.filter((session) => !replacedIds.has(session.id));
2627
+ const next = Array.from({
2628
+ length: retained.length + changes.length
2629
+ });
2630
+ for (const { session, sortIndex } of changes) {
2631
+ if (sortIndex < 0 || sortIndex >= next.length || next[sortIndex]) {
2632
+ throw new Error(`Invalid scan refresh sort index: ${sortIndex}`);
2633
+ }
2634
+ next[sortIndex] = session;
2635
+ }
2636
+ let retainedIndex = 0;
2637
+ for (let index = 0; index < next.length; index += 1) {
2638
+ if (!next[index]) next[index] = retained[retainedIndex++];
2639
+ }
2640
+ if (retainedIndex !== retained.length || next.some((session) => !session)) {
2641
+ throw new Error("Invalid scan refresh delta");
2642
+ }
2643
+ return next;
2644
+ }
2645
+ function applyMetaChanges(previous, changed, replacedSessionIds) {
2646
+ const next = { ...previous };
2647
+ for (const id of replacedSessionIds) delete next[id];
2648
+ return Object.assign(next, changed);
2649
+ }
2428
2650
  var ThreadWorkerRunner = class {
2429
2651
  constructor(workerUrl) {
2430
2652
  this.workerUrl = workerUrl;
2431
2653
  }
2432
2654
  workerUrl;
2433
- workers = /* @__PURE__ */ new Set();
2655
+ workers = /* @__PURE__ */ new Map();
2656
+ nextRequestId = 1;
2657
+ isShuttingDown = false;
2434
2658
  get activeCount() {
2435
- return this.workers.size;
2659
+ let count = 0;
2660
+ for (const slot of this.workers.values()) count += slot.pending.size;
2661
+ return count;
2436
2662
  }
2437
2663
  run(agentName, payload) {
2664
+ if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE2));
2665
+ const request = {
2666
+ type: "run",
2667
+ requestId: this.nextRequestId++,
2668
+ agentName,
2669
+ previousSessions: payload.previousSessions,
2670
+ changedIds: payload.changedIds,
2671
+ sourceSync: payload.sourceSync,
2672
+ scanOptions: payload.scanOptions,
2673
+ meta: payload.meta
2674
+ };
2438
2675
  return new Promise((resolve4, reject) => {
2439
- const worker = new Worker2(this.workerUrl, {
2440
- workerData: {
2441
- agentName,
2442
- previousSessions: payload.previousSessions,
2443
- changedIds: payload.changedIds,
2444
- sourceSync: payload.sourceSync,
2445
- scanOptions: payload.scanOptions,
2446
- meta: payload.meta
2447
- }
2448
- });
2449
- worker.unref();
2450
- this.workers.add(worker);
2451
- let settled = false;
2452
- const finish = (callback, terminate = true) => {
2453
- if (settled) return;
2454
- settled = true;
2455
- this.workers.delete(worker);
2456
- if (terminate) void worker.terminate();
2457
- callback();
2458
- };
2459
- worker.on("message", (message) => {
2460
- if (message.type === "progress") {
2461
- payload.onProgress?.(message.progress);
2462
- return;
2463
- }
2464
- if (message.type === "done") {
2465
- finish(
2466
- () => resolve4({
2467
- sessions: message.sessions,
2468
- meta: message.meta,
2469
- changedIds: message.changedIds
2470
- })
2471
- );
2676
+ let slot = this.workers.get(agentName);
2677
+ const isNewWorker = !slot;
2678
+ if (!slot) {
2679
+ try {
2680
+ slot = this.createWorker(agentName, request);
2681
+ } catch (error) {
2682
+ reject(error instanceof Error ? error : new Error(String(error)));
2472
2683
  return;
2473
2684
  }
2474
- finish(() => reject(new Error(message.error)));
2475
- });
2476
- worker.once("error", (error) => {
2477
- finish(() => reject(error));
2478
- });
2479
- worker.once("exit", (code) => {
2480
- if (settled) return;
2481
- appLogger.warn("scan.refresh_worker.exit_before_done", { agent: agentName, code });
2482
- finish(
2483
- () => reject(new Error(`Scan refresh worker exited before completing (code ${code})`)),
2484
- false
2485
- );
2685
+ }
2686
+ slot.pending.set(request.requestId, {
2687
+ resolve: resolve4,
2688
+ reject,
2689
+ payload,
2690
+ onProgress: payload.onProgress
2486
2691
  });
2692
+ if (!isNewWorker) {
2693
+ try {
2694
+ slot.worker.postMessage(request);
2695
+ } catch (error) {
2696
+ slot.pending.delete(request.requestId);
2697
+ reject(error instanceof Error ? error : new Error(String(error)));
2698
+ }
2699
+ }
2487
2700
  });
2488
2701
  }
2489
2702
  async shutdown() {
2490
- const workers = [...this.workers];
2491
- await Promise.allSettled(workers.map((worker) => worker.terminate()));
2703
+ this.isShuttingDown = true;
2704
+ const slots = [...this.workers.values()];
2492
2705
  this.workers.clear();
2706
+ const shutdownError = new Error(SHUTDOWN_ERROR_MESSAGE2);
2707
+ for (const slot of slots) {
2708
+ slot.closed = true;
2709
+ for (const pending of slot.pending.values()) pending.reject(shutdownError);
2710
+ slot.pending.clear();
2711
+ }
2712
+ await Promise.allSettled(slots.map((slot) => slot.worker.terminate()));
2713
+ }
2714
+ createWorker(agentName, request) {
2715
+ const worker = new Worker2(this.workerUrl, { workerData: request });
2716
+ const slot = { worker, pending: /* @__PURE__ */ new Map(), closed: false };
2717
+ worker.unref();
2718
+ this.workers.set(agentName, slot);
2719
+ worker.on("message", (message) => {
2720
+ this.handleMessage(slot, message);
2721
+ });
2722
+ worker.on("error", (error) => {
2723
+ this.closeWorker(agentName, slot, error);
2724
+ });
2725
+ worker.on("exit", (code) => {
2726
+ if (slot.closed) return;
2727
+ const error = new Error(`Scan refresh worker exited before completing (code ${code})`);
2728
+ if (slot.pending.size > 0) {
2729
+ appLogger.warn("scan.refresh_worker.exit_before_done", { agent: agentName, code });
2730
+ }
2731
+ this.closeWorker(agentName, slot, error);
2732
+ });
2733
+ return slot;
2734
+ }
2735
+ handleMessage(slot, message) {
2736
+ const pending = slot.pending.get(message.requestId);
2737
+ if (!pending) return;
2738
+ if (message.type === "progress") {
2739
+ pending.onProgress?.(message.progress);
2740
+ return;
2741
+ }
2742
+ slot.pending.delete(message.requestId);
2743
+ if (message.type === "error") {
2744
+ pending.reject(new Error(message.error));
2745
+ return;
2746
+ }
2747
+ const changedIds = message.changes.map(({ session }) => session.id);
2748
+ const replacedSessionIds = [...changedIds, ...message.removedSessionIds];
2749
+ const removedMetaIds = [...message.removedSessionIds, ...message.removedMetaIds];
2750
+ try {
2751
+ pending.resolve({
2752
+ sessions: applySessionChanges2(
2753
+ pending.payload.previousSessions,
2754
+ message.changes,
2755
+ message.removedSessionIds
2756
+ ),
2757
+ meta: applyMetaChanges(pending.payload.meta, message.meta, removedMetaIds),
2758
+ changedIds: pending.payload.sourceSync ? replacedSessionIds : void 0
2759
+ });
2760
+ } catch (error) {
2761
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
2762
+ }
2763
+ }
2764
+ closeWorker(agentName, slot, error) {
2765
+ if (slot.closed) return;
2766
+ slot.closed = true;
2767
+ if (this.workers.get(agentName) === slot) this.workers.delete(agentName);
2768
+ for (const pending of slot.pending.values()) pending.reject(error);
2769
+ slot.pending.clear();
2493
2770
  }
2494
2771
  };
2495
2772
 
@@ -2500,7 +2777,7 @@ function mergeEvents(previous, next) {
2500
2777
  const removedSessionRefs = /* @__PURE__ */ new Map();
2501
2778
  const sessionKey = (agentName, sessionId) => `${agentName}\0${sessionId}`;
2502
2779
  const addChanged = (item) => {
2503
- const key = sessionKey(item.agentName, item.session.id);
2780
+ const key = sessionKey(item.reference.agentName, item.reference.sessionId);
2504
2781
  removedSessionRefs.delete(key);
2505
2782
  changedSessionHeads.set(key, item);
2506
2783
  };
@@ -2531,9 +2808,6 @@ var LiveScanStore = class {
2531
2808
  startupScanOptions;
2532
2809
  deferInitialRefresh;
2533
2810
  syncEngine;
2534
- agents = [];
2535
- byAgent = {};
2536
- sessions = [];
2537
2811
  listeners = /* @__PURE__ */ new Set();
2538
2812
  watcher = null;
2539
2813
  pendingEvent = null;
@@ -2547,11 +2821,12 @@ var LiveScanStore = class {
2547
2821
  this.deferInitialRefresh = options.deferInitialRefresh === true;
2548
2822
  const workerRunner = options.workerRunner ?? new ThreadWorkerRunner(new URL("./scan-refresh-worker.js", import.meta.url));
2549
2823
  this.syncEngine = new AgentSyncEngine({
2550
- snapshot: () => this.getSnapshot(),
2551
2824
  startupScanOptions: this.startupScanOptions,
2552
2825
  workerRunner
2553
2826
  });
2554
- this.syncEngine.subscribeSessionsChanged((change) => this.applySessionsChanged(change));
2827
+ this.syncEngine.subscribeSessionsChanged((change) => {
2828
+ if (change.event) this.emit(change.event);
2829
+ });
2555
2830
  }
2556
2831
  async initialize() {
2557
2832
  const startedAt = performance.now();
@@ -2572,8 +2847,12 @@ var LiveScanStore = class {
2572
2847
  smartTagWorkerUrl: this.getSmartTagWorkerUrl() ?? void 0,
2573
2848
  includeSmartTags: this.deferInitialRefresh ? false : void 0
2574
2849
  });
2575
- this.applyScanResult(initialResult);
2576
- this.syncEngine.initialize(initialResult.cacheTimestamps);
2850
+ this.syncEngine.initialize(initialResult, {
2851
+ cacheTimestamps: initialResult.cacheTimestamps,
2852
+ registeredAgents: createRegisteredAgents(),
2853
+ allowedAgents: this.getAllowedAgents()
2854
+ });
2855
+ const snapshot = this.getSnapshot();
2577
2856
  const indexStartedAt = performance.now();
2578
2857
  if (!this.deferInitialRefresh) await this.syncEngine.syncInitialIndex();
2579
2858
  const indexDuration = performance.now() - indexStartedAt;
@@ -2581,9 +2860,9 @@ var LiveScanStore = class {
2581
2860
  duration_ms: Math.round(performance.now() - startedAt),
2582
2861
  index_ms: this.deferInitialRefresh ? void 0 : Math.round(indexDuration),
2583
2862
  deferred: this.deferInitialRefresh || void 0,
2584
- sessions: this.sessions.length,
2863
+ sessions: snapshot.sessions.length,
2585
2864
  agents: Object.fromEntries(
2586
- Object.entries(this.byAgent).map(([key, value]) => [key, value.length])
2865
+ Object.entries(snapshot.byAgent).map(([key, value]) => [key, value.length])
2587
2866
  ),
2588
2867
  agent_timings: initialResult.timings ? Object.fromEntries(
2589
2868
  Object.entries(initialResult.timings).map(([name, timing]) => [
@@ -2602,13 +2881,13 @@ var LiveScanStore = class {
2602
2881
  if (!this.watchEnabled) return;
2603
2882
  this.watcher = new SessionWatcher();
2604
2883
  this.watcher.onAgentsChanged((agentNames) => this.syncEngine.handleAgentsChanged(agentNames));
2605
- this.watcher.start(this.agents.map((agent) => agent.name));
2884
+ this.watcher.start(snapshot.agents);
2606
2885
  }
2607
2886
  startBackgroundRefresh() {
2608
2887
  this.syncEngine.startBackgroundRefresh();
2609
2888
  }
2610
2889
  getSnapshot() {
2611
- return { sessions: this.sessions, byAgent: this.byAgent, agents: this.agents };
2890
+ return this.syncEngine.snapshot();
2612
2891
  }
2613
2892
  getScanStatus() {
2614
2893
  return this.syncEngine.status();
@@ -2637,13 +2916,6 @@ var LiveScanStore = class {
2637
2916
  this.watcher = null;
2638
2917
  }
2639
2918
  }
2640
- applySessionsChanged(change) {
2641
- this.byAgent[change.agentName] = sortSessions(change.sessions);
2642
- this.rebuildSessions();
2643
- if (!change.event) return;
2644
- change.event.totalSessions = this.sessions.length;
2645
- this.emit(change.event);
2646
- }
2647
2919
  emit(event) {
2648
2920
  if (this.shuttingDown) return;
2649
2921
  if (this.pendingEvent || event.newSessions > 0) {
@@ -2665,29 +2937,11 @@ var LiveScanStore = class {
2665
2937
  if (pending) this.emitNow(pending);
2666
2938
  }, NEW_SESSION_EVENT_WINDOW_MS);
2667
2939
  }
2668
- rebuildSessions() {
2669
- this.sessions = sortSessions(Object.values(this.byAgent).flat());
2670
- }
2671
2940
  getSmartTagWorkerUrl() {
2672
2941
  const workerUrl = new URL("./smart-tag-worker.js", import.meta.url);
2673
2942
  if (workerUrl.protocol === "file:" && !existsSync4(fileURLToPath3(workerUrl))) return null;
2674
2943
  return workerUrl;
2675
2944
  }
2676
- applyScanResult(result) {
2677
- const agentMap = /* @__PURE__ */ new Map();
2678
- const allowedAgents = this.getAllowedAgents();
2679
- for (const agent of result.agents) agentMap.set(agent.name, agent);
2680
- for (const agent of createRegisteredAgents()) {
2681
- if (!agentMap.has(agent.name)) agentMap.set(agent.name, agent);
2682
- }
2683
- this.agents = [...agentMap.values()].filter(
2684
- (agent) => !allowedAgents || allowedAgents.has(agent.name.toLowerCase())
2685
- );
2686
- this.byAgent = Object.fromEntries(
2687
- this.agents.map((agent) => [agent.name, sortSessions(result.byAgent[agent.name] ?? [])])
2688
- );
2689
- this.rebuildSessions();
2690
- }
2691
2945
  getAllowedAgents() {
2692
2946
  if (!this.scanOptions.agents?.length) return null;
2693
2947
  return new Set(this.scanOptions.agents.map((agent) => agent.toLowerCase()));
@@ -2883,7 +3137,7 @@ var main = defineCommand({
2883
3137
  log_path: appLogger.getLogPath()
2884
3138
  });
2885
3139
  if (clearCache) {
2886
- const { clearCache: clear } = await import("./dist-AIDCOQZO.js");
3140
+ const { clearCache: clear } = await import("./dist-KEPJFHOC.js");
2887
3141
  clear();
2888
3142
  appLogger.info("cache.clear");
2889
3143
  console.log("Cache cleared.");