codesesh 0.16.0 → 0.18.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.
Files changed (46) hide show
  1. package/README.md +2 -2
  2. package/dist/{chunk-EWC6IODK.js → chunk-DHNP33L4.js} +23 -12
  3. package/dist/chunk-DHNP33L4.js.map +1 -0
  4. package/dist/{chunk-MWSJTNOW.js → chunk-HIYT2ZJL.js} +2091 -1261
  5. package/dist/chunk-HIYT2ZJL.js.map +1 -0
  6. package/dist/dist-DMGLFBW6.js +125 -0
  7. package/dist/index.js +1182 -785
  8. package/dist/index.js.map +1 -1
  9. package/dist/scan-refresh-worker.js +70 -64
  10. package/dist/scan-refresh-worker.js.map +1 -1
  11. package/dist/search-index-worker.js +73 -44
  12. package/dist/search-index-worker.js.map +1 -1
  13. package/dist/smart-tag-worker.js +2 -2
  14. package/dist/web/assets/AgentIcon-DcUJM198.js +1 -0
  15. package/dist/web/assets/BookmarkButton-CJjTjyae.js +1 -0
  16. package/dist/web/assets/Dashboard-wjTLT17d.js +39 -0
  17. package/dist/web/assets/DialogTitle-Bz0eXTGu.js +1 -0
  18. package/dist/web/assets/InteractiveReceipt-xnRqJvfd.js +1 -0
  19. package/dist/web/assets/PrismHighlighter-C7OfXHq2.js +6 -0
  20. package/dist/web/assets/Projects-DBxubQuM.js +1 -0
  21. package/dist/web/assets/SearchFilterBar-BG9ah_Po.js +1 -0
  22. package/dist/web/assets/SearchResultsPanel-Clmf2xKw.js +1 -0
  23. package/dist/web/assets/SessionDetail-CgR7ipmi.js +54 -0
  24. package/dist/web/assets/index-DR07vs5V.js +1890 -0
  25. package/dist/web/assets/{index-Bxe686Vr.css → index-DuB6tGoe.css} +1 -1
  26. package/dist/web/assets/index.dom-CCLJ9_FR.js +1 -0
  27. package/dist/web/assets/jsx-runtime-n5LQ9ujS.js +1 -0
  28. package/dist/web/assets/preload-helper-CmKXJxR3.js +1 -0
  29. package/dist/web/assets/session-indexes-EHJdYE4j.js +4 -0
  30. package/dist/web/assets/session-title-CNhapdPJ.js +1 -0
  31. package/dist/web/assets/useTheme-CwDcVdGz.js +1 -0
  32. package/dist/web/assets/utils-B6KiDbIe.js +1 -0
  33. package/dist/web/index.html +11 -7
  34. package/package.json +7 -12
  35. package/dist/chunk-EWC6IODK.js.map +0 -1
  36. package/dist/chunk-MWSJTNOW.js.map +0 -1
  37. package/dist/dist-AIDCOQZO.js +0 -237
  38. package/dist/web/assets/index-BpmbwB2_.js +0 -113
  39. package/dist/web/assets/markdown-dR4kAewZ.js +0 -14
  40. package/dist/web/assets/react-3acXkZNw.js +0 -1821
  41. package/dist/web/assets/rolldown-runtime-QTnfLwEv.js +0 -1
  42. package/dist/web/assets/syntax-mZXQuVcd.js +0 -6
  43. package/dist/web/assets/vendor-BVE7Fps8.js +0 -41
  44. package/dist/web/icon/agent/antigravity.svg +0 -1
  45. package/dist/web/icon/agent/kilocode.svg +0 -1
  46. /package/dist/{dist-AIDCOQZO.js.map → dist-DMGLFBW6.js.map} +0 -0
package/dist/index.js CHANGED
@@ -2,29 +2,29 @@
2
2
  import {
3
3
  appLogger,
4
4
  logSearchIndexSync
5
- } from "./chunk-EWC6IODK.js";
5
+ } from "./chunk-DHNP33L4.js";
6
6
  import {
7
7
  FileSystemSessionSource,
8
+ SessionAliasValidationError,
8
9
  StateStorageUnavailableError,
10
+ addCalendarDays,
9
11
  attachMissingProjectIdentities,
12
+ attachProjectMetrics,
10
13
  buildAgentCacheMeta,
11
14
  buildDashboard,
12
- classifySessionTags,
13
- computeIdentity,
14
15
  computeSessionDiff,
16
+ countCalendarDays,
15
17
  createProjectScopeMatcher,
16
18
  createRegisteredAgents,
17
19
  deleteBookmark,
18
20
  deleteSessionAlias,
19
21
  executeSessionSearch,
20
- extractSessionFileActivity,
22
+ filterSessionSearchCandidates,
23
+ formatSessionReference,
21
24
  getAgentInfoMap,
22
25
  getAgentLastFullSyncAt,
23
- getCursorDataPath,
24
26
  getSessionActivityTime,
25
- getSessionAgentName,
26
- getSmartTagSourceTimestamp,
27
- getTotalTokens,
27
+ getSessionAgentKey,
28
28
  importBookmarks,
29
29
  isAgentCacheInitialized,
30
30
  isProjectIdentityKind,
@@ -32,25 +32,26 @@ import {
32
32
  listCachedProjectGroups,
33
33
  listFileActivity,
34
34
  listSessionAliases,
35
- listSessionFileActivity,
36
- loadCachedSessionDataEntry,
37
35
  loadCachedSessions,
38
36
  markAgentFullSyncCompleted,
39
37
  matchesProjectIdentity,
40
38
  matchesProjectScope,
41
- matchesSessionSearchFilters,
39
+ materializeSessionDetailResponse,
42
40
  mergeSearchQueryOptions,
41
+ mergeSessionsUpdatedEvents,
42
+ mergeSortedSessions,
43
+ normalizeSessionReference,
43
44
  perf,
44
- realFs,
45
+ publishPendingPricing,
45
46
  refreshPricingCache,
46
- resolveProviderRoots,
47
47
  scanSessions,
48
+ sessionRoutePath,
48
49
  sessionSignature,
49
50
  sortSessions,
50
- startOfLocalDay,
51
+ startOfCalendarDay,
51
52
  upsertBookmark,
52
53
  upsertSessionAlias
53
- } from "./chunk-MWSJTNOW.js";
54
+ } from "./chunk-HIYT2ZJL.js";
54
55
 
55
56
  // src/index.ts
56
57
  import { defineCommand, runMain } from "citty";
@@ -62,6 +63,7 @@ import { compress } from "hono/compress";
62
63
  import { serve } from "@hono/node-server";
63
64
  import { serveStatic } from "@hono/node-server/serve-static";
64
65
  import { existsSync } from "fs";
66
+ import { createServer as createHttpsServer } from "https";
65
67
  import { resolve, dirname } from "path";
66
68
  import { fileURLToPath } from "url";
67
69
 
@@ -69,7 +71,7 @@ import { fileURLToPath } from "url";
69
71
  import { Hono } from "hono";
70
72
 
71
73
  // src/time-window-resolution.ts
72
- var DAY_MS = 24 * 60 * 60 * 1e3;
74
+ var ROLLING_DAY_MS = 24 * 60 * 60 * 1e3;
73
75
  var DEFAULT_DASHBOARD_DAYS = 30;
74
76
  function resolveTimeWindow(request) {
75
77
  return request.mode === "cli" ? resolveCliWindow(request) : resolveDashboardWindow(request);
@@ -82,7 +84,7 @@ function resolveCliWindow(request) {
82
84
  const days = parseDays(request.days);
83
85
  if (days === 0) return { to, days };
84
86
  if (days == null || days < 0) return { to };
85
- const rollingFrom = now - days * DAY_MS;
87
+ const rollingFrom = now - days * ROLLING_DAY_MS;
86
88
  return Number.isFinite(rollingFrom) ? { from: rollingFrom, to, days } : { to };
87
89
  }
88
90
  function resolveDashboardWindow(request) {
@@ -94,19 +96,19 @@ function resolveDashboardWindow(request) {
94
96
  let days = parsedDays != null && parsedDays > 0 ? parsedDays : defaults.days;
95
97
  const queryFrom = parseOptionalDate(request.query.from);
96
98
  if (queryFrom != null) {
97
- days ??= elapsedDays(queryFrom, to);
99
+ days ??= countCalendarDays(queryFrom, to);
98
100
  return { from: queryFrom, to, days };
99
101
  }
100
102
  if (parsedDays === 0 || !hasQueryDays && defaults.days === 0) {
101
103
  return { to, days: 0 };
102
104
  }
103
105
  if (defaults.from != null) {
104
- days ??= elapsedDays(defaults.from, to);
106
+ days ??= countCalendarDays(defaults.from, to);
105
107
  return { from: defaults.from, to, days };
106
108
  }
107
109
  const resolvedDays = days != null && days > 0 ? days : DEFAULT_DASHBOARD_DAYS;
108
110
  return {
109
- from: startOfLocalDay(to) - (resolvedDays - 1) * DAY_MS,
111
+ from: addCalendarDays(startOfCalendarDay(to), -(resolvedDays - 1)),
110
112
  to,
111
113
  days: resolvedDays
112
114
  };
@@ -125,32 +127,101 @@ function parseOptionalDate(value) {
125
127
  function parseDays(value) {
126
128
  if (value == null) return void 0;
127
129
  const days = Number.parseInt(value, 10);
128
- return Number.isSafeInteger(days) && Number.isSafeInteger(days * DAY_MS) ? days : void 0;
129
- }
130
- function elapsedDays(from, to) {
131
- return Math.max(1, Math.ceil((to - from) / DAY_MS));
130
+ return Number.isSafeInteger(days) && Number.isSafeInteger(days * ROLLING_DAY_MS) ? days : void 0;
132
131
  }
133
132
 
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;
133
+ // src/api/query-params.ts
134
+ var SMART_TAGS = [
135
+ "bugfix",
136
+ "refactoring",
137
+ "feature-dev",
138
+ "testing",
139
+ "docs",
140
+ "git-ops",
141
+ "build-deploy",
142
+ "exploration",
143
+ "planning"
144
+ ];
145
+ var SEARCH_LIMIT_MAX = 100;
146
+ var SEARCH_LIMIT_DEFAULT = 50;
147
+ function searchParams(c) {
148
+ return new URL(c.req.url ?? "http://localhost/", "http://localhost/").searchParams;
149
+ }
150
+ function queryValues(params, ...names) {
151
+ return names.flatMap(
152
+ (name) => params.getAll(name).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean)
153
+ );
154
+ }
155
+ function optionalQueryValue(value) {
156
+ const normalized = value?.trim();
157
+ return normalized ? normalized : void 0;
158
+ }
159
+ function parseDateParam(value, fallback) {
160
+ if (value == null) return fallback;
161
+ const ts = new Date(value).getTime();
162
+ return Number.isNaN(ts) ? fallback : ts;
163
+ }
164
+ function parseNumberParam(value) {
165
+ if (value == null || !value.trim()) return void 0;
166
+ const number = Number(value);
167
+ return Number.isFinite(number) ? number : void 0;
168
+ }
169
+ function parseSmartTags(values) {
170
+ const tags = values.map((value) => value.toLowerCase()).filter((value) => SMART_TAGS.includes(value));
171
+ return tags.length > 0 ? [...new Set(tags)] : void 0;
172
+ }
173
+ function parseFileActivityKind(value) {
174
+ if (value === "read" || value === "edit" || value === "write" || value === "delete") {
175
+ return value;
176
+ }
177
+ return void 0;
178
+ }
179
+ function parseProjectIdentityFilter(kindValue, keyValue) {
180
+ const kind = optionalQueryValue(kindValue);
181
+ const key = optionalQueryValue(keyValue);
182
+ if (!kind && !key) return void 0;
183
+ if (!kind || !key || !isProjectIdentityKind(kind)) return null;
184
+ return { kind, key };
185
+ }
186
+ function parseSearchOptions(c, defaults, projectIdentity) {
187
+ const params = searchParams(c);
188
+ const limitValue = parseNumberParam(params.get("limit") ?? void 0);
189
+ return {
190
+ agent: optionalQueryValue(params.get("agent") ?? void 0),
191
+ project: optionalQueryValue(params.get("project") ?? void 0),
192
+ projectKind: projectIdentity?.kind,
193
+ projectKey: projectIdentity?.key,
194
+ cwd: optionalQueryValue(params.get("cwd") ?? void 0),
195
+ tags: parseSmartTags(queryValues(params, "tag", "tags", "signal")),
196
+ tools: queryValues(params, "tool", "tools").map((tool) => tool.toLowerCase()),
197
+ file: optionalQueryValue(params.get("file") ?? params.get("path") ?? void 0),
198
+ fileKind: parseFileActivityKind(
199
+ optionalQueryValue(params.get("fileKind") ?? params.get("fileActivity") ?? void 0)
200
+ ),
201
+ costMin: parseNumberParam(params.get("costMin") ?? void 0),
202
+ costMax: parseNumberParam(params.get("costMax") ?? void 0),
203
+ from: parseDateParam(params.get("from") ?? void 0, defaults.from),
204
+ to: parseDateParam(params.get("to") ?? void 0, defaults.to),
205
+ limit: limitValue && limitValue > 0 ? Math.min(limitValue, SEARCH_LIMIT_MAX) : SEARCH_LIMIT_DEFAULT
206
+ };
139
207
  }
140
- function getSessionAliasKey(agentKey, sessionId) {
141
- return `${agentKey.toLowerCase()}\0${sessionId}`;
208
+ function filterSessionsByActivityWindow(sessions, from, to) {
209
+ if (from == null && to == null) return sessions;
210
+ return sessions.filter((session) => {
211
+ const activity = getSessionActivityTime(session);
212
+ if (from != null && activity < from) return false;
213
+ if (to != null && activity > to) return false;
214
+ return true;
215
+ });
142
216
  }
143
- function getSessionAgentKey(session) {
144
- return session.slug.split("/")[0]?.toLowerCase() ?? "";
217
+
218
+ // src/api/session-aliases-view.ts
219
+ function aliasKey(reference) {
220
+ return `${reference.agentName.toLowerCase()}\0${reference.sessionId}`;
145
221
  }
146
- function loadSessionAliasMap() {
222
+ function loadAliasMap() {
147
223
  try {
148
- return new Map(
149
- listSessionAliases().map((alias) => [
150
- getSessionAliasKey(alias.agentKey, alias.sessionId),
151
- alias.alias
152
- ])
153
- );
224
+ return new Map(listSessionAliases().map((alias) => [aliasKey(alias.reference), alias]));
154
225
  } catch (error) {
155
226
  if (!(error instanceof StateStorageUnavailableError)) {
156
227
  appLogger.warn("api.session_aliases.load_failed", {
@@ -160,52 +231,97 @@ function loadSessionAliasMap() {
160
231
  return /* @__PURE__ */ new Map();
161
232
  }
162
233
  }
163
- function isStateStorageUnavailable(error) {
164
- return error instanceof StateStorageUnavailableError;
234
+ function buildAliasView() {
235
+ const aliases = loadAliasMap();
236
+ return {
237
+ size: aliases.size,
238
+ get: (reference) => aliases.get(aliasKey(reference))?.alias,
239
+ decorate(record, reference) {
240
+ const alias = aliases.get(aliasKey(reference))?.alias;
241
+ return alias ? { ...record, display_title: alias } : record;
242
+ },
243
+ entries: () => aliases.values()
244
+ };
165
245
  }
166
- function withDisplayTitle(session, agentKey, aliases) {
167
- const alias = aliases.get(getSessionAliasKey(agentKey, session.id));
168
- return alias ? { ...session, display_title: alias } : session;
246
+ var cachedView = null;
247
+ function loadAliasView() {
248
+ return cachedView ??= buildAliasView();
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 invalidateAliasView() {
251
+ cachedView = null;
173
252
  }
174
- function withFileActivityDisplayTitle(activity, aliases) {
253
+ function decorateBookmark(bookmark, aliases) {
175
254
  return {
176
- ...activity,
177
- session: withDisplayTitle(activity.session, activity.agent_name, aliases)
255
+ ...bookmark,
256
+ session: aliases.decorate(bookmark.session, bookmark.reference)
178
257
  };
179
258
  }
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);
259
+ function decorateFileActivity(activity, aliases) {
260
+ return {
261
+ ...activity,
262
+ session: aliases.decorate(activity.session, activity.reference)
263
+ };
185
264
  }
186
265
  function findAliasSearchResults(query, options, scanResult, aliases) {
187
266
  const search = mergeSearchQueryOptions(query, options);
188
267
  const needle = search.text.trim().toLowerCase();
189
268
  if (!needle || aliases.size === 0) return [];
190
- const projectScope = search.options.cwd ? createProjectScopeMatcher(search.options.cwd) : null;
269
+ const sessionsByAgent = /* @__PURE__ */ new Map();
270
+ const lookupSession = (agentName, sessionId) => {
271
+ let byId = sessionsByAgent.get(agentName);
272
+ if (!byId) {
273
+ byId = new Map((scanResult.byAgent[agentName] ?? []).map((item) => [item.id, item]));
274
+ sessionsByAgent.set(agentName, byId);
275
+ }
276
+ return byId.get(sessionId);
277
+ };
191
278
  const results = [];
192
- for (const [aliasKey, alias] of aliases) {
193
- if (!alias.toLowerCase().includes(needle)) continue;
194
- const session = findSessionByAliasKey(scanResult, aliasKey);
279
+ for (const alias of aliases.entries()) {
280
+ if (!alias.alias.toLowerCase().includes(needle)) continue;
281
+ const { agentName, sessionId } = alias.reference;
282
+ const session = lookupSession(agentName, sessionId);
195
283
  if (!session) continue;
196
- const agentName = aliasKey.slice(0, aliasKey.indexOf("\0"));
197
- if (!matchesSessionSearchFilters(agentName, session, search.options, projectScope)) continue;
198
284
  results.push({
199
- agentName,
200
- session: withDisplayTitle(session, agentName, aliases),
285
+ reference: alias.reference,
286
+ session: aliases.decorate(session, alias.reference),
201
287
  snippet: `Alias \xB7 ${session.directory}`,
202
288
  matchType: "title"
203
289
  });
204
290
  }
205
- return results.sort(
291
+ return filterSessionSearchCandidates(results, search.options).sort(
206
292
  (a, b) => getSessionActivityTime(b.session) - getSessionActivityTime(a.session)
207
293
  );
208
294
  }
295
+
296
+ // src/api/handlers.ts
297
+ var SNAPSHOT_AGGREGATION_CACHE_LIMIT = 64;
298
+ var snapshotAggregationCaches = /* @__PURE__ */ new WeakMap();
299
+ function getSnapshotAggregation(source, sessions, key, build) {
300
+ let cache = snapshotAggregationCaches.get(source);
301
+ if (!cache || cache.sessions !== sessions) {
302
+ cache = { sessions, values: /* @__PURE__ */ new Map() };
303
+ snapshotAggregationCaches.set(source, cache);
304
+ }
305
+ const cacheKey = JSON.stringify(key);
306
+ if (cache.values.has(cacheKey)) return cache.values.get(cacheKey);
307
+ const value = build();
308
+ if (cache.values.size >= SNAPSHOT_AGGREGATION_CACHE_LIMIT) {
309
+ const oldestKey = cache.values.keys().next().value;
310
+ if (oldestKey != null) cache.values.delete(oldestKey);
311
+ }
312
+ cache.values.set(cacheKey, value);
313
+ return value;
314
+ }
315
+ function withStorageErrors(handler, onUnavailable) {
316
+ try {
317
+ return handler();
318
+ } catch (error) {
319
+ if (error instanceof StateStorageUnavailableError) {
320
+ return onUnavailable();
321
+ }
322
+ throw error;
323
+ }
324
+ }
209
325
  function isRecord(value) {
210
326
  return typeof value === "object" && value !== null;
211
327
  }
@@ -213,15 +329,14 @@ function isSessionStats(value) {
213
329
  if (!isRecord(value)) return false;
214
330
  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
331
  }
216
- function parseBookmarkPayload(value) {
332
+ function parseBookmarkSession(value, reference) {
217
333
  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)) {
334
+ 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
335
  return null;
220
336
  }
221
337
  return {
222
- agentKey: value.agentKey,
223
- sessionId: value.sessionId,
224
- fullPath: value.fullPath,
338
+ id: reference.sessionId,
339
+ slug: formatSessionReference(reference),
225
340
  title: value.title,
226
341
  directory: value.directory,
227
342
  time_created: value.time_created,
@@ -229,70 +344,38 @@ function parseBookmarkPayload(value) {
229
344
  stats: value.stats
230
345
  };
231
346
  }
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
- };
347
+ function parseSessionReferencePayload(value) {
348
+ if (!isRecord(value) || typeof value.agentName !== "string" || typeof value.sessionId !== "string" || !value.agentName.trim() || !value.sessionId) {
349
+ return null;
350
+ }
351
+ return normalizeSessionReference({
352
+ agentName: value.agentName.trim().toLowerCase(),
353
+ sessionId: value.sessionId
354
+ });
287
355
  }
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;
356
+ function parseBookmarkPayload(value) {
357
+ if (!isRecord(value)) return null;
358
+ const reference = parseSessionReferencePayload(value.reference);
359
+ if (reference) {
360
+ const session2 = parseBookmarkSession(value.session, reference);
361
+ return session2 ? { reference, session: session2 } : null;
362
+ }
363
+ if (typeof value.agentKey !== "string" || typeof value.sessionId !== "string" || typeof value.fullPath !== "string" || !value.agentKey.trim() || !value.sessionId) {
364
+ return null;
365
+ }
366
+ const legacyReference = normalizeSessionReference({
367
+ agentName: value.agentKey.trim().toLowerCase(),
368
+ sessionId: value.sessionId
295
369
  });
370
+ const session = parseBookmarkSession(
371
+ {
372
+ ...value,
373
+ id: value.sessionId,
374
+ slug: value.fullPath
375
+ },
376
+ legacyReference
377
+ );
378
+ return session ? { reference: legacyReference, session } : null;
296
379
  }
297
380
  function sanitizeClientLogData(value) {
298
381
  if (!isRecord(value)) return {};
@@ -306,60 +389,58 @@ function sanitizeClientLogData(value) {
306
389
  })
307
390
  );
308
391
  }
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
- });
392
+ function toSessionListItem(session) {
393
+ if (!session.model_usage) return session;
394
+ const item = { ...session };
395
+ delete item.model_usage;
396
+ return item;
397
+ }
398
+ function getSessionHeadReference(session) {
399
+ return {
400
+ agentName: getSessionAgentKey(session),
401
+ sessionId: session.id
402
+ };
403
+ }
404
+ var SESSION_DETAIL_STREAM_BATCH_CHARS = 64 * 1024;
405
+ function createSessionDetailJsonResponse(data, messages) {
406
+ const encoder = new TextEncoder();
407
+ const headerJson = JSON.stringify(data);
408
+ const iterator = messages[Symbol.iterator]();
409
+ let wroteHeader = false;
410
+ let wroteMessage = false;
411
+ return new Response(
412
+ new ReadableStream({
413
+ pull(controller) {
414
+ if (!wroteHeader) {
415
+ controller.enqueue(encoder.encode(`${headerJson.slice(0, -1)},"messages":[`));
416
+ wroteHeader = true;
417
+ return;
418
+ }
419
+ const batch = [];
420
+ let batchLength = 0;
421
+ let next = iterator.next();
422
+ while (!next.done) {
423
+ const prefix = wroteMessage ? "," : "";
424
+ batch.push(prefix, next.value);
425
+ batchLength += prefix.length + next.value.length;
426
+ wroteMessage = true;
427
+ if (batchLength >= SESSION_DETAIL_STREAM_BATCH_CHARS) break;
428
+ next = iterator.next();
429
+ }
430
+ if (next.done) {
431
+ batch.push("]}");
432
+ controller.enqueue(encoder.encode(batch.join("")));
433
+ controller.close();
434
+ return;
435
+ }
436
+ controller.enqueue(encoder.encode(batch.join("")));
437
+ },
438
+ cancel() {
439
+ iterator.return?.();
440
+ }
441
+ }),
442
+ { headers: { "Content-Type": "application/json; charset=UTF-8" } }
443
+ );
363
444
  }
364
445
  function handleGetConfig(c, defaults) {
365
446
  const payload = {
@@ -378,22 +459,38 @@ function handleGetAgents(c, scanSource, defaults = {}) {
378
459
  const scanResult = scanSource.getSnapshot();
379
460
  const from = parseDateParam(c.req.query("from"), defaults.from);
380
461
  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
- ])
462
+ const agents = getSnapshotAggregation(
463
+ scanSource,
464
+ scanResult.sessions,
465
+ ["agents", from, to],
466
+ () => {
467
+ const counts = Object.fromEntries(
468
+ Object.entries(scanResult.byAgent).map(([agentName, sessions]) => [
469
+ agentName,
470
+ filterSessionsByActivityWindow(sessions, from, to).length
471
+ ])
472
+ );
473
+ return getAgentInfoMap(counts);
474
+ }
386
475
  );
387
- return c.json(getAgentInfoMap(counts));
476
+ return c.json(agents);
388
477
  }
389
478
  function handleGetProjects(c, scanSource, defaults = {}) {
390
479
  const scanResult = scanSource.getSnapshot();
391
480
  const from = parseDateParam(c.req.query("from"), defaults.from);
392
481
  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
- });
482
+ const projects = getSnapshotAggregation(
483
+ scanSource,
484
+ scanResult.sessions,
485
+ ["projects", from, to],
486
+ () => {
487
+ const sessions = filterSessionsByActivityWindow(scanResult.sessions, from, to);
488
+ return {
489
+ projects: attachProjectMetrics(listCachedProjectGroups(sessions), sessions)
490
+ };
491
+ }
492
+ );
493
+ return c.json(projects);
397
494
  }
398
495
  function handleGetSessions(c, scanSource, defaults = {}) {
399
496
  const scanResult = scanSource.getSnapshot();
@@ -428,16 +525,16 @@ function handleGetSessions(c, scanSource, defaults = {}) {
428
525
  if (tag) {
429
526
  sessions = sessions.filter((s) => s.smart_tags?.includes(tag));
430
527
  }
431
- const aliases = loadSessionAliasMap();
528
+ const aliases = loadAliasView();
432
529
  if (q) {
433
530
  sessions = sessions.filter((session) => {
434
- const alias = aliases.get(getSessionAliasKey(getSessionAgentKey(session), session.id));
531
+ const alias = aliases.get(getSessionHeadReference(session));
435
532
  return session.title.toLowerCase().includes(q) || alias?.toLowerCase().includes(q);
436
533
  });
437
534
  }
438
535
  return c.json({
439
536
  sessions: sessions.map(
440
- (session) => withDisplayTitle(session, getSessionAgentKey(session), aliases)
537
+ (session) => toSessionListItem(aliases.decorate(session, getSessionHeadReference(session)))
441
538
  )
442
539
  });
443
540
  }
@@ -452,35 +549,18 @@ function handleSearchSessions(c, scanSource, defaults = {}) {
452
549
  return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
453
550
  }
454
551
  const searchOptions = parseSearchOptions(c, defaults, projectIdentity);
455
- const aliases = loadSessionAliasMap();
552
+ const aliases = loadAliasView();
456
553
  const results = executeSessionSearch(query, searchOptions, scanResult).map((result) => ({
457
554
  ...result,
458
- session: withDisplayTitle(result.session, result.agentName, aliases)
555
+ session: aliases.decorate(result.session, result.reference)
459
556
  }));
460
557
  const aliasResults = findAliasSearchResults(query, searchOptions, scanResult, aliases);
461
558
  const deduped = /* @__PURE__ */ new Map();
462
559
  for (const result of [...aliasResults, ...results]) {
463
- deduped.set(`${result.agentName}\0${result.session.id}`, result);
560
+ deduped.set(`${result.reference.agentName}\0${result.reference.sessionId}`, result);
464
561
  }
465
562
  return c.json({ results: [...deduped.values()].slice(0, searchOptions.limit ?? 50) });
466
563
  }
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
564
  function handleGetFileActivity(c, defaults = {}) {
485
565
  const limitValue = Number(c.req.query("limit"));
486
566
  const limit = Number.isFinite(limitValue) && limitValue > 0 ? Math.min(limitValue, 200) : 50;
@@ -491,7 +571,7 @@ function handleGetFileActivity(c, defaults = {}) {
491
571
  if (projectIdentity === null) {
492
572
  return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
493
573
  }
494
- const aliases = loadSessionAliasMap();
574
+ const aliases = loadAliasView();
495
575
  return c.json({
496
576
  activity: listFileActivity({
497
577
  agent: optionalQueryValue(c.req.query("agent")),
@@ -505,12 +585,11 @@ function handleGetFileActivity(c, defaults = {}) {
505
585
  from: parseDateParam(c.req.query("from"), defaults.from),
506
586
  to: parseDateParam(c.req.query("to"), defaults.to),
507
587
  limit
508
- }).map((activity) => withFileActivityDisplayTitle(activity, aliases))
588
+ }).map((activity) => decorateFileActivity(activity, aliases))
509
589
  });
510
590
  }
511
591
  async function handleGetSessionData(c, scanSource) {
512
592
  const startedAt = performance.now();
513
- const scanResult = scanSource.getSnapshot();
514
593
  const agentName = c.req.param("agent");
515
594
  const sessionId = c.req.param("id");
516
595
  if (!agentName) {
@@ -519,21 +598,15 @@ async function handleGetSessionData(c, scanSource) {
519
598
  if (!sessionId) {
520
599
  return c.json({ error: "Missing session ID" }, 400);
521
600
  }
522
- const agent = scanResult.agents.find((a) => a.name === agentName);
523
- if (!agent) {
524
- return c.json({ error: `Unknown agent: ${agentName}` }, 404);
525
- }
526
601
  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) {
602
+ const result = materializeSessionDetailResponse(scanSource.getSnapshot(), {
603
+ agentName,
604
+ sessionId
605
+ });
606
+ if (result.status === "unknown-agent") {
607
+ return c.json({ error: `Unknown agent: ${agentName}` }, 404);
608
+ }
609
+ if (result.status === "not-ready") {
537
610
  appLogger.warn("api.session_data.cache_miss", {
538
611
  agent: agentName,
539
612
  session_id: sessionId,
@@ -541,27 +614,20 @@ async function handleGetSessionData(c, scanSource) {
541
614
  });
542
615
  return c.json({ error: "Session cache not ready" }, 404);
543
616
  }
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
617
  appLogger.info("api.session_data", {
550
618
  agent: agentName,
551
619
  session_id: sessionId,
552
- messages: data.messages.length,
553
- load_duration_ms: Math.round(loadDuration),
554
- tag_duration_ms: Math.round(tagDuration),
620
+ messages: result.status === "found-json" ? result.messageCount : result.data.messages.length,
555
621
  duration_ms: Math.round(performance.now() - startedAt)
556
622
  });
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
- });
623
+ const aliases = loadAliasView();
624
+ if (result.status === "found-json") {
625
+ return createSessionDetailJsonResponse(
626
+ aliases.decorate(result.data, result.data.reference),
627
+ result.messages
628
+ );
629
+ }
630
+ return c.json(aliases.decorate(result.data, result.data.reference));
565
631
  } catch (err) {
566
632
  const message = err instanceof Error ? err.message : "Failed to load session";
567
633
  appLogger.error("api.session_data.error", {
@@ -584,32 +650,26 @@ async function handlePostClientLog(c) {
584
650
  return c.json({ ok: true });
585
651
  }
586
652
  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
- }
653
+ return withStorageErrors(
654
+ () => {
655
+ const aliases = loadAliasView();
656
+ return c.json({
657
+ bookmarks: listBookmarks().map((bookmark) => decorateBookmark(bookmark, aliases)),
658
+ storageAvailable: true
659
+ });
660
+ },
661
+ () => c.json({ bookmarks: [], storageAvailable: false })
662
+ );
599
663
  }
600
664
  async function handlePutBookmark(c) {
601
665
  const payload = parseBookmarkPayload(await c.req.json().catch(() => null));
602
666
  if (!payload) {
603
667
  return c.json({ error: "Invalid bookmark payload" }, 400);
604
668
  }
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
- }
669
+ return withStorageErrors(
670
+ () => c.json({ bookmark: upsertBookmark(payload), storageAvailable: true }),
671
+ () => c.json({ error: "Bookmark storage is unavailable" }, 503)
672
+ );
613
673
  }
614
674
  async function handleImportBookmarks(c) {
615
675
  const payload = await c.req.json().catch(() => null);
@@ -620,14 +680,10 @@ async function handleImportBookmarks(c) {
620
680
  if (bookmarks.length !== payload.length) {
621
681
  return c.json({ error: "Invalid bookmark payload" }, 400);
622
682
  }
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
- }
683
+ return withStorageErrors(
684
+ () => c.json({ bookmarks: importBookmarks(bookmarks), storageAvailable: true }),
685
+ () => c.json({ error: "Bookmark storage is unavailable" }, 503)
686
+ );
631
687
  }
632
688
  function handleDeleteBookmark(c) {
633
689
  const agentKey = c.req.param("agent");
@@ -635,32 +691,35 @@ function handleDeleteBookmark(c) {
635
691
  if (!agentKey || !sessionId) {
636
692
  return c.json({ error: "Missing bookmark identifier" }, 400);
637
693
  }
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
- }
694
+ return withStorageErrors(
695
+ () => {
696
+ deleteBookmark({ agentName: agentKey, sessionId });
697
+ return c.json({ ok: true, storageAvailable: true });
698
+ },
699
+ () => c.json({ error: "Bookmark storage is unavailable" }, 503)
700
+ );
647
701
  }
648
702
  async function handlePutSessionAlias(c) {
649
703
  const agentKey = c.req.param("agent");
650
704
  const sessionId = c.req.param("id");
651
705
  const payload = await c.req.json().catch(() => null);
652
- if (!agentKey || !sessionId || typeof payload?.alias !== "string") {
706
+ const aliasValue = payload?.alias;
707
+ if (!agentKey || !sessionId || typeof aliasValue !== "string") {
653
708
  return c.json({ error: "Invalid session alias payload" }, 400);
654
709
  }
655
710
  try {
656
- return c.json({ alias: upsertSessionAlias(agentKey, sessionId, payload.alias) });
711
+ return withStorageErrors(
712
+ () => {
713
+ const alias = upsertSessionAlias({ agentName: agentKey, sessionId }, aliasValue);
714
+ invalidateAliasView();
715
+ return c.json({ alias });
716
+ },
717
+ () => c.json({ error: "Session alias storage is unavailable" }, 503)
718
+ );
657
719
  } catch (error) {
658
- if (error instanceof TypeError) {
720
+ if (error instanceof SessionAliasValidationError) {
659
721
  return c.json({ error: "Session alias must be non-empty and at most 160 characters" }, 400);
660
722
  }
661
- if (isStateStorageUnavailable(error)) {
662
- return c.json({ error: "Session alias storage is unavailable" }, 503);
663
- }
664
723
  throw error;
665
724
  }
666
725
  }
@@ -670,15 +729,14 @@ function handleDeleteSessionAlias(c) {
670
729
  if (!agentKey || !sessionId) {
671
730
  return c.json({ error: "Missing session alias identifier" }, 400);
672
731
  }
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
- }
732
+ return withStorageErrors(
733
+ () => {
734
+ deleteSessionAlias({ agentName: agentKey, sessionId });
735
+ invalidateAliasView();
736
+ return c.json({ ok: true });
737
+ },
738
+ () => c.json({ error: "Session alias storage is unavailable" }, 503)
739
+ );
682
740
  }
683
741
  function handleGetDashboard(c, scanSource, defaults = {}) {
684
742
  const scanResult = scanSource.getSnapshot();
@@ -703,15 +761,30 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
703
761
  projectKind: projectIdentity?.kind,
704
762
  projectKey: projectIdentity?.key
705
763
  };
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
- });
764
+ const fixedTo = parseDateParam(c.req.query("to"), defaults.to);
765
+ const aggregate = getSnapshotAggregation(
766
+ scanSource,
767
+ scanResult.sessions,
768
+ [
769
+ "dashboard",
770
+ scope.agent,
771
+ scope.projectKind,
772
+ scope.projectKey,
773
+ from,
774
+ fixedTo ?? startOfCalendarDay(to)
775
+ ],
776
+ () => {
777
+ const agentInfo = getAgentInfoMap({});
778
+ const agentInfoMap = new Map(agentInfo.map((agent) => [agent.name, agent]));
779
+ return buildDashboard(scanResult.sessions, {
780
+ byAgentNames: Object.keys(scanResult.byAgent),
781
+ scope,
782
+ from,
783
+ to,
784
+ agentInfoMap
785
+ });
786
+ }
787
+ );
715
788
  const data = {
716
789
  ...aggregate,
717
790
  recentFileActivities: listFileActivity({
@@ -724,14 +797,15 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
724
797
  }),
725
798
  window: { from, to, days }
726
799
  };
727
- const aliases = loadSessionAliasMap();
800
+ const aliases = loadAliasView();
728
801
  return c.json({
729
802
  ...data,
730
- recentSessions: data.recentSessions.map(
731
- (session) => withDisplayTitle(session, session.agentName, aliases)
732
- ),
803
+ recentSessions: data.recentSessions.map((item) => ({
804
+ ...item,
805
+ session: aliases.decorate(item.session, item.reference)
806
+ })),
733
807
  recentFileActivities: data.recentFileActivities.map(
734
- (activity) => withFileActivityDisplayTitle(activity, aliases)
808
+ (activity) => decorateFileActivity(activity, aliases)
735
809
  )
736
810
  });
737
811
  }
@@ -828,8 +902,48 @@ function createApiRoutes(scanSource, eventSource, options = {}) {
828
902
 
829
903
  // src/remote-access.ts
830
904
  import { randomBytes, timingSafeEqual } from "crypto";
905
+ import { readFileSync } from "fs";
831
906
  import { isIP } from "net";
832
907
  var REMOTE_ACCESS_QUERY_PARAM = "access_token";
908
+ var RemoteTransportError = class extends Error {
909
+ };
910
+ function resolveRemoteTransport(request) {
911
+ const hasCert = Boolean(request.tlsCertPath);
912
+ const hasKey = Boolean(request.tlsKeyPath);
913
+ if (hasCert !== hasKey) {
914
+ throw new RemoteTransportError("TLS requires both --tls-cert and --tls-key.");
915
+ }
916
+ if (hasCert && request.trustProxy) {
917
+ throw new RemoteTransportError(
918
+ "Use either --tls-cert/--tls-key or --trust-proxy, not both: only one of them terminates TLS."
919
+ );
920
+ }
921
+ if (hasCert) {
922
+ try {
923
+ return {
924
+ kind: "tls",
925
+ cert: readFileSync(request.tlsCertPath),
926
+ key: readFileSync(request.tlsKeyPath)
927
+ };
928
+ } catch (error) {
929
+ throw new RemoteTransportError(
930
+ `Unable to read the TLS certificate or key: ${error instanceof Error ? error.message : String(error)}`
931
+ );
932
+ }
933
+ }
934
+ if (request.trustProxy) return { kind: "trusted-proxy" };
935
+ return isLoopbackHostname(request.hostname) ? { kind: "loopback" } : { kind: "plaintext" };
936
+ }
937
+ var FORWARDED_PROTO_HEADER = "X-Forwarded-Proto";
938
+ function requireProxyTls() {
939
+ return async (c, next) => {
940
+ const proto = c.req.header(FORWARDED_PROTO_HEADER)?.split(",")[0]?.trim().toLowerCase();
941
+ if (proto !== "https") {
942
+ return c.json({ error: "Requests must arrive over TLS through the trusted proxy" }, 403);
943
+ }
944
+ await next();
945
+ };
946
+ }
833
947
  function createRemoteAccessToken() {
834
948
  return randomBytes(32).toString("base64url");
835
949
  }
@@ -909,6 +1023,7 @@ async function createServer(port, store, options = {}) {
909
1023
  const app = new Hono2();
910
1024
  const hostname = options.hostname ?? "127.0.0.1";
911
1025
  const isLoopback = isLoopbackHostname(hostname);
1026
+ const transport = options.transport ?? (isLoopback ? { kind: "loopback" } : { kind: "plaintext" });
912
1027
  const remoteAccessToken = !isLoopback ? options.remoteAccessToken ?? (options.remoteAccess ? createRemoteAccessToken() : null) : null;
913
1028
  if (!isLoopback && !remoteAccessToken) {
914
1029
  throw new Error(
@@ -935,6 +1050,9 @@ async function createServer(port, store, options = {}) {
935
1050
  });
936
1051
  }
937
1052
  });
1053
+ if (transport.kind === "trusted-proxy") {
1054
+ app.use("/api/*", requireProxyTls());
1055
+ }
938
1056
  if (remoteAccessToken) {
939
1057
  app.use("/api/*", remoteAccessAuth(remoteAccessToken));
940
1058
  }
@@ -952,7 +1070,7 @@ async function createServer(port, store, options = {}) {
952
1070
  defaultSessionDays: options.defaultSessionDays
953
1071
  };
954
1072
  app.route("/api", createApiRoutes(store, store, routeOptions));
955
- const webDistPath = findWebDistPath();
1073
+ const webDistPath = options.webDistPath ?? findWebDistPath();
956
1074
  if (webDistPath) {
957
1075
  app.use("/*", serveStatic({ root: webDistPath }));
958
1076
  app.get("/*", serveStatic({ root: webDistPath, path: "index.html" }));
@@ -962,7 +1080,15 @@ async function createServer(port, store, options = {}) {
962
1080
  let actualPort = port;
963
1081
  for (let offset = 0; offset < attempts; offset += 1) {
964
1082
  const candidatePort = port + offset;
965
- server = serve({ fetch: app.fetch, port: candidatePort, hostname });
1083
+ server = serve({
1084
+ fetch: app.fetch,
1085
+ port: candidatePort,
1086
+ hostname,
1087
+ ...transport.kind === "tls" ? {
1088
+ createServer: createHttpsServer,
1089
+ serverOptions: { cert: transport.cert, key: transport.key }
1090
+ } : {}
1091
+ });
966
1092
  try {
967
1093
  await waitForListening(server);
968
1094
  actualPort = getListeningPort(server, candidatePort);
@@ -982,19 +1108,33 @@ async function createServer(port, store, options = {}) {
982
1108
  throw new Error(getServerStartupErrorMessage(error, candidatePort));
983
1109
  }
984
1110
  }
985
- const baseUrl = isLoopback ? `http://localhost:${actualPort}` : `http://${hostname}:${actualPort}`;
1111
+ const scheme = transport.kind === "tls" ? "https" : "http";
1112
+ const baseUrl = isLoopback ? `${scheme}://localhost:${actualPort}` : `${scheme}://${hostname}:${actualPort}`;
986
1113
  const url = remoteAccessToken ? `${baseUrl}/?${REMOTE_ACCESS_QUERY_PARAM}=${encodeURIComponent(remoteAccessToken)}` : baseUrl;
987
1114
  appLogger.info("server.listen", {
988
1115
  port: actualPort,
989
1116
  requested_port: port,
990
1117
  hostname,
991
- remote_access: Boolean(remoteAccessToken)
1118
+ remote_access: Boolean(remoteAccessToken),
1119
+ transport: transport.kind
992
1120
  });
993
1121
  if (!isLoopback) {
994
- appLogger.warn("server.listen.remote_access", { hostname, port: actualPort });
1122
+ appLogger.warn("server.listen.remote_access", {
1123
+ hostname,
1124
+ port: actualPort,
1125
+ transport: transport.kind
1126
+ });
995
1127
  console.warn(`
996
1128
  \u26A0 \u8FDC\u7A0B\u8BBF\u95EE\u5DF2\u542F\u7528\u3002\u4EFB\u4F55\u6301\u6709\u542F\u52A8 URL \u7684\u4EBA\u90FD\u53EF\u4EE5\u8BFB\u53D6\u4F60\u7684 AI \u4F1A\u8BDD\u8BB0\u5F55\u3002
997
1129
  `);
1130
+ if (transport.kind === "plaintext") {
1131
+ appLogger.warn("server.listen.plaintext", { hostname, port: actualPort });
1132
+ console.warn(
1133
+ `\u26A0 \u4F20\u8F93\u672A\u52A0\u5BC6\u3002\u8BBF\u95EE\u4EE4\u724C\u4E0E\u5B8C\u6574\u4F1A\u8BDD\u5185\u5BB9\u4EE5\u660E\u6587\u7ECF\u8FC7\u7F51\u7EDC\uFF0CURL \u4E2D\u7684\u4EE4\u724C\u8FD8\u53EF\u80FD\u5199\u5165\u4EE3\u7406\u8BBF\u95EE\u65E5\u5FD7\u3002
1134
+ \u4F7F\u7528 --tls-cert/--tls-key \u76F4\u63A5\u542F\u7528 TLS\uFF0C\u6216\u5728\u53D7\u4FE1\u53CD\u5411\u4EE3\u7406\u540E\u4F7F\u7528 --trust-proxy\u3002
1135
+ `
1136
+ );
1137
+ }
998
1138
  }
999
1139
  return {
1000
1140
  url,
@@ -1016,6 +1156,227 @@ async function createServer(port, store, options = {}) {
1016
1156
  import { existsSync as existsSync4 } from "fs";
1017
1157
  import { fileURLToPath as fileURLToPath3 } from "url";
1018
1158
 
1159
+ // src/agent-operation-scheduler.ts
1160
+ var PENDING_REFRESH_DELAY_MS = 100;
1161
+ var MAX_ADAPTIVE_REFRESH_DELAY_MS = 3e4;
1162
+ var ADAPTIVE_REFRESH_DELAY_MULTIPLIER = 4;
1163
+ var AgentOperationScheduler = class {
1164
+ constructor(runRefresh) {
1165
+ this.runRefresh = runRefresh;
1166
+ }
1167
+ runRefresh;
1168
+ states = /* @__PURE__ */ new Map();
1169
+ operationGenerations = /* @__PURE__ */ new Map();
1170
+ operationTails = /* @__PURE__ */ new Map();
1171
+ isStopped = false;
1172
+ notify(agentName, delayMs) {
1173
+ if (this.isStopped) return;
1174
+ this.state(agentName).pendingSignalCount += 1;
1175
+ this.schedule(agentName, delayMs);
1176
+ }
1177
+ schedule(agentName, delayMs) {
1178
+ if (this.isStopped) return;
1179
+ const state = this.state(agentName);
1180
+ const adaptiveDelayMs = Math.min(
1181
+ state.lastRefreshDurationMs * ADAPTIVE_REFRESH_DELAY_MULTIPLIER,
1182
+ MAX_ADAPTIVE_REFRESH_DELAY_MS
1183
+ );
1184
+ const effectiveDelayMs = Math.max(delayMs, adaptiveDelayMs);
1185
+ const deadline = Date.now() + effectiveDelayMs;
1186
+ if (state.timer) {
1187
+ if (deadline >= state.timerDeadline) return;
1188
+ clearTimeout(state.timer);
1189
+ }
1190
+ appLogger.debug("scan.refresh.schedule", { agent: agentName, delay_ms: effectiveDelayMs });
1191
+ state.timerDeadline = deadline;
1192
+ state.timer = setTimeout(() => {
1193
+ state.timer = null;
1194
+ state.timerDeadline = 0;
1195
+ void this.refresh(agentName);
1196
+ }, effectiveDelayMs);
1197
+ }
1198
+ async refresh(agentName) {
1199
+ if (this.isStopped) return;
1200
+ const state = this.state(agentName);
1201
+ if (state.isRefreshRunning) {
1202
+ appLogger.debug("scan.refresh.pending", { agent: agentName });
1203
+ state.hasPendingRefresh = true;
1204
+ return;
1205
+ }
1206
+ state.isRefreshRunning = true;
1207
+ try {
1208
+ await this.run(agentName, "refresh", () => this.runRefresh(agentName));
1209
+ } finally {
1210
+ state.isRefreshRunning = false;
1211
+ if (state.hasPendingRefresh && !this.isStopped) {
1212
+ state.hasPendingRefresh = false;
1213
+ this.schedule(agentName, PENDING_REFRESH_DELAY_MS);
1214
+ }
1215
+ }
1216
+ }
1217
+ run(agentName, kind, operation) {
1218
+ const previous = this.operationTails.get(agentName) ?? Promise.resolve();
1219
+ const run = previous.then(async () => {
1220
+ if (this.isStopped) return "skipped";
1221
+ const lifecycle = this.beginOperation(agentName, kind);
1222
+ try {
1223
+ const result = await operation();
1224
+ this.completeOperation(lifecycle, result);
1225
+ return result;
1226
+ } catch (error) {
1227
+ this.completeOperation(lifecycle, "failed");
1228
+ throw error;
1229
+ }
1230
+ });
1231
+ const tail = run.then(
1232
+ () => void 0,
1233
+ () => void 0
1234
+ );
1235
+ this.operationTails.set(agentName, tail);
1236
+ void tail.finally(() => {
1237
+ if (this.operationTails.get(agentName) === tail) this.operationTails.delete(agentName);
1238
+ });
1239
+ return run;
1240
+ }
1241
+ takePendingSignalCount(agentName) {
1242
+ const state = this.state(agentName);
1243
+ const count = state.pendingSignalCount;
1244
+ state.pendingSignalCount = 0;
1245
+ return count;
1246
+ }
1247
+ recordRefreshDuration(agentName, durationMs) {
1248
+ this.state(agentName).lastRefreshDurationMs = durationMs;
1249
+ }
1250
+ snapshot() {
1251
+ return {
1252
+ activeOperations: this.operationTails.size,
1253
+ activeRefreshes: [...this.states.values()].filter((state) => state.isRefreshRunning).length
1254
+ };
1255
+ }
1256
+ stop() {
1257
+ this.isStopped = true;
1258
+ for (const state of this.states.values()) {
1259
+ if (state.timer) clearTimeout(state.timer);
1260
+ state.timer = null;
1261
+ state.timerDeadline = 0;
1262
+ state.hasPendingRefresh = false;
1263
+ state.pendingSignalCount = 0;
1264
+ }
1265
+ }
1266
+ async waitForIdle() {
1267
+ await Promise.allSettled(this.operationTails.values());
1268
+ }
1269
+ state(agentName) {
1270
+ const existing = this.states.get(agentName);
1271
+ if (existing) return existing;
1272
+ const state = {
1273
+ timer: null,
1274
+ timerDeadline: 0,
1275
+ isRefreshRunning: false,
1276
+ hasPendingRefresh: false,
1277
+ lastRefreshDurationMs: 0,
1278
+ pendingSignalCount: 0
1279
+ };
1280
+ this.states.set(agentName, state);
1281
+ return state;
1282
+ }
1283
+ beginOperation(agentName, kind) {
1284
+ const generation = (this.operationGenerations.get(agentName) ?? 0) + 1;
1285
+ const startedAt = Date.now();
1286
+ this.operationGenerations.set(agentName, generation);
1287
+ appLogger.info("scan.agent_operation.started", {
1288
+ agent: agentName,
1289
+ operation: kind,
1290
+ generation,
1291
+ started_at: startedAt
1292
+ });
1293
+ return { agentName, kind, generation, startedAt };
1294
+ }
1295
+ completeOperation(lifecycle, result) {
1296
+ const completedAt = Date.now();
1297
+ appLogger.info("scan.agent_operation.completed", {
1298
+ agent: lifecycle.agentName,
1299
+ operation: lifecycle.kind,
1300
+ generation: lifecycle.generation,
1301
+ started_at: lifecycle.startedAt,
1302
+ completed_at: completedAt,
1303
+ duration_ms: completedAt - lifecycle.startedAt,
1304
+ result
1305
+ });
1306
+ }
1307
+ };
1308
+
1309
+ // src/live-session-index.ts
1310
+ var LiveSessionIndex = class {
1311
+ agents = [];
1312
+ agentsByName = /* @__PURE__ */ new Map();
1313
+ byAgent = {};
1314
+ sessions = [];
1315
+ signatureCaches = /* @__PURE__ */ new Map();
1316
+ initialize(snapshot, options = {}) {
1317
+ const agentMap = /* @__PURE__ */ new Map();
1318
+ for (const agent of snapshot.agents) agentMap.set(agent.name, agent);
1319
+ for (const agent of options.registeredAgents ?? []) {
1320
+ if (!agentMap.has(agent.name)) agentMap.set(agent.name, agent);
1321
+ }
1322
+ this.agents = [...agentMap.values()].filter(
1323
+ (agent) => !options.allowedAgents || options.allowedAgents.has(agent.name.toLowerCase())
1324
+ );
1325
+ this.agentsByName = new Map(this.agents.map((agent) => [agent.name, agent]));
1326
+ this.byAgent = Object.fromEntries(
1327
+ this.agents.map((agent) => [agent.name, sortSessions(snapshot.byAgent[agent.name] ?? [])])
1328
+ );
1329
+ this.sessions = mergeSortedSessions(Object.values(this.byAgent));
1330
+ this.signatureCaches.clear();
1331
+ }
1332
+ snapshot() {
1333
+ return {
1334
+ agents: this.agents,
1335
+ byAgent: this.byAgent,
1336
+ sessions: this.sessions
1337
+ };
1338
+ }
1339
+ findAgent(agentName) {
1340
+ return this.agentsByName.get(agentName);
1341
+ }
1342
+ commitAgentSessions(agentName, nextSessions, candidateChangedIds = []) {
1343
+ const previousSessions = this.byAgent[agentName] ?? [];
1344
+ const signatureCache = this.signatureCache(agentName);
1345
+ const { changes, removedSessionIds, counts } = computeSessionDiff(
1346
+ previousSessions,
1347
+ nextSessions,
1348
+ candidateChangedIds,
1349
+ sessionSignature,
1350
+ signatureCache
1351
+ );
1352
+ for (const removedId of removedSessionIds) signatureCache.delete(removedId);
1353
+ this.byAgent[agentName] = sortSessions(nextSessions);
1354
+ this.sessions = mergeSortedSessions(Object.values(this.byAgent));
1355
+ if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) return null;
1356
+ return {
1357
+ type: "sessions-updated",
1358
+ changedAgents: [agentName],
1359
+ newSessions: counts.new,
1360
+ updatedSessions: counts.updated,
1361
+ removedSessions: counts.removed,
1362
+ totalSessions: this.sessions.length,
1363
+ timestamp: Date.now(),
1364
+ changedSessionHeads: changes.map(({ session }) => ({
1365
+ reference: { agentName, sessionId: session.id },
1366
+ session
1367
+ })),
1368
+ removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
1369
+ };
1370
+ }
1371
+ signatureCache(agentName) {
1372
+ const existing = this.signatureCaches.get(agentName);
1373
+ if (existing) return existing;
1374
+ const cache = /* @__PURE__ */ new Map();
1375
+ this.signatureCaches.set(agentName, cache);
1376
+ return cache;
1377
+ }
1378
+ };
1379
+
1019
1380
  // src/search-index-job-runner.ts
1020
1381
  import { existsSync as existsSync2 } from "fs";
1021
1382
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -1168,7 +1529,6 @@ var SearchIndexJobRunner = class {
1168
1529
  nextBatchId = 1;
1169
1530
  pendingJobs = new PendingSearchIndexJobs();
1170
1531
  isShuttingDown = false;
1171
- hasCheckedFtsIntegrity = false;
1172
1532
  enqueue(context, jobs) {
1173
1533
  if (jobs.length === 0) return Promise.resolve();
1174
1534
  if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE));
@@ -1241,8 +1601,7 @@ var SearchIndexJobRunner = class {
1241
1601
  jobs: batch.jobs,
1242
1602
  agentNames: [],
1243
1603
  sessionsByAgent: {},
1244
- metaByAgent: {},
1245
- skipFtsIntegrityCheck: this.hasCheckedFtsIntegrity
1604
+ metaByAgent: {}
1246
1605
  }
1247
1606
  });
1248
1607
  worker.unref();
@@ -1253,12 +1612,27 @@ var SearchIndexJobRunner = class {
1253
1612
  logSearchIndexSync(message.context, message.result);
1254
1613
  return;
1255
1614
  }
1615
+ if (message.type === "persist-failed") {
1616
+ appLogger.error("search_index.persist_failed", {
1617
+ batch_id: batch.id,
1618
+ context: message.context,
1619
+ stage: message.stage,
1620
+ agent: message.agentName,
1621
+ sessions: message.sessions
1622
+ });
1623
+ this.settle(
1624
+ batch,
1625
+ new Error(
1626
+ `Search index worker failed to persist ${message.stage} for ${message.agentName}`
1627
+ )
1628
+ );
1629
+ return;
1630
+ }
1256
1631
  if (message.type !== "done") return;
1257
1632
  appLogger.info(`${message.context}.done`, {
1258
1633
  duration_ms: Math.round(message.durationMs),
1259
1634
  sessions: message.sessions
1260
1635
  });
1261
- this.hasCheckedFtsIntegrity = true;
1262
1636
  this.settle(batch);
1263
1637
  });
1264
1638
  worker.on("error", (error) => {
@@ -1414,6 +1788,21 @@ var ScanStatusModel = class {
1414
1788
  updatedAt: now
1415
1789
  });
1416
1790
  }
1791
+ indexAgent(agentName) {
1792
+ const status = this.status.agentStatuses[agentName];
1793
+ if (!this.status.active || !status || status.status !== "scanning") return null;
1794
+ const now = Date.now();
1795
+ const agentStatuses = {
1796
+ ...this.status.agentStatuses,
1797
+ [agentName]: { ...status, status: "indexing", updatedAt: now }
1798
+ };
1799
+ return this.set({
1800
+ ...this.status,
1801
+ phase: this.activePhase(this.status.pendingAgents, this.status.scanningAgents, agentStatuses),
1802
+ agentStatuses,
1803
+ updatedAt: now
1804
+ });
1805
+ }
1417
1806
  finishAgent(agentName, sessionCount) {
1418
1807
  const pendingAgents = this.status.pendingAgents.filter((agent) => agent !== agentName);
1419
1808
  const scanningAgents = this.status.scanningAgents.filter((agent) => agent !== agentName);
@@ -1422,26 +1811,27 @@ var ScanStatusModel = class {
1422
1811
  const now = Date.now();
1423
1812
  const previousStatus = this.status.agentStatuses[agentName];
1424
1813
  const total = previousStatus?.total ?? previousStatus?.processed;
1814
+ const agentStatuses = {
1815
+ ...this.status.agentStatuses,
1816
+ [agentName]: {
1817
+ agentName,
1818
+ status: "complete",
1819
+ total,
1820
+ processed: total,
1821
+ sessions: sessionCount ?? previousStatus?.sessions ?? 0,
1822
+ startedAt: previousStatus?.startedAt,
1823
+ updatedAt: now,
1824
+ completedAt: now
1825
+ }
1826
+ };
1425
1827
  return this.set({
1426
1828
  ...this.status,
1427
1829
  active: isActive,
1428
- phase: isActive ? "scanning" : "idle",
1830
+ phase: isActive ? this.activePhase(pendingAgents, scanningAgents, agentStatuses) : "idle",
1429
1831
  pendingAgents,
1430
1832
  scanningAgents,
1431
1833
  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
- },
1834
+ agentStatuses,
1445
1835
  updatedAt: now,
1446
1836
  completedAt: isActive ? void 0 : now
1447
1837
  });
@@ -1471,6 +1861,12 @@ var ScanStatusModel = class {
1471
1861
  updatedAt: Date.now()
1472
1862
  });
1473
1863
  }
1864
+ activePhase(pendingAgents, scanningAgents, agentStatuses) {
1865
+ if (pendingAgents.length === 0 && scanningAgents.length > 0 && scanningAgents.every((agentName) => agentStatuses[agentName]?.status === "indexing")) {
1866
+ return "indexing";
1867
+ }
1868
+ return this.status.phase === "initializing" ? "initializing" : "scanning";
1869
+ }
1474
1870
  set(status) {
1475
1871
  this.status = status;
1476
1872
  return this.snapshot();
@@ -1480,38 +1876,16 @@ var ScanStatusModel = class {
1480
1876
  // src/agent-sync-engine.ts
1481
1877
  var REFRESH_DEBOUNCE_MS = 200;
1482
1878
  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
1879
  var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
1487
1880
  var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1488
- function buildRefreshDiff(agentName, previousSessions, nextSessions, candidateChangedIds = [], signatureCache) {
1489
- const { changes, removedSessionIds, counts } = computeSessionDiff(
1881
+ function buildPersistenceDiff(previousSessions, nextSessions, candidateChangedIds = []) {
1882
+ const { changes, removedSessionIds } = computeSessionDiff(
1490
1883
  previousSessions,
1491
1884
  nextSessions,
1492
1885
  candidateChangedIds,
1493
- sessionSignature,
1494
- signatureCache
1886
+ sessionSignature
1495
1887
  );
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
- };
1888
+ return { changedSessions: changes, removedSessionIds };
1515
1889
  }
1516
1890
  function restoreAgentCacheMeta(agent, cached) {
1517
1891
  agent.setSessionMetaMap(new Map(Object.entries(cached.meta)));
@@ -1519,11 +1893,12 @@ function restoreAgentCacheMeta(agent, cached) {
1519
1893
  var AgentSyncEngine = class {
1520
1894
  constructor(options) {
1521
1895
  this.options = options;
1896
+ this.scheduler = new AgentOperationScheduler((agentName) => this.performRefresh(agentName));
1522
1897
  }
1523
- options;
1524
- refreshStates = /* @__PURE__ */ new Map();
1525
- operationGenerations = /* @__PURE__ */ new Map();
1526
- operationTails = /* @__PURE__ */ new Map();
1898
+ options;
1899
+ lastRefreshAtByAgent = /* @__PURE__ */ new Map();
1900
+ scheduler;
1901
+ sessionIndex = new LiveSessionIndex();
1527
1902
  backfillQueue = [];
1528
1903
  currentBackfillAgent;
1529
1904
  completedBackfillAgents = [];
@@ -1532,13 +1907,22 @@ var AgentSyncEngine = class {
1532
1907
  statusChangedListeners = /* @__PURE__ */ new Set();
1533
1908
  scanStatus = new ScanStatusModel();
1534
1909
  searchIndexJobs = new SearchIndexJobRunner();
1910
+ nextPublicationId = 1;
1535
1911
  backgroundRefreshTimer = null;
1536
1912
  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();
1913
+ initialize(snapshot, options = {}) {
1914
+ this.sessionIndex.initialize(snapshot, options);
1915
+ this.lastRefreshAtByAgent.clear();
1916
+ for (const agent of this.sessionIndex.snapshot().agents) {
1917
+ this.lastRefreshAtByAgent.set(
1918
+ agent.name,
1919
+ options.cacheTimestamps?.[agent.name] ?? Date.now()
1920
+ );
1540
1921
  }
1541
1922
  }
1923
+ snapshot() {
1924
+ return this.sessionIndex.snapshot();
1925
+ }
1542
1926
  status() {
1543
1927
  return this.scanStatus.snapshot();
1544
1928
  }
@@ -1551,49 +1935,45 @@ var AgentSyncEngine = class {
1551
1935
  return () => this.statusChangedListeners.delete(listener);
1552
1936
  }
1553
1937
  async syncInitialIndex() {
1554
- await this.searchIndexJobs.enqueue(
1555
- "scan.initial",
1556
- this.buildFullSearchIndexJobs("scan.initial")
1557
- );
1938
+ const jobs = this.buildFullSearchIndexJobs("scan.initial");
1939
+ await this.commitSearchIndex("scan.initial", jobs, {
1940
+ publicationId: this.publicationId("scan.initial"),
1941
+ agents: jobs.map((job) => job.agentName)
1942
+ });
1558
1943
  }
1559
1944
  handleAgentsChanged(agentNames) {
1560
- const snapshot = this.options.snapshot();
1945
+ const snapshot = this.sessionIndex.snapshot();
1561
1946
  for (const agentName of agentNames) {
1562
- this.state(agentName).pendingPathCount += 1;
1563
1947
  const delayMs = (snapshot.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
1564
- this.scheduleRefresh(agentName, delayMs);
1948
+ this.scheduler.notify(agentName, delayMs);
1565
1949
  }
1566
1950
  }
1567
1951
  startBackgroundRefresh() {
1568
1952
  if (this.backgroundRefreshTimer) return;
1569
- const agentNames = this.options.snapshot().agents.map((agent) => agent.name);
1953
+ const agentNames = this.sessionIndex.snapshot().agents.map((agent) => agent.name);
1570
1954
  this.startScanBatch(agentNames, "scanning");
1571
1955
  this.backgroundRefreshTimer = setTimeout(() => {
1572
1956
  this.backgroundRefreshTimer = null;
1573
- for (const agentName of agentNames) this.scheduleRefresh(agentName, 0);
1957
+ for (const agentName of agentNames) this.scheduler.schedule(agentName, 0);
1574
1958
  if (agentNames.length === 0) this.finishScanBatch();
1575
1959
  }, 0);
1576
1960
  }
1577
1961
  async refresh(agentName) {
1578
- await this.runCoalescedRefresh(agentName);
1962
+ await this.scheduler.refresh(agentName);
1579
1963
  }
1580
1964
  async shutdown() {
1581
1965
  this.isShuttingDown = true;
1966
+ const schedulerSnapshot = this.scheduler.snapshot();
1582
1967
  const activeOperations = {
1583
- agent_operations: this.operationTails.size,
1584
- refreshes: [...this.refreshStates.values()].filter((state) => state.isRunning).length,
1968
+ agent_operations: schedulerSnapshot.activeOperations,
1969
+ refreshes: schedulerSnapshot.activeRefreshes,
1585
1970
  backfill_running: this.currentBackfillAgent != null || void 0,
1586
1971
  scan_workers: this.options.workerRunner.activeCount
1587
1972
  };
1588
1973
  if (activeOperations.agent_operations > 0 || activeOperations.scan_workers > 0) {
1589
1974
  appLogger.warn("scan.shutdown.active_operations", activeOperations);
1590
1975
  }
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
- }
1976
+ this.scheduler.stop();
1597
1977
  if (this.backgroundRefreshTimer) {
1598
1978
  clearTimeout(this.backgroundRefreshTimer);
1599
1979
  this.backgroundRefreshTimer = null;
@@ -1607,7 +1987,7 @@ var AgentSyncEngine = class {
1607
1987
  });
1608
1988
  await this.searchIndexJobs.shutdown();
1609
1989
  await this.options.workerRunner.shutdown();
1610
- await Promise.allSettled(this.operationTails.values());
1990
+ await this.scheduler.waitForIdle();
1611
1991
  const stoppedSearchIndexSnapshot = this.searchIndexJobs.snapshot();
1612
1992
  appLogger.info("search_index.shutdown.completed", {
1613
1993
  active_batch_id: searchIndexSnapshot.activeBatchId,
@@ -1615,7 +1995,7 @@ var AgentSyncEngine = class {
1615
1995
  });
1616
1996
  }
1617
1997
  startScanBatch(agentNames, phase) {
1618
- const snapshot = this.options.snapshot();
1998
+ const snapshot = this.sessionIndex.snapshot();
1619
1999
  const sessionCounts = Object.fromEntries(
1620
2000
  agentNames.map((agentName) => [agentName, snapshot.byAgent[agentName]?.length ?? 0])
1621
2001
  );
@@ -1625,7 +2005,7 @@ var AgentSyncEngine = class {
1625
2005
  this.publishStatus(this.scanStatus.setPhase(phase));
1626
2006
  }
1627
2007
  beginAgentScan(agentName) {
1628
- const snapshot = this.options.snapshot();
2008
+ const snapshot = this.sessionIndex.snapshot();
1629
2009
  if (!this.scanStatus.snapshot().active) this.startScanBatch([agentName], "scanning");
1630
2010
  this.publishStatus(
1631
2011
  this.scanStatus.beginAgent(agentName, snapshot.byAgent[agentName]?.length ?? 0)
@@ -1634,8 +2014,11 @@ var AgentSyncEngine = class {
1634
2014
  updateAgentScanProgress(agentName, progress) {
1635
2015
  this.publishStatus(this.scanStatus.updateAgent(agentName, progress));
1636
2016
  }
2017
+ beginAgentIndexing(agentName) {
2018
+ this.publishStatus(this.scanStatus.indexAgent(agentName));
2019
+ }
1637
2020
  finishAgentScan(agentName) {
1638
- const count = this.options.snapshot().byAgent[agentName]?.length;
2021
+ const count = this.sessionIndex.snapshot().byAgent[agentName]?.length;
1639
2022
  this.publishStatus(this.scanStatus.finishAgent(agentName, count));
1640
2023
  }
1641
2024
  finishScanBatch() {
@@ -1652,44 +2035,6 @@ var AgentSyncEngine = class {
1652
2035
  if (this.isShuttingDown) return;
1653
2036
  for (const listener of this.sessionsChangedListeners) listener(change);
1654
2037
  }
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
2038
  async performRefresh(agentName) {
1694
2039
  this.beginAgentScan(agentName);
1695
2040
  try {
@@ -1706,18 +2051,16 @@ var AgentSyncEngine = class {
1706
2051
  }
1707
2052
  async runRefresh(agentName) {
1708
2053
  const startedAt = performance.now();
1709
- const state = this.state(agentName);
1710
- const pendingPathCount = state.pendingPathCount;
1711
- state.pendingPathCount = 0;
2054
+ const pendingPathCount = this.scheduler.takePendingSignalCount(agentName);
1712
2055
  const agent = this.findAgent(agentName);
1713
2056
  if (!agent) {
1714
2057
  appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
1715
2058
  return "skipped";
1716
2059
  }
1717
- const previousSessions = this.options.snapshot().byAgent[agentName] ?? [];
2060
+ const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
1718
2061
  const cached = loadCachedSessions(agentName);
1719
2062
  const refreshBaseline = cached?.sessions ?? previousSessions;
1720
- const cacheTimestamp = cached?.timestamp ?? state.lastRefreshAt;
2063
+ const cacheTimestamp = cached?.timestamp ?? this.lastRefreshAtByAgent.get(agentName) ?? 0;
1721
2064
  if (cached) restoreAgentCacheMeta(agent, cached);
1722
2065
  const isInitialized = isAgentCacheInitialized(agentName);
1723
2066
  const availabilityStartedAt = performance.now();
@@ -1742,74 +2085,73 @@ var AgentSyncEngine = class {
1742
2085
  }
1743
2086
  if (strategyResult.status === "unchanged") return "unchanged";
1744
2087
  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
2088
  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;
2089
+ const persistenceDiff = strategyResult.persistenceDiff;
2090
+ const changedSessionIds = persistenceDiff ? new Set(persistenceDiff.changedSessions.map(({ session }) => session.id)) : void 0;
1758
2091
  const persistStartedAt = performance.now();
1759
- const persistentJob = strategyResult.usedIncrementalScan ? {
2092
+ const persistentJob = persistenceDiff ? {
1760
2093
  kind: "changes",
1761
2094
  context: "scan.refresh",
1762
2095
  agentName,
1763
- changes: persistentChanges,
1764
- removedSessionIds: persistentRemovedSessionIds,
2096
+ changes: persistenceDiff.changedSessions,
2097
+ removedSessionIds: persistenceDiff.removedSessionIds,
1765
2098
  meta: buildAgentCacheMeta(agent, changedSessionIds),
1766
2099
  ...searchIndexOptions ? { searchIndexOptions } : {}
1767
- } : strategyResult.fullScanSessions ? {
2100
+ } : {
1768
2101
  kind: "full",
1769
2102
  context: "scan.refresh",
1770
2103
  agentName,
1771
- sessions: strategyResult.fullScanSessions,
2104
+ sessions: strategyResult.fullScanSessions ?? nextSessions,
1772
2105
  meta: buildAgentCacheMeta(agent),
1773
2106
  saveCache: true,
1774
2107
  ...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
- }
2108
+ };
2109
+ this.beginAgentIndexing(agentName);
2110
+ const publication = await this.commitSessionPublication({
2111
+ context: "scan.refresh",
2112
+ agentName,
2113
+ sessions: nextSessions,
2114
+ candidateChangedIds: strategyResult.preciseChangedIds ?? [],
2115
+ indexJob: persistentJob
2116
+ });
1787
2117
  const persistDuration = performance.now() - persistStartedAt;
1788
2118
  logSearchIndexSync("scan.refresh", null, { pending_paths: pendingPathCount });
1789
- this.emitSessionsChanged({ agentName, sessions: nextSessions, event: diff.event });
1790
2119
  const totalDurationMs = performance.now() - startedAt;
1791
- state.lastRefreshDurationMs = totalDurationMs;
2120
+ this.scheduler.recordRefreshDuration(agentName, totalDurationMs);
1792
2121
  appLogger.info("scan.refresh.done", {
1793
2122
  agent: agentName,
1794
2123
  duration_ms: Math.round(totalDurationMs),
1795
2124
  sessions: nextSessions.length,
1796
- new_sessions: diff.event?.newSessions ?? 0,
1797
- updated_sessions: diff.event?.updatedSessions ?? 0,
1798
- removed_sessions: diff.event?.removedSessions ?? 0,
2125
+ new_sessions: publication.event?.newSessions ?? 0,
2126
+ updated_sessions: publication.event?.updatedSessions ?? 0,
2127
+ removed_sessions: publication.event?.removedSessions ?? 0,
1799
2128
  pending_paths: pendingPathCount,
1800
2129
  availability_ms: Math.round(availabilityDuration),
1801
2130
  check_ms: Math.round(strategyResult.checkDuration),
1802
2131
  scan_ms: Math.round(strategyResult.scanDuration),
1803
- diff_ms: Math.round(diffDuration),
2132
+ diff_ms: Math.round(publication.diffDuration),
1804
2133
  persist_ms: Math.round(persistDuration),
1805
- search_index_ms: 0,
1806
- persistent_index_worker_job: persistentJob?.kind,
1807
- persistent_index_skipped: !persistentJob || void 0
2134
+ search_index_ms: Math.round(persistDuration),
2135
+ persistent_index_worker_job: persistentJob.kind
1808
2136
  });
1809
2137
  return "committed";
1810
2138
  }
2139
+ /**
2140
+ * An agent that cannot be reached was never scanned, so there is nothing to
2141
+ * compare against. Publishing an empty result here would diff every known
2142
+ * session into a removal — the same mistake as reading a failed query as an
2143
+ * empty database.
2144
+ */
1811
2145
  refreshUnavailableAgent(agentName) {
1812
- this.state(agentName).lastRefreshAt = Date.now();
2146
+ this.lastRefreshAtByAgent.set(agentName, Date.now());
2147
+ const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
2148
+ if (previousSessions.length > 0) {
2149
+ appLogger.warn("scan.refresh.agent_unavailable", {
2150
+ agent: agentName,
2151
+ retained_sessions: previousSessions.length
2152
+ });
2153
+ return this.refreshStrategyResult(previousSessions, { status: "unchanged" });
2154
+ }
1813
2155
  return this.refreshStrategyResult([]);
1814
2156
  }
1815
2157
  async initializeAgent(agent, previousSessions) {
@@ -1818,7 +2160,7 @@ var AgentSyncEngine = class {
1818
2160
  const result = await this.runWorker(agent, previousSessions, null, this.startupScanOptions());
1819
2161
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
1820
2162
  const sessions = attachMissingProjectIdentities(result.sessions);
1821
- this.state(agent.name).lastRefreshAt = Date.now();
2163
+ this.lastRefreshAtByAgent.set(agent.name, Date.now());
1822
2164
  return this.refreshStrategyResult(sessions, {
1823
2165
  fullScanSessions: sessions,
1824
2166
  scanDuration: performance.now() - scanStartedAt
@@ -1833,17 +2175,17 @@ var AgentSyncEngine = class {
1833
2175
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
1834
2176
  const sessions = attachMissingProjectIdentities(result.sessions);
1835
2177
  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);
2178
+ const persistenceDiff = buildPersistenceDiff(cached.sessions, sessions, preciseChangedIds);
2179
+ this.lastRefreshAtByAgent.set(agent.name, Date.now());
2180
+ if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0) {
2181
+ this.logUnchangedRefresh(agent.name, refreshStartedAt);
2182
+ return this.refreshStrategyResult(sessions, {
2183
+ status: "unchanged",
2184
+ scanDuration: performance.now() - scanStartedAt
2185
+ });
2186
+ }
1844
2187
  return this.refreshStrategyResult(sessions, {
1845
2188
  preciseChangedIds,
1846
- usedIncrementalScan: true,
1847
2189
  persistenceDiff,
1848
2190
  scanDuration: performance.now() - scanStartedAt
1849
2191
  });
@@ -1852,22 +2194,29 @@ var AgentSyncEngine = class {
1852
2194
  const checkStartedAt = performance.now();
1853
2195
  const checkResult = await Promise.resolve(agent.checkForChanges(cacheTimestamp, baseline));
1854
2196
  const checkDuration = performance.now() - checkStartedAt;
1855
- this.state(agent.name).lastRefreshAt = checkResult.timestamp;
2197
+ this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
1856
2198
  if (!checkResult.hasChanges) {
1857
2199
  this.logUnchangedRefresh(agent.name, refreshStartedAt);
1858
2200
  return this.refreshStrategyResult(baseline, { status: "unchanged", checkDuration });
1859
2201
  }
1860
2202
  const preciseChangedIds = checkResult.changedIds ?? null;
1861
2203
  const scanStartedAt = performance.now();
2204
+ if (preciseChangedIds === null) {
2205
+ const result = await this.runWorker(agent, baseline, null, {});
2206
+ agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
2207
+ const sessions2 = attachMissingProjectIdentities(result.sessions);
2208
+ return this.refreshStrategyResult(sessions2, {
2209
+ persistenceDiff: buildPersistenceDiff(baseline, sessions2),
2210
+ checkDuration,
2211
+ scanDuration: performance.now() - scanStartedAt
2212
+ });
2213
+ }
1862
2214
  const sessions = attachMissingProjectIdentities(
1863
- await Promise.resolve(
1864
- agent.incrementalScan(baseline, checkResult.changedIds ?? [], checkResult.refs)
1865
- )
2215
+ await Promise.resolve(agent.incrementalScan(baseline, preciseChangedIds, checkResult.refs))
1866
2216
  );
1867
2217
  return this.refreshStrategyResult(sessions, {
1868
2218
  preciseChangedIds,
1869
- usedIncrementalScan: Array.isArray(checkResult.changedIds),
1870
- persistenceDiff: buildRefreshDiff(agent.name, baseline, sessions, preciseChangedIds ?? []),
2219
+ persistenceDiff: buildPersistenceDiff(baseline, sessions, preciseChangedIds),
1871
2220
  checkDuration,
1872
2221
  scanDuration: performance.now() - scanStartedAt
1873
2222
  });
@@ -1877,7 +2226,7 @@ var AgentSyncEngine = class {
1877
2226
  const result = await this.runWorker(agent, previousSessions, null, {});
1878
2227
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
1879
2228
  const sessions = attachMissingProjectIdentities(result.sessions);
1880
- this.state(agent.name).lastRefreshAt = Date.now();
2229
+ this.lastRefreshAtByAgent.set(agent.name, Date.now());
1881
2230
  return this.refreshStrategyResult(sessions, {
1882
2231
  fullScanSessions: sessions,
1883
2232
  scanDuration: performance.now() - scanStartedAt
@@ -1889,7 +2238,6 @@ var AgentSyncEngine = class {
1889
2238
  nextSessions,
1890
2239
  fullScanSessions: null,
1891
2240
  preciseChangedIds: null,
1892
- usedIncrementalScan: false,
1893
2241
  persistenceDiff: null,
1894
2242
  checkDuration: 0,
1895
2243
  scanDuration: 0,
@@ -1927,30 +2275,31 @@ var AgentSyncEngine = class {
1927
2275
  if (!agentName) return;
1928
2276
  this.currentBackfillAgent = agentName;
1929
2277
  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);
2278
+ void this.runBackfill(agentName).then((result) => {
2279
+ if (this.isShuttingDown) return;
2280
+ this.currentBackfillAgent = void 0;
2281
+ if (result === "committed") {
2282
+ if (!this.completedBackfillAgents.includes(agentName)) {
2283
+ this.completedBackfillAgents.push(agentName);
1943
2284
  }
1944
- this.publishBackfillStatus();
1945
- this.pumpBackfillQueue();
2285
+ this.failedBackfillAgents = this.failedBackfillAgents.filter(
2286
+ (failedAgent) => failedAgent !== agentName
2287
+ );
2288
+ } else if (!this.failedBackfillAgents.includes(agentName)) {
2289
+ this.failedBackfillAgents.push(agentName);
1946
2290
  }
1947
- );
2291
+ this.publishBackfillStatus();
2292
+ this.pumpBackfillQueue();
2293
+ });
2294
+ }
2295
+ runBackfill(agentName) {
2296
+ return this.scheduler.run(agentName, "backfill", () => this.performBackfill(agentName));
1948
2297
  }
1949
2298
  async performBackfill(agentName) {
1950
2299
  const startedAt = performance.now();
1951
2300
  const agent = this.findAgent(agentName);
1952
2301
  if (!agent || !agent.isAvailable()) return "skipped";
1953
- const snapshot = this.options.snapshot();
2302
+ const snapshot = this.sessionIndex.snapshot();
1954
2303
  const cached = loadCachedSessions(agentName);
1955
2304
  const baseline = cached?.sessions ?? snapshot.byAgent[agentName] ?? [];
1956
2305
  const meta = cached?.meta ?? buildAgentCacheMeta(agent);
@@ -1968,14 +2317,12 @@ var AgentSyncEngine = class {
1968
2317
  );
1969
2318
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
1970
2319
  const fullSessions = attachMissingProjectIdentities(result.sessions);
1971
- const diff = buildRefreshDiff(
2320
+ await this.commitSessionPublication({
2321
+ context: "scan.backfill",
1972
2322
  agentName,
1973
- snapshot.byAgent[agentName] ?? [],
1974
- fullSessions,
1975
- result.changedIds ?? []
1976
- );
1977
- await this.searchIndexJobs.enqueue("scan.backfill", [
1978
- {
2323
+ sessions: fullSessions,
2324
+ candidateChangedIds: result.changedIds ?? [],
2325
+ indexJob: {
1979
2326
  kind: "full",
1980
2327
  context: "scan.backfill",
1981
2328
  agentName,
@@ -1983,9 +2330,8 @@ var AgentSyncEngine = class {
1983
2330
  meta: buildAgentCacheMeta(agent),
1984
2331
  saveCache: true
1985
2332
  }
1986
- ]);
2333
+ });
1987
2334
  markAgentFullSyncCompleted(agentName);
1988
- this.emitSessionsChanged({ agentName, sessions: fullSessions, event: diff.event });
1989
2335
  appLogger.info("scan.backfill.done", {
1990
2336
  agent: agentName,
1991
2337
  duration_ms: Math.round(performance.now() - startedAt),
@@ -1999,70 +2345,6 @@ var AgentSyncEngine = class {
1999
2345
  return "failed";
2000
2346
  }
2001
2347
  }
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
2348
  backfillStatus() {
2067
2349
  return {
2068
2350
  active: this.currentBackfillAgent != null || this.backfillQueue.length > 0,
@@ -2073,7 +2355,7 @@ var AgentSyncEngine = class {
2073
2355
  };
2074
2356
  }
2075
2357
  buildFullSearchIndexJobs(context) {
2076
- const snapshot = this.options.snapshot();
2358
+ const snapshot = this.sessionIndex.snapshot();
2077
2359
  return snapshot.agents.map((agent) => {
2078
2360
  const cached = loadCachedSessions(agent.name);
2079
2361
  return cached ? {
@@ -2091,8 +2373,65 @@ var AgentSyncEngine = class {
2091
2373
  };
2092
2374
  });
2093
2375
  }
2376
+ publicationId(context, agentName) {
2377
+ const id = this.nextPublicationId++;
2378
+ return agentName ? `${context}:${agentName}:${id}` : `${context}:${id}`;
2379
+ }
2380
+ async commitSearchIndex(context, jobs, details) {
2381
+ appLogger.info("session.publication.prepared", {
2382
+ publication_id: details.publicationId,
2383
+ context,
2384
+ agent: details.agent,
2385
+ agents: details.agents,
2386
+ jobs: jobs.length
2387
+ });
2388
+ try {
2389
+ await this.searchIndexJobs.enqueue(context, jobs);
2390
+ } catch (error) {
2391
+ appLogger.error("session.publication.failed", {
2392
+ publication_id: details.publicationId,
2393
+ context,
2394
+ agent: details.agent,
2395
+ stage: "search_index",
2396
+ error
2397
+ });
2398
+ throw error;
2399
+ }
2400
+ appLogger.info("session.publication.index_committed", {
2401
+ publication_id: details.publicationId,
2402
+ context,
2403
+ agent: details.agent
2404
+ });
2405
+ }
2406
+ async commitSessionPublication(publication) {
2407
+ const publicationId = this.publicationId(publication.context, publication.agentName);
2408
+ await this.commitSearchIndex(publication.context, [publication.indexJob], {
2409
+ publicationId,
2410
+ agent: publication.agentName
2411
+ });
2412
+ const diffStartedAt = performance.now();
2413
+ const event = this.sessionIndex.commitAgentSessions(
2414
+ publication.agentName,
2415
+ publication.sessions,
2416
+ publication.candidateChangedIds
2417
+ );
2418
+ const diffDuration = performance.now() - diffStartedAt;
2419
+ this.emitSessionsChanged({
2420
+ agentName: publication.agentName,
2421
+ sessions: this.sessionIndex.snapshot().byAgent[publication.agentName] ?? [],
2422
+ event
2423
+ });
2424
+ appLogger.info("session.publication.published", {
2425
+ publication_id: publicationId,
2426
+ context: publication.context,
2427
+ agent: publication.agentName,
2428
+ sessions: publication.sessions.length,
2429
+ has_event: event != null
2430
+ });
2431
+ return { event, diffDuration };
2432
+ }
2094
2433
  findAgent(agentName) {
2095
- return this.options.snapshot().agents.find((agent) => agent.name === agentName);
2434
+ return this.sessionIndex.findAgent(agentName);
2096
2435
  }
2097
2436
  startupScanOptions() {
2098
2437
  return this.options.startupScanOptions ?? {};
@@ -2148,8 +2487,12 @@ function isSameOrChildPath(parentPath, childPath) {
2148
2487
  const path = relative(parentPath, childPath);
2149
2488
  return path === "" || !path.startsWith("..") && !isAbsolute(path);
2150
2489
  }
2490
+ var SQLITE_SIDECAR_SUFFIXES = ["-wal", "-journal"];
2491
+ function isSqliteSidecarOf(changedPath, targetPath) {
2492
+ return SQLITE_SIDECAR_SUFFIXES.some((suffix) => changedPath === `${targetPath}${suffix}`);
2493
+ }
2151
2494
  function isRelatedPath(changedPath, targetPath) {
2152
- return isSameOrChildPath(targetPath, changedPath) || isSameOrChildPath(changedPath, targetPath);
2495
+ return isSameOrChildPath(targetPath, changedPath) || isSameOrChildPath(changedPath, targetPath) || isSqliteSidecarOf(changedPath, targetPath);
2153
2496
  }
2154
2497
  function mergeScopes(target, scopes) {
2155
2498
  for (const scope of scopes) {
@@ -2167,49 +2510,6 @@ function resolveWatchEventPath(watchPath, filename) {
2167
2510
  }
2168
2511
  return isAbsolute(filenameText) ? filenameText : join(watchPath, filenameText);
2169
2512
  }
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
2513
  var SessionWatcher = class {
2214
2514
  watchers = [];
2215
2515
  fallbackWatchScopes = /* @__PURE__ */ new Map();
@@ -2222,16 +2522,24 @@ var SessionWatcher = class {
2222
2522
  this.listeners.delete(cb);
2223
2523
  };
2224
2524
  }
2225
- /** Begin watching the given agent names' data directories. */
2226
- start(agentNames) {
2525
+ /** Begin watching the session sources declared by each agent adapter. */
2526
+ start(agents) {
2227
2527
  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 });
2528
+ for (const agent of agents) {
2529
+ const plan = agent.getSessionWatchPlan();
2530
+ if (plan.status !== "supported") {
2531
+ appLogger.debug("watch.skip", {
2532
+ agent: agent.name,
2533
+ status: plan.status,
2534
+ reason: plan.reason
2535
+ });
2536
+ continue;
2537
+ }
2538
+ if (plan.targets.length === 0) {
2539
+ appLogger.debug("watch.skip", { agent: agent.name, status: plan.status });
2232
2540
  continue;
2233
2541
  }
2234
- for (const target of watchTargets) {
2542
+ for (const target of plan.targets) {
2235
2543
  const watchRootPath = closestWatchablePath(target.root ?? target.path);
2236
2544
  if (!watchRootPath) continue;
2237
2545
  let rootPath;
@@ -2243,17 +2551,17 @@ var SessionWatcher = class {
2243
2551
  }
2244
2552
  const targetPath = toAbsolutePath(target.path);
2245
2553
  const scopes = scopesByRoot.get(rootPath) ?? [];
2246
- if (!scopes.some((scope) => scope.agentName === agentName && scope.targetPath === targetPath)) {
2247
- scopes.push({ agentName, targetPath });
2554
+ if (!scopes.some((scope) => scope.agentName === agent.name && scope.targetPath === targetPath)) {
2555
+ scopes.push({ agentName: agent.name, targetPath });
2248
2556
  }
2249
2557
  scopesByRoot.set(rootPath, scopes);
2250
2558
  }
2251
2559
  }
2252
2560
  for (const [rootPath, scopes] of scopesByRoot.entries()) {
2253
- const agents = Array.from(new Set(scopes.map((scope) => scope.agentName)));
2561
+ const agents2 = Array.from(new Set(scopes.map((scope) => scope.agentName)));
2254
2562
  appLogger.info("watch.start", {
2255
2563
  root: rootPath,
2256
- agents,
2564
+ agents: agents2,
2257
2565
  targets: scopes.map((scope) => ({
2258
2566
  agent: scope.agentName,
2259
2567
  path: scope.targetPath
@@ -2425,115 +2733,165 @@ var SessionWatcher = class {
2425
2733
 
2426
2734
  // src/worker-runner.ts
2427
2735
  import { Worker as Worker2 } from "worker_threads";
2736
+ var SHUTDOWN_ERROR_MESSAGE2 = "Scan refresh worker shut down";
2737
+ function applySessionChanges2(previousSessions, changes, removedSessionIds) {
2738
+ const replacedIds = new Set(removedSessionIds);
2739
+ for (const { session } of changes) replacedIds.add(session.id);
2740
+ const retained = previousSessions.filter((session) => !replacedIds.has(session.id));
2741
+ const next = Array.from({
2742
+ length: retained.length + changes.length
2743
+ });
2744
+ for (const { session, sortIndex } of changes) {
2745
+ if (sortIndex < 0 || sortIndex >= next.length || next[sortIndex]) {
2746
+ throw new Error(`Invalid scan refresh sort index: ${sortIndex}`);
2747
+ }
2748
+ next[sortIndex] = session;
2749
+ }
2750
+ let retainedIndex = 0;
2751
+ for (let index = 0; index < next.length; index += 1) {
2752
+ if (!next[index]) next[index] = retained[retainedIndex++];
2753
+ }
2754
+ if (retainedIndex !== retained.length || next.some((session) => !session)) {
2755
+ throw new Error("Invalid scan refresh delta");
2756
+ }
2757
+ return next;
2758
+ }
2759
+ function applyMetaChanges(previous, changed, replacedSessionIds) {
2760
+ const next = { ...previous };
2761
+ for (const id of replacedSessionIds) delete next[id];
2762
+ return Object.assign(next, changed);
2763
+ }
2428
2764
  var ThreadWorkerRunner = class {
2429
2765
  constructor(workerUrl) {
2430
2766
  this.workerUrl = workerUrl;
2431
2767
  }
2432
2768
  workerUrl;
2433
- workers = /* @__PURE__ */ new Set();
2769
+ workers = /* @__PURE__ */ new Map();
2770
+ nextRequestId = 1;
2771
+ isShuttingDown = false;
2434
2772
  get activeCount() {
2435
- return this.workers.size;
2773
+ let count = 0;
2774
+ for (const slot of this.workers.values()) count += slot.pending.size;
2775
+ return count;
2436
2776
  }
2437
2777
  run(agentName, payload) {
2778
+ if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE2));
2779
+ const request = {
2780
+ type: "run",
2781
+ requestId: this.nextRequestId++,
2782
+ agentName,
2783
+ previousSessions: payload.previousSessions,
2784
+ changedIds: payload.changedIds,
2785
+ sourceSync: payload.sourceSync,
2786
+ scanOptions: payload.scanOptions,
2787
+ meta: payload.meta
2788
+ };
2438
2789
  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
- );
2790
+ let slot = this.workers.get(agentName);
2791
+ const isNewWorker = !slot;
2792
+ if (!slot) {
2793
+ try {
2794
+ slot = this.createWorker(agentName, request);
2795
+ } catch (error) {
2796
+ reject(error instanceof Error ? error : new Error(String(error)));
2472
2797
  return;
2473
2798
  }
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
- );
2799
+ }
2800
+ slot.pending.set(request.requestId, {
2801
+ resolve: resolve4,
2802
+ reject,
2803
+ payload,
2804
+ onProgress: payload.onProgress
2486
2805
  });
2806
+ if (!isNewWorker) {
2807
+ try {
2808
+ slot.worker.postMessage(request);
2809
+ } catch (error) {
2810
+ slot.pending.delete(request.requestId);
2811
+ reject(error instanceof Error ? error : new Error(String(error)));
2812
+ }
2813
+ }
2487
2814
  });
2488
2815
  }
2489
2816
  async shutdown() {
2490
- const workers = [...this.workers];
2491
- await Promise.allSettled(workers.map((worker) => worker.terminate()));
2817
+ this.isShuttingDown = true;
2818
+ const slots = [...this.workers.values()];
2492
2819
  this.workers.clear();
2820
+ const shutdownError = new Error(SHUTDOWN_ERROR_MESSAGE2);
2821
+ for (const slot of slots) {
2822
+ slot.closed = true;
2823
+ for (const pending of slot.pending.values()) pending.reject(shutdownError);
2824
+ slot.pending.clear();
2825
+ }
2826
+ await Promise.allSettled(slots.map((slot) => slot.worker.terminate()));
2827
+ }
2828
+ createWorker(agentName, request) {
2829
+ const worker = new Worker2(this.workerUrl, { workerData: request });
2830
+ const slot = { worker, pending: /* @__PURE__ */ new Map(), closed: false };
2831
+ worker.unref();
2832
+ this.workers.set(agentName, slot);
2833
+ worker.on("message", (message) => {
2834
+ this.handleMessage(slot, message);
2835
+ });
2836
+ worker.on("error", (error) => {
2837
+ this.closeWorker(agentName, slot, error);
2838
+ });
2839
+ worker.on("exit", (code) => {
2840
+ if (slot.closed) return;
2841
+ const error = new Error(`Scan refresh worker exited before completing (code ${code})`);
2842
+ if (slot.pending.size > 0) {
2843
+ appLogger.warn("scan.refresh_worker.exit_before_done", { agent: agentName, code });
2844
+ }
2845
+ this.closeWorker(agentName, slot, error);
2846
+ });
2847
+ return slot;
2848
+ }
2849
+ handleMessage(slot, message) {
2850
+ const pending = slot.pending.get(message.requestId);
2851
+ if (!pending) return;
2852
+ if (message.type === "progress") {
2853
+ pending.onProgress?.(message.progress);
2854
+ return;
2855
+ }
2856
+ slot.pending.delete(message.requestId);
2857
+ if (message.type === "error") {
2858
+ pending.reject(new Error(message.error));
2859
+ return;
2860
+ }
2861
+ const changedIds = message.changes.map(({ session }) => session.id);
2862
+ const replacedSessionIds = [...changedIds, ...message.removedSessionIds];
2863
+ const removedMetaIds = [...message.removedSessionIds, ...message.removedMetaIds];
2864
+ try {
2865
+ pending.resolve({
2866
+ sessions: applySessionChanges2(
2867
+ pending.payload.previousSessions,
2868
+ message.changes,
2869
+ message.removedSessionIds
2870
+ ),
2871
+ meta: applyMetaChanges(pending.payload.meta, message.meta, removedMetaIds),
2872
+ changedIds: pending.payload.sourceSync ? replacedSessionIds : void 0
2873
+ });
2874
+ } catch (error) {
2875
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
2876
+ }
2877
+ }
2878
+ closeWorker(agentName, slot, error) {
2879
+ if (slot.closed) return;
2880
+ slot.closed = true;
2881
+ if (this.workers.get(agentName) === slot) this.workers.delete(agentName);
2882
+ for (const pending of slot.pending.values()) pending.reject(error);
2883
+ slot.pending.clear();
2493
2884
  }
2494
2885
  };
2495
2886
 
2496
2887
  // src/live-scan.ts
2497
2888
  var NEW_SESSION_EVENT_WINDOW_MS = 250;
2498
- function mergeEvents(previous, next) {
2499
- const changedSessionHeads = /* @__PURE__ */ new Map();
2500
- const removedSessionRefs = /* @__PURE__ */ new Map();
2501
- const sessionKey = (agentName, sessionId) => `${agentName}\0${sessionId}`;
2502
- const addChanged = (item) => {
2503
- const key = sessionKey(item.agentName, item.session.id);
2504
- removedSessionRefs.delete(key);
2505
- changedSessionHeads.set(key, item);
2506
- };
2507
- const addRemoved = (item) => {
2508
- const key = sessionKey(item.agentName, item.sessionId);
2509
- changedSessionHeads.delete(key);
2510
- removedSessionRefs.set(key, item);
2511
- };
2512
- for (const item of previous.changedSessionHeads) addChanged(item);
2513
- for (const item of previous.removedSessionRefs) addRemoved(item);
2514
- for (const item of next.changedSessionHeads) addChanged(item);
2515
- for (const item of next.removedSessionRefs) addRemoved(item);
2516
- return {
2517
- type: "sessions-updated",
2518
- changedAgents: Array.from(/* @__PURE__ */ new Set([...previous.changedAgents, ...next.changedAgents])),
2519
- newSessions: previous.newSessions + next.newSessions,
2520
- updatedSessions: previous.updatedSessions + next.updatedSessions,
2521
- removedSessions: previous.removedSessions + next.removedSessions,
2522
- totalSessions: next.totalSessions,
2523
- timestamp: next.timestamp,
2524
- changedSessionHeads: [...changedSessionHeads.values()],
2525
- removedSessionRefs: [...removedSessionRefs.values()]
2526
- };
2527
- }
2528
2889
  var LiveScanStore = class {
2529
2890
  watchEnabled;
2530
2891
  scanOptions;
2531
2892
  startupScanOptions;
2532
2893
  deferInitialRefresh;
2533
2894
  syncEngine;
2534
- agents = [];
2535
- byAgent = {};
2536
- sessions = [];
2537
2895
  listeners = /* @__PURE__ */ new Set();
2538
2896
  watcher = null;
2539
2897
  pendingEvent = null;
@@ -2547,11 +2905,12 @@ var LiveScanStore = class {
2547
2905
  this.deferInitialRefresh = options.deferInitialRefresh === true;
2548
2906
  const workerRunner = options.workerRunner ?? new ThreadWorkerRunner(new URL("./scan-refresh-worker.js", import.meta.url));
2549
2907
  this.syncEngine = new AgentSyncEngine({
2550
- snapshot: () => this.getSnapshot(),
2551
2908
  startupScanOptions: this.startupScanOptions,
2552
2909
  workerRunner
2553
2910
  });
2554
- this.syncEngine.subscribeSessionsChanged((change) => this.applySessionsChanged(change));
2911
+ this.syncEngine.subscribeSessionsChanged((change) => {
2912
+ if (change.event) this.emit(change.event);
2913
+ });
2555
2914
  }
2556
2915
  async initialize() {
2557
2916
  const startedAt = performance.now();
@@ -2572,18 +2931,26 @@ var LiveScanStore = class {
2572
2931
  smartTagWorkerUrl: this.getSmartTagWorkerUrl() ?? void 0,
2573
2932
  includeSmartTags: this.deferInitialRefresh ? false : void 0
2574
2933
  });
2575
- this.applyScanResult(initialResult);
2576
- this.syncEngine.initialize(initialResult.cacheTimestamps);
2934
+ this.syncEngine.initialize(initialResult, {
2935
+ cacheTimestamps: initialResult.cacheTimestamps,
2936
+ registeredAgents: createRegisteredAgents(),
2937
+ allowedAgents: this.getAllowedAgents()
2938
+ });
2939
+ const snapshot = this.getSnapshot();
2577
2940
  const indexStartedAt = performance.now();
2578
- if (!this.deferInitialRefresh) await this.syncEngine.syncInitialIndex();
2941
+ if (!this.deferInitialRefresh) {
2942
+ await this.syncEngine.syncInitialIndex().catch((error) => {
2943
+ appLogger.error("scan.initial.index_failed", { error });
2944
+ });
2945
+ }
2579
2946
  const indexDuration = performance.now() - indexStartedAt;
2580
2947
  appLogger.info("scan.initial.done", {
2581
2948
  duration_ms: Math.round(performance.now() - startedAt),
2582
2949
  index_ms: this.deferInitialRefresh ? void 0 : Math.round(indexDuration),
2583
2950
  deferred: this.deferInitialRefresh || void 0,
2584
- sessions: this.sessions.length,
2951
+ sessions: snapshot.sessions.length,
2585
2952
  agents: Object.fromEntries(
2586
- Object.entries(this.byAgent).map(([key, value]) => [key, value.length])
2953
+ Object.entries(snapshot.byAgent).map(([key, value]) => [key, value.length])
2587
2954
  ),
2588
2955
  agent_timings: initialResult.timings ? Object.fromEntries(
2589
2956
  Object.entries(initialResult.timings).map(([name, timing]) => [
@@ -2602,13 +2969,13 @@ var LiveScanStore = class {
2602
2969
  if (!this.watchEnabled) return;
2603
2970
  this.watcher = new SessionWatcher();
2604
2971
  this.watcher.onAgentsChanged((agentNames) => this.syncEngine.handleAgentsChanged(agentNames));
2605
- this.watcher.start(this.agents.map((agent) => agent.name));
2972
+ this.watcher.start(snapshot.agents);
2606
2973
  }
2607
2974
  startBackgroundRefresh() {
2608
2975
  this.syncEngine.startBackgroundRefresh();
2609
2976
  }
2610
2977
  getSnapshot() {
2611
- return { sessions: this.sessions, byAgent: this.byAgent, agents: this.agents };
2978
+ return this.syncEngine.snapshot();
2612
2979
  }
2613
2980
  getScanStatus() {
2614
2981
  return this.syncEngine.status();
@@ -2637,13 +3004,6 @@ var LiveScanStore = class {
2637
3004
  this.watcher = null;
2638
3005
  }
2639
3006
  }
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
3007
  emit(event) {
2648
3008
  if (this.shuttingDown) return;
2649
3009
  if (this.pendingEvent || event.newSessions > 0) {
@@ -2656,7 +3016,7 @@ var LiveScanStore = class {
2656
3016
  for (const listener of this.listeners) listener(event);
2657
3017
  }
2658
3018
  queueEvent(event) {
2659
- this.pendingEvent = this.pendingEvent ? mergeEvents(this.pendingEvent, event) : event;
3019
+ this.pendingEvent = this.pendingEvent ? mergeSessionsUpdatedEvents(this.pendingEvent, event) : event;
2660
3020
  if (this.pendingEventTimer) return;
2661
3021
  this.pendingEventTimer = setTimeout(() => {
2662
3022
  const pending = this.pendingEvent;
@@ -2665,29 +3025,11 @@ var LiveScanStore = class {
2665
3025
  if (pending) this.emitNow(pending);
2666
3026
  }, NEW_SESSION_EVENT_WINDOW_MS);
2667
3027
  }
2668
- rebuildSessions() {
2669
- this.sessions = sortSessions(Object.values(this.byAgent).flat());
2670
- }
2671
3028
  getSmartTagWorkerUrl() {
2672
3029
  const workerUrl = new URL("./smart-tag-worker.js", import.meta.url);
2673
3030
  if (workerUrl.protocol === "file:" && !existsSync4(fileURLToPath3(workerUrl))) return null;
2674
3031
  return workerUrl;
2675
3032
  }
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
3033
  getAllowedAgents() {
2692
3034
  if (!this.scanOptions.agents?.length) return null;
2693
3035
  return new Set(this.scanOptions.agents.map((agent) => agent.toLowerCase()));
@@ -2698,11 +3040,11 @@ var LiveScanStore = class {
2698
3040
  import { consola } from "consola";
2699
3041
 
2700
3042
  // src/version.ts
2701
- import { readFileSync } from "fs";
3043
+ import { readFileSync as readFileSync2 } from "fs";
2702
3044
  import { resolve as resolve3, dirname as dirname3 } from "path";
2703
3045
  import { fileURLToPath as fileURLToPath4 } from "url";
2704
3046
  var __dirname = dirname3(fileURLToPath4(import.meta.url));
2705
- var pkg = JSON.parse(readFileSync(resolve3(__dirname, "../package.json"), "utf-8"));
3047
+ var pkg = JSON.parse(readFileSync2(resolve3(__dirname, "../package.json"), "utf-8"));
2706
3048
  var VERSION = pkg.version;
2707
3049
 
2708
3050
  // src/output.ts
@@ -2724,6 +3066,25 @@ function printScanResults(agents) {
2724
3066
  consola.log("");
2725
3067
  }
2726
3068
 
3069
+ // src/session-index-output.ts
3070
+ function buildSessionIndexOutput(snapshot, window = {}) {
3071
+ const sessions = snapshot.sessions.filter((session) => {
3072
+ const activity = session.time_updated ?? session.time_created;
3073
+ if (window.from != null && activity < window.from) return false;
3074
+ if (window.to != null && activity > window.to) return false;
3075
+ return true;
3076
+ });
3077
+ const agents = getAgentInfoMap(
3078
+ Object.fromEntries(Object.entries(snapshot.byAgent).map(([name, list]) => [name, list.length]))
3079
+ ).map(({ name, displayName, count }) => ({
3080
+ name,
3081
+ displayName,
3082
+ count,
3083
+ available: count > 0
3084
+ }));
3085
+ return { agents, sessions };
3086
+ }
3087
+
2727
3088
  // src/runtime-plan.ts
2728
3089
  function parseSessionUri(uri) {
2729
3090
  const match = uri.match(/^([a-z]+):\/\/(.+)$/i);
@@ -2749,7 +3110,10 @@ function buildCliRuntimePlan(input, environment) {
2749
3110
  function resolveStartupUrl(startupUrl, targetSession) {
2750
3111
  if (!targetSession) return startupUrl;
2751
3112
  const url = new URL(startupUrl);
2752
- url.pathname = `/${targetSession.agent.toLowerCase()}/${targetSession.sessionId}`;
3113
+ url.pathname = sessionRoutePath({
3114
+ agentName: targetSession.agent,
3115
+ sessionId: targetSession.sessionId
3116
+ });
2753
3117
  return url.toString();
2754
3118
  }
2755
3119
  function redactStartupUrl(startupUrl) {
@@ -2774,6 +3138,27 @@ function hasExplicitPortArg(argv) {
2774
3138
  });
2775
3139
  }
2776
3140
 
3141
+ // src/pricing-refresh.ts
3142
+ var REFRESH_TIMEOUT_MS = 1e4;
3143
+ function startPricingRefresh(timeoutMs = REFRESH_TIMEOUT_MS) {
3144
+ const controller = new AbortController();
3145
+ const completion = refreshPricingCache({ signal: controller.signal, timeoutMs }).catch(
3146
+ (error) => {
3147
+ appLogger.warn("pricing.refresh.error", { error });
3148
+ return false;
3149
+ }
3150
+ );
3151
+ return {
3152
+ publish() {
3153
+ if (publishPendingPricing()) appLogger.info("pricing.refresh.published", {});
3154
+ },
3155
+ async cancel() {
3156
+ controller.abort();
3157
+ await completion;
3158
+ }
3159
+ };
3160
+ }
3161
+
2777
3162
  // src/index.ts
2778
3163
  var main = defineCommand({
2779
3164
  meta: {
@@ -2798,6 +3183,19 @@ var main = defineCommand({
2798
3183
  description: "Allow authenticated access when binding to a non-loopback address",
2799
3184
  default: false
2800
3185
  },
3186
+ "tls-cert": {
3187
+ type: "string",
3188
+ description: "Path to a TLS certificate; serve remote access over HTTPS"
3189
+ },
3190
+ "tls-key": {
3191
+ type: "string",
3192
+ description: "Path to the TLS private key matching --tls-cert"
3193
+ },
3194
+ "trust-proxy": {
3195
+ type: "boolean",
3196
+ description: "A reverse proxy in front of CodeSesh terminates TLS",
3197
+ default: false
3198
+ },
2801
3199
  agent: {
2802
3200
  type: "string",
2803
3201
  alias: "a",
@@ -2870,6 +3268,18 @@ var main = defineCommand({
2870
3268
  );
2871
3269
  process.exit(1);
2872
3270
  }
3271
+ let transport;
3272
+ try {
3273
+ transport = resolveRemoteTransport({
3274
+ hostname,
3275
+ tlsCertPath: args["tls-cert"],
3276
+ tlsKeyPath: args["tls-key"],
3277
+ trustProxy: args["trust-proxy"]
3278
+ });
3279
+ } catch (error) {
3280
+ console.error(error instanceof Error ? error.message : String(error));
3281
+ process.exit(1);
3282
+ }
2873
3283
  if (trace) {
2874
3284
  perf.enable();
2875
3285
  }
@@ -2883,12 +3293,12 @@ var main = defineCommand({
2883
3293
  log_path: appLogger.getLogPath()
2884
3294
  });
2885
3295
  if (clearCache) {
2886
- const { clearCache: clear } = await import("./dist-AIDCOQZO.js");
3296
+ const { clearCache: clear } = await import("./dist-DMGLFBW6.js");
2887
3297
  clear();
2888
3298
  appLogger.info("cache.clear");
2889
3299
  console.log("Cache cleared.");
2890
3300
  }
2891
- void refreshPricingCache();
3301
+ const pricingRefresh = startPricingRefresh();
2892
3302
  let targetSession = null;
2893
3303
  if (args.session) {
2894
3304
  targetSession = parseSessionUri(args.session);
@@ -2932,26 +3342,10 @@ var main = defineCommand({
2932
3342
  console.log(perf.getReport());
2933
3343
  }
2934
3344
  if (jsonOnly) {
2935
- const windowed = result.sessions.filter((s) => {
2936
- const activity = s.time_updated ?? s.time_created;
2937
- if (listDefaultFrom != null && activity < listDefaultFrom) return false;
2938
- if (listDefaultTo != null && activity > listDefaultTo) return false;
2939
- return true;
2940
- });
2941
- const info = getAgentInfoMap(
2942
- Object.fromEntries(Object.entries(result.byAgent).map(([k, v]) => [k, v.length]))
2943
- );
2944
- const output = {
2945
- agents: info.map(({ name, displayName, count }) => ({
2946
- name,
2947
- displayName,
2948
- count,
2949
- available: count > 0
2950
- })),
2951
- sessions: windowed
2952
- };
3345
+ await pricingRefresh.cancel();
3346
+ const output = buildSessionIndexOutput(result, { from: listDefaultFrom, to: listDefaultTo });
2953
3347
  appLogger.info("cli.json_output", {
2954
- sessions: windowed.length,
3348
+ sessions: output.sessions.length,
2955
3349
  duration_ms: Math.round(performance.now() - startedAt)
2956
3350
  });
2957
3351
  console.log(JSON.stringify(output, null, 2));
@@ -2967,7 +3361,8 @@ var main = defineCommand({
2967
3361
  defaultSessionDays: listDefaultDays,
2968
3362
  portFallbackAttempts: explicitPort ? 1 : DEFAULT_PORT_FALLBACK_ATTEMPTS,
2969
3363
  hostname,
2970
- remoteAccess
3364
+ remoteAccess,
3365
+ transport
2971
3366
  });
2972
3367
  } catch (error) {
2973
3368
  console.error(getServerStartupErrorMessage(error, port));
@@ -2975,6 +3370,7 @@ var main = defineCommand({
2975
3370
  }
2976
3371
  const { url } = app;
2977
3372
  if (!jsonOnly) {
3373
+ pricingRefresh.publish();
2978
3374
  store.startBackgroundRefresh();
2979
3375
  }
2980
3376
  let shuttingDown = false;
@@ -2982,6 +3378,7 @@ var main = defineCommand({
2982
3378
  if (shuttingDown) return;
2983
3379
  shuttingDown = true;
2984
3380
  appLogger.info("cli.shutdown", { signal });
3381
+ await pricingRefresh.cancel();
2985
3382
  await app.shutdown();
2986
3383
  process.exit(0);
2987
3384
  };