codesesh 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,26 +1,28 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ appLogger,
4
+ logSearchIndexSync
5
+ } from "./chunk-A4U2SMJJ.js";
2
6
  import {
3
7
  FileSystemSessionSource,
8
+ SessionAliasValidationError,
4
9
  StateStorageUnavailableError,
5
10
  attachMissingProjectIdentities,
11
+ attachProjectMetrics,
6
12
  buildAgentCacheMeta,
7
13
  buildDashboard,
8
- classifySessionTags,
9
- computeIdentity,
10
14
  computeSessionDiff,
11
15
  createProjectScopeMatcher,
12
16
  createRegisteredAgents,
13
17
  deleteBookmark,
14
18
  deleteSessionAlias,
15
19
  executeSessionSearch,
16
- extractSessionFileActivity,
20
+ filterSessionSearchCandidates,
21
+ formatSessionReference,
17
22
  getAgentInfoMap,
18
23
  getAgentLastFullSyncAt,
19
- getCursorDataPath,
20
24
  getSessionActivityTime,
21
- getSessionAgentName,
22
- getSmartTagSourceTimestamp,
23
- getTotalTokens,
25
+ getSessionAgentKey,
24
26
  importBookmarks,
25
27
  isAgentCacheInitialized,
26
28
  isProjectIdentityKind,
@@ -28,24 +30,23 @@ import {
28
30
  listCachedProjectGroups,
29
31
  listFileActivity,
30
32
  listSessionAliases,
31
- listSessionFileActivity,
32
- loadCachedSessionData,
33
33
  loadCachedSessions,
34
34
  markAgentFullSyncCompleted,
35
35
  matchesProjectIdentity,
36
36
  matchesProjectScope,
37
+ materializeSessionDetailResponse,
37
38
  mergeSearchQueryOptions,
39
+ mergeSortedSessions,
40
+ normalizeSessionReference,
38
41
  perf,
39
- realFs,
40
42
  refreshPricingCache,
41
- resolveProviderRoots,
42
43
  scanSessions,
43
44
  sessionSignature,
44
45
  sortSessions,
45
46
  startOfLocalDay,
46
47
  upsertBookmark,
47
48
  upsertSessionAlias
48
- } from "./chunk-NBCLV4CX.js";
49
+ } from "./chunk-7APNDHQ6.js";
49
50
 
50
51
  // src/index.ts
51
52
  import { defineCommand, runMain } from "citty";
@@ -53,164 +54,16 @@ import { defineCommand, runMain } from "citty";
53
54
  // src/server.ts
54
55
  import { Hono as Hono2 } from "hono";
55
56
  import { bodyLimit } from "hono/body-limit";
57
+ import { compress } from "hono/compress";
56
58
  import { serve } from "@hono/node-server";
57
59
  import { serveStatic } from "@hono/node-server/serve-static";
58
- import { existsSync as existsSync2 } from "fs";
60
+ import { existsSync } from "fs";
59
61
  import { resolve, dirname } from "path";
60
62
  import { fileURLToPath } from "url";
61
63
 
62
64
  // src/api/routes.ts
63
65
  import { Hono } from "hono";
64
66
 
65
- // src/logging.ts
66
- import {
67
- appendFileSync,
68
- existsSync,
69
- mkdirSync,
70
- readdirSync,
71
- renameSync,
72
- statSync,
73
- unlinkSync
74
- } from "fs";
75
- import { homedir } from "os";
76
- import { join } from "path";
77
- var LEVEL_WEIGHT = {
78
- debug: 10,
79
- info: 20,
80
- warn: 30,
81
- error: 40
82
- };
83
- function parseLevel(value) {
84
- if (value === "debug" || value === "info" || value === "warn" || value === "error") {
85
- return value;
86
- }
87
- return "info";
88
- }
89
- function parsePositiveInt(value, fallback) {
90
- const parsed = Number(value);
91
- return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
92
- }
93
- function getDefaultLogDir() {
94
- const base = process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
95
- return join(base, "codesesh", "logs");
96
- }
97
- function toLogValue(value, depth = 0) {
98
- if (value == null || typeof value === "string" || typeof value === "number") return value;
99
- if (typeof value === "boolean") return value;
100
- if (typeof value === "bigint") return value.toString();
101
- if (value instanceof Error) {
102
- return {
103
- name: value.name,
104
- message: value.message,
105
- stack: value.stack
106
- };
107
- }
108
- if (depth >= 4) return "[truncated]";
109
- if (Array.isArray(value)) return value.slice(0, 50).map((item) => toLogValue(item, depth + 1));
110
- if (typeof value === "object") {
111
- return Object.fromEntries(
112
- Object.entries(value).map(([key, item]) => [
113
- key,
114
- toLogValue(item, depth + 1)
115
- ])
116
- );
117
- }
118
- return String(value);
119
- }
120
- function timestampForFile(date = /* @__PURE__ */ new Date()) {
121
- return date.toISOString().replace(/[:.]/g, "-");
122
- }
123
- var AppLogger = class {
124
- logDir;
125
- level;
126
- maxBytes;
127
- maxFiles;
128
- currentPath;
129
- rotationIndex = 0;
130
- constructor(options = {}) {
131
- this.logDir = options.logDir ?? process.env.CODESESH_LOG_DIR ?? getDefaultLogDir();
132
- this.level = options.level ?? parseLevel(process.env.CODESESH_LOG_LEVEL);
133
- this.maxBytes = options.maxBytes ?? parsePositiveInt(process.env.CODESESH_LOG_MAX_BYTES, 5e6);
134
- this.maxFiles = options.maxFiles ?? parsePositiveInt(process.env.CODESESH_LOG_MAX_FILES, 5);
135
- this.currentPath = join(this.logDir, "codesesh.log");
136
- }
137
- getLogPath() {
138
- return this.currentPath;
139
- }
140
- debug(event, data = {}) {
141
- this.write("debug", event, data);
142
- }
143
- info(event, data = {}) {
144
- this.write("info", event, data);
145
- }
146
- warn(event, data = {}) {
147
- this.write("warn", event, data);
148
- }
149
- error(event, data = {}) {
150
- this.write("error", event, data);
151
- }
152
- write(level, event, data) {
153
- if (LEVEL_WEIGHT[level] < LEVEL_WEIGHT[this.level]) return;
154
- try {
155
- mkdirSync(this.logDir, { recursive: true });
156
- const line = `${JSON.stringify({
157
- ts: (/* @__PURE__ */ new Date()).toISOString(),
158
- level,
159
- event,
160
- pid: process.pid,
161
- ...toLogValue(data)
162
- })}
163
- `;
164
- this.rotateIfNeeded(Buffer.byteLength(line));
165
- appendFileSync(this.currentPath, line, "utf8");
166
- } catch {
167
- }
168
- }
169
- rotateIfNeeded(nextBytes) {
170
- if (!existsSync(this.currentPath)) {
171
- this.removeExpiredLogs();
172
- return;
173
- }
174
- const currentSize = statSync(this.currentPath).size;
175
- if (currentSize + nextBytes <= this.maxBytes) return;
176
- this.rotationIndex += 1;
177
- const rotatedPath = join(
178
- this.logDir,
179
- `codesesh-${timestampForFile()}-${process.pid}-${this.rotationIndex}.log`
180
- );
181
- renameSync(this.currentPath, rotatedPath);
182
- this.removeExpiredLogs();
183
- }
184
- removeExpiredLogs() {
185
- const rotated = readdirSync(this.logDir).filter((name) => /^codesesh-.+\.log$/.test(name)).map((name) => {
186
- const path = join(this.logDir, name);
187
- return { path, mtimeMs: statSync(path).mtimeMs };
188
- }).toSorted((a, b) => b.mtimeMs - a.mtimeMs);
189
- for (const item of rotated.slice(Math.max(0, this.maxFiles - 1))) {
190
- unlinkSync(item.path);
191
- }
192
- }
193
- };
194
- var appLogger = new AppLogger();
195
- function logSearchIndexSync(context, result, data = {}) {
196
- if (!result || result.mode !== "bulk" || result.rebuildDurationMs == null) {
197
- return;
198
- }
199
- appLogger.info("search_index.sync", {
200
- context,
201
- agent: result.agentName,
202
- mode: result.mode,
203
- sessions: result.sessions,
204
- changed: result.changed,
205
- deleted: result.deleted,
206
- indexed: result.indexed,
207
- skipped: result.skipped,
208
- duration_ms: Math.round(result.durationMs),
209
- rebuild_duration_ms: Math.round(result.rebuildDurationMs),
210
- ...data
211
- });
212
- }
213
-
214
67
  // src/time-window-resolution.ts
215
68
  var DAY_MS = 24 * 60 * 60 * 1e3;
216
69
  var DEFAULT_DASHBOARD_DAYS = 30;
@@ -274,90 +127,31 @@ function elapsedDays(from, to) {
274
127
  return Math.max(1, Math.ceil((to - from) / DAY_MS));
275
128
  }
276
129
 
277
- // src/api/handlers.ts
278
- function getSessionAliasKey(agentKey, sessionId) {
279
- return `${agentKey.toLowerCase()}\0${sessionId}`;
280
- }
281
- function getSessionAgentKey(session) {
282
- return session.slug.split("/")[0]?.toLowerCase() ?? "";
283
- }
284
- function loadSessionAliasMap() {
285
- try {
286
- return new Map(
287
- listSessionAliases().map((alias) => [
288
- getSessionAliasKey(alias.agentKey, alias.sessionId),
289
- alias.alias
290
- ])
291
- );
292
- } catch (error) {
293
- if (!(error instanceof StateStorageUnavailableError)) {
294
- appLogger.warn("api.session_aliases.load_failed", {
295
- error: error instanceof Error ? error.message : String(error)
296
- });
297
- }
298
- return /* @__PURE__ */ new Map();
299
- }
300
- }
301
- function isStateStorageUnavailable(error) {
302
- return error instanceof StateStorageUnavailableError;
303
- }
304
- function withDisplayTitle(session, agentKey, aliases) {
305
- const alias = aliases.get(getSessionAliasKey(agentKey, session.id));
306
- return alias ? { ...session, display_title: alias } : session;
307
- }
308
- function withBookmarkDisplayTitle(bookmark, aliases) {
309
- const alias = aliases.get(getSessionAliasKey(bookmark.agentKey, bookmark.sessionId));
310
- return alias ? { ...bookmark, display_title: alias } : bookmark;
311
- }
312
- function withFileActivityDisplayTitle(activity, aliases) {
313
- return {
314
- ...activity,
315
- session: withDisplayTitle(activity.session, activity.agent_name, aliases)
316
- };
317
- }
318
- function findAliasSearchResults(query, options, scanResult, aliases) {
319
- const search = mergeSearchQueryOptions(query, options);
320
- const needle = search.text.trim().toLowerCase();
321
- if (!needle || aliases.size === 0) return [];
322
- return executeSessionSearch(
323
- "",
324
- { ...search.options, limit: Math.max(scanResult.sessions.length, 1) },
325
- scanResult
326
- ).flatMap((result) => {
327
- const alias = aliases.get(getSessionAliasKey(result.agentName, result.session.id));
328
- if (!alias || !alias.toLowerCase().includes(needle)) return [];
329
- return [
330
- {
331
- agentName: result.agentName,
332
- session: withDisplayTitle(result.session, result.agentName, aliases),
333
- snippet: `Alias \xB7 ${result.session.directory}`,
334
- matchType: "title"
335
- }
336
- ];
337
- });
338
- }
339
- function isRecord(value) {
340
- return typeof value === "object" && value !== null;
130
+ // src/api/query-params.ts
131
+ var SMART_TAGS = [
132
+ "bugfix",
133
+ "refactoring",
134
+ "feature-dev",
135
+ "testing",
136
+ "docs",
137
+ "git-ops",
138
+ "build-deploy",
139
+ "exploration",
140
+ "planning"
141
+ ];
142
+ var SEARCH_LIMIT_MAX = 100;
143
+ var SEARCH_LIMIT_DEFAULT = 50;
144
+ function searchParams(c) {
145
+ return new URL(c.req.url ?? "http://localhost/", "http://localhost/").searchParams;
341
146
  }
342
- function isSessionStats(value) {
343
- if (!isRecord(value)) return false;
344
- 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");
147
+ function queryValues(params, ...names) {
148
+ return names.flatMap(
149
+ (name) => params.getAll(name).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean)
150
+ );
345
151
  }
346
- function parseBookmarkPayload(value) {
347
- if (!isRecord(value)) return null;
348
- 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)) {
349
- return null;
350
- }
351
- return {
352
- agentKey: value.agentKey,
353
- sessionId: value.sessionId,
354
- fullPath: value.fullPath,
355
- title: value.title,
356
- directory: value.directory,
357
- time_created: value.time_created,
358
- time_updated: value.time_updated ?? void 0,
359
- stats: value.stats
360
- };
152
+ function optionalQueryValue(value) {
153
+ const normalized = value?.trim();
154
+ return normalized ? normalized : void 0;
361
155
  }
362
156
  function parseDateParam(value, fallback) {
363
157
  if (value == null) return fallback;
@@ -369,30 +163,23 @@ function parseNumberParam(value) {
369
163
  const number = Number(value);
370
164
  return Number.isFinite(number) ? number : void 0;
371
165
  }
372
- function searchParams(c) {
373
- return new URL(c.req.url ?? "http://localhost/", "http://localhost/").searchParams;
374
- }
375
- function queryValues(params, ...names) {
376
- return names.flatMap(
377
- (name) => params.getAll(name).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean)
378
- );
379
- }
380
166
  function parseSmartTags(values) {
381
- const tags = values.map((value) => value.toLowerCase()).filter(
382
- (value) => [
383
- "bugfix",
384
- "refactoring",
385
- "feature-dev",
386
- "testing",
387
- "docs",
388
- "git-ops",
389
- "build-deploy",
390
- "exploration",
391
- "planning"
392
- ].includes(value)
393
- );
167
+ const tags = values.map((value) => value.toLowerCase()).filter((value) => SMART_TAGS.includes(value));
394
168
  return tags.length > 0 ? [...new Set(tags)] : void 0;
395
169
  }
170
+ function parseFileActivityKind(value) {
171
+ if (value === "read" || value === "edit" || value === "write" || value === "delete") {
172
+ return value;
173
+ }
174
+ return void 0;
175
+ }
176
+ function parseProjectIdentityFilter(kindValue, keyValue) {
177
+ const kind = optionalQueryValue(kindValue);
178
+ const key = optionalQueryValue(keyValue);
179
+ if (!kind && !key) return void 0;
180
+ if (!kind || !key || !isProjectIdentityKind(kind)) return null;
181
+ return { kind, key };
182
+ }
396
183
  function parseSearchOptions(c, defaults, projectIdentity) {
397
184
  const params = searchParams(c);
398
185
  const limitValue = parseNumberParam(params.get("limit") ?? void 0);
@@ -412,7 +199,7 @@ function parseSearchOptions(c, defaults, projectIdentity) {
412
199
  costMax: parseNumberParam(params.get("costMax") ?? void 0),
413
200
  from: parseDateParam(params.get("from") ?? void 0, defaults.from),
414
201
  to: parseDateParam(params.get("to") ?? void 0, defaults.to),
415
- limit: limitValue && limitValue > 0 ? Math.min(limitValue, 100) : 50
202
+ limit: limitValue && limitValue > 0 ? Math.min(limitValue, SEARCH_LIMIT_MAX) : SEARCH_LIMIT_DEFAULT
416
203
  };
417
204
  }
418
205
  function filterSessionsByActivityWindow(sessions, from, to) {
@@ -424,6 +211,169 @@ function filterSessionsByActivityWindow(sessions, from, to) {
424
211
  return true;
425
212
  });
426
213
  }
214
+
215
+ // src/api/session-aliases-view.ts
216
+ function aliasKey(reference) {
217
+ return `${reference.agentName.toLowerCase()}\0${reference.sessionId}`;
218
+ }
219
+ function loadAliasMap() {
220
+ try {
221
+ return new Map(listSessionAliases().map((alias) => [aliasKey(alias.reference), alias]));
222
+ } catch (error) {
223
+ if (!(error instanceof StateStorageUnavailableError)) {
224
+ appLogger.warn("api.session_aliases.load_failed", {
225
+ error: error instanceof Error ? error.message : String(error)
226
+ });
227
+ }
228
+ return /* @__PURE__ */ new Map();
229
+ }
230
+ }
231
+ function buildAliasView() {
232
+ const aliases = loadAliasMap();
233
+ return {
234
+ size: aliases.size,
235
+ get: (reference) => aliases.get(aliasKey(reference))?.alias,
236
+ decorate(record, reference) {
237
+ const alias = aliases.get(aliasKey(reference))?.alias;
238
+ return alias ? { ...record, display_title: alias } : record;
239
+ },
240
+ entries: () => aliases.values()
241
+ };
242
+ }
243
+ var cachedView = null;
244
+ function loadAliasView() {
245
+ return cachedView ??= buildAliasView();
246
+ }
247
+ function invalidateAliasView() {
248
+ cachedView = null;
249
+ }
250
+ function decorateBookmark(bookmark, aliases) {
251
+ return {
252
+ ...bookmark,
253
+ session: aliases.decorate(bookmark.session, bookmark.reference)
254
+ };
255
+ }
256
+ function decorateFileActivity(activity, aliases) {
257
+ return {
258
+ ...activity,
259
+ session: aliases.decorate(activity.session, activity.reference)
260
+ };
261
+ }
262
+ function findAliasSearchResults(query, options, scanResult, aliases) {
263
+ const search = mergeSearchQueryOptions(query, options);
264
+ const needle = search.text.trim().toLowerCase();
265
+ if (!needle || aliases.size === 0) return [];
266
+ const sessionsByAgent = /* @__PURE__ */ new Map();
267
+ const lookupSession = (agentName, sessionId) => {
268
+ let byId = sessionsByAgent.get(agentName);
269
+ if (!byId) {
270
+ byId = new Map((scanResult.byAgent[agentName] ?? []).map((item) => [item.id, item]));
271
+ sessionsByAgent.set(agentName, byId);
272
+ }
273
+ return byId.get(sessionId);
274
+ };
275
+ const results = [];
276
+ for (const alias of aliases.entries()) {
277
+ if (!alias.alias.toLowerCase().includes(needle)) continue;
278
+ const { agentName, sessionId } = alias.reference;
279
+ const session = lookupSession(agentName, sessionId);
280
+ if (!session) continue;
281
+ results.push({
282
+ reference: alias.reference,
283
+ session: aliases.decorate(session, alias.reference),
284
+ snippet: `Alias \xB7 ${session.directory}`,
285
+ matchType: "title"
286
+ });
287
+ }
288
+ return filterSessionSearchCandidates(results, search.options).sort(
289
+ (a, b) => getSessionActivityTime(b.session) - getSessionActivityTime(a.session)
290
+ );
291
+ }
292
+
293
+ // src/api/handlers.ts
294
+ var SNAPSHOT_AGGREGATION_CACHE_LIMIT = 64;
295
+ var snapshotAggregationCaches = /* @__PURE__ */ new WeakMap();
296
+ function getSnapshotAggregation(source, sessions, key, build) {
297
+ let cache = snapshotAggregationCaches.get(source);
298
+ if (!cache || cache.sessions !== sessions) {
299
+ cache = { sessions, values: /* @__PURE__ */ new Map() };
300
+ snapshotAggregationCaches.set(source, cache);
301
+ }
302
+ const cacheKey = JSON.stringify(key);
303
+ if (cache.values.has(cacheKey)) return cache.values.get(cacheKey);
304
+ const value = build();
305
+ if (cache.values.size >= SNAPSHOT_AGGREGATION_CACHE_LIMIT) {
306
+ const oldestKey = cache.values.keys().next().value;
307
+ if (oldestKey != null) cache.values.delete(oldestKey);
308
+ }
309
+ cache.values.set(cacheKey, value);
310
+ return value;
311
+ }
312
+ function withStorageErrors(handler, onUnavailable) {
313
+ try {
314
+ return handler();
315
+ } catch (error) {
316
+ if (error instanceof StateStorageUnavailableError) {
317
+ return onUnavailable();
318
+ }
319
+ throw error;
320
+ }
321
+ }
322
+ function isRecord(value) {
323
+ return typeof value === "object" && value !== null;
324
+ }
325
+ function isSessionStats(value) {
326
+ if (!isRecord(value)) return false;
327
+ return typeof value.message_count === "number" && typeof value.total_input_tokens === "number" && typeof value.total_output_tokens === "number" && typeof value.total_cost === "number" && (value.total_tokens == null || typeof value.total_tokens === "number");
328
+ }
329
+ function parseBookmarkSession(value, reference) {
330
+ if (!isRecord(value)) return null;
331
+ if (value.id !== reference.sessionId || typeof value.slug !== "string" || typeof value.title !== "string" || typeof value.directory !== "string" || typeof value.time_created !== "number" || value.time_updated != null && typeof value.time_updated !== "number" || !isSessionStats(value.stats)) {
332
+ return null;
333
+ }
334
+ return {
335
+ id: reference.sessionId,
336
+ slug: formatSessionReference(reference),
337
+ title: value.title,
338
+ directory: value.directory,
339
+ time_created: value.time_created,
340
+ time_updated: value.time_updated ?? void 0,
341
+ stats: value.stats
342
+ };
343
+ }
344
+ function parseSessionReferencePayload(value) {
345
+ if (!isRecord(value) || typeof value.agentName !== "string" || typeof value.sessionId !== "string" || !value.agentName.trim() || !value.sessionId) {
346
+ return null;
347
+ }
348
+ return normalizeSessionReference({
349
+ agentName: value.agentName.trim().toLowerCase(),
350
+ sessionId: value.sessionId
351
+ });
352
+ }
353
+ function parseBookmarkPayload(value) {
354
+ if (!isRecord(value)) return null;
355
+ const reference = parseSessionReferencePayload(value.reference);
356
+ if (reference) {
357
+ const session2 = parseBookmarkSession(value.session, reference);
358
+ return session2 ? { reference, session: session2 } : null;
359
+ }
360
+ if (typeof value.agentKey !== "string" || typeof value.sessionId !== "string" || typeof value.fullPath !== "string" || !value.agentKey.trim() || !value.sessionId) {
361
+ return null;
362
+ }
363
+ const legacyReference = normalizeSessionReference({
364
+ agentName: value.agentKey.trim().toLowerCase(),
365
+ sessionId: value.sessionId
366
+ });
367
+ const session = parseBookmarkSession(
368
+ {
369
+ ...value,
370
+ id: value.sessionId,
371
+ slug: value.fullPath
372
+ },
373
+ legacyReference
374
+ );
375
+ return session ? { reference: legacyReference, session } : null;
376
+ }
427
377
  function sanitizeClientLogData(value) {
428
378
  if (!isRecord(value)) return {};
429
379
  return Object.fromEntries(
@@ -436,60 +386,47 @@ function sanitizeClientLogData(value) {
436
386
  })
437
387
  );
438
388
  }
439
- function getProjectGroupKey(identityKind, identityKey) {
440
- return `${identityKind}:${identityKey}`;
441
- }
442
- function attachProjectMetrics(projects, sessions) {
443
- const metrics = /* @__PURE__ */ new Map();
444
- for (const session of sessions) {
445
- const identity = session.project_identity;
446
- if (!identity) continue;
447
- const key = getProjectGroupKey(identity.kind, identity.key);
448
- let current = metrics.get(key);
449
- if (!current) {
450
- current = {
451
- messages: 0,
452
- tokens: 0,
453
- cost: 0,
454
- hasEstimatedCost: false,
455
- agentStats: /* @__PURE__ */ new Map()
456
- };
457
- metrics.set(key, current);
458
- }
459
- const tokens = getTotalTokens(session.stats);
460
- const cost = session.stats.total_cost ?? 0;
461
- current.messages += session.stats.message_count;
462
- current.tokens += tokens;
463
- current.cost += cost;
464
- if (session.stats.cost_source === "estimated") current.hasEstimatedCost = true;
465
- const agentName = getSessionAgentName(session);
466
- const agent = current.agentStats.get(agentName);
467
- if (agent) {
468
- agent.sessions += 1;
469
- agent.messages += session.stats.message_count;
470
- agent.tokens += tokens;
471
- agent.cost += cost;
472
- } else {
473
- current.agentStats.set(agentName, {
474
- name: agentName,
475
- sessions: 1,
476
- messages: session.stats.message_count,
477
- tokens,
478
- cost
479
- });
480
- }
481
- }
482
- return projects.map((project) => {
483
- const metric = metrics.get(getProjectGroupKey(project.identityKind, project.identityKey));
484
- return {
485
- ...project,
486
- messages: metric?.messages ?? 0,
487
- tokens: metric?.tokens ?? 0,
488
- cost: metric?.cost ?? 0,
489
- cost_source: metric && metric.cost > 0 ? metric.hasEstimatedCost ? "estimated" : "recorded" : void 0,
490
- agentStats: [...metric?.agentStats.values() ?? []].sort((a, b) => b.sessions - a.sessions)
491
- };
492
- });
389
+ function toSessionListItem(session) {
390
+ if (!session.model_usage) return session;
391
+ const item = { ...session };
392
+ delete item.model_usage;
393
+ return item;
394
+ }
395
+ function getSessionHeadReference(session) {
396
+ return {
397
+ agentName: getSessionAgentKey(session),
398
+ sessionId: session.id
399
+ };
400
+ }
401
+ function createSessionDetailJsonResponse(data, messages) {
402
+ const encoder = new TextEncoder();
403
+ const headerJson = JSON.stringify(data);
404
+ const iterator = messages[Symbol.iterator]();
405
+ let wroteHeader = false;
406
+ let wroteMessage = false;
407
+ return new Response(
408
+ new ReadableStream({
409
+ pull(controller) {
410
+ if (!wroteHeader) {
411
+ controller.enqueue(encoder.encode(`${headerJson.slice(0, -1)},"messages":[`));
412
+ wroteHeader = true;
413
+ return;
414
+ }
415
+ const next = iterator.next();
416
+ if (!next.done) {
417
+ controller.enqueue(encoder.encode(`${wroteMessage ? "," : ""}${next.value}`));
418
+ wroteMessage = true;
419
+ return;
420
+ }
421
+ controller.enqueue(encoder.encode("]}"));
422
+ controller.close();
423
+ },
424
+ cancel() {
425
+ iterator.return?.();
426
+ }
427
+ }),
428
+ { headers: { "Content-Type": "application/json; charset=UTF-8" } }
429
+ );
493
430
  }
494
431
  function handleGetConfig(c, defaults) {
495
432
  const payload = {
@@ -508,22 +445,38 @@ function handleGetAgents(c, scanSource, defaults = {}) {
508
445
  const scanResult = scanSource.getSnapshot();
509
446
  const from = parseDateParam(c.req.query("from"), defaults.from);
510
447
  const to = parseDateParam(c.req.query("to"), defaults.to);
511
- const counts = Object.fromEntries(
512
- Object.entries(scanResult.byAgent).map(([agentName, sessions]) => [
513
- agentName,
514
- filterSessionsByActivityWindow(sessions, from, to).length
515
- ])
448
+ const agents = getSnapshotAggregation(
449
+ scanSource,
450
+ scanResult.sessions,
451
+ ["agents", from, to],
452
+ () => {
453
+ const counts = Object.fromEntries(
454
+ Object.entries(scanResult.byAgent).map(([agentName, sessions]) => [
455
+ agentName,
456
+ filterSessionsByActivityWindow(sessions, from, to).length
457
+ ])
458
+ );
459
+ return getAgentInfoMap(counts);
460
+ }
516
461
  );
517
- return c.json(getAgentInfoMap(counts));
462
+ return c.json(agents);
518
463
  }
519
464
  function handleGetProjects(c, scanSource, defaults = {}) {
520
465
  const scanResult = scanSource.getSnapshot();
521
466
  const from = parseDateParam(c.req.query("from"), defaults.from);
522
467
  const to = parseDateParam(c.req.query("to"), defaults.to);
523
- const sessions = filterSessionsByActivityWindow(scanResult.sessions, from, to);
524
- return c.json({
525
- projects: attachProjectMetrics(listCachedProjectGroups(sessions), sessions)
526
- });
468
+ const projects = getSnapshotAggregation(
469
+ scanSource,
470
+ scanResult.sessions,
471
+ ["projects", from, to],
472
+ () => {
473
+ const sessions = filterSessionsByActivityWindow(scanResult.sessions, from, to);
474
+ return {
475
+ projects: attachProjectMetrics(listCachedProjectGroups(sessions), sessions)
476
+ };
477
+ }
478
+ );
479
+ return c.json(projects);
527
480
  }
528
481
  function handleGetSessions(c, scanSource, defaults = {}) {
529
482
  const scanResult = scanSource.getSnapshot();
@@ -558,16 +511,16 @@ function handleGetSessions(c, scanSource, defaults = {}) {
558
511
  if (tag) {
559
512
  sessions = sessions.filter((s) => s.smart_tags?.includes(tag));
560
513
  }
561
- const aliases = loadSessionAliasMap();
514
+ const aliases = loadAliasView();
562
515
  if (q) {
563
516
  sessions = sessions.filter((session) => {
564
- const alias = aliases.get(getSessionAliasKey(getSessionAgentKey(session), session.id));
517
+ const alias = aliases.get(getSessionHeadReference(session));
565
518
  return session.title.toLowerCase().includes(q) || alias?.toLowerCase().includes(q);
566
519
  });
567
520
  }
568
521
  return c.json({
569
522
  sessions: sessions.map(
570
- (session) => withDisplayTitle(session, getSessionAgentKey(session), aliases)
523
+ (session) => toSessionListItem(aliases.decorate(session, getSessionHeadReference(session)))
571
524
  )
572
525
  });
573
526
  }
@@ -582,35 +535,18 @@ function handleSearchSessions(c, scanSource, defaults = {}) {
582
535
  return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
583
536
  }
584
537
  const searchOptions = parseSearchOptions(c, defaults, projectIdentity);
585
- const aliases = loadSessionAliasMap();
538
+ const aliases = loadAliasView();
586
539
  const results = executeSessionSearch(query, searchOptions, scanResult).map((result) => ({
587
540
  ...result,
588
- session: withDisplayTitle(result.session, result.agentName, aliases)
541
+ session: aliases.decorate(result.session, result.reference)
589
542
  }));
590
543
  const aliasResults = findAliasSearchResults(query, searchOptions, scanResult, aliases);
591
544
  const deduped = /* @__PURE__ */ new Map();
592
545
  for (const result of [...aliasResults, ...results]) {
593
- deduped.set(`${result.agentName}\0${result.session.id}`, result);
546
+ deduped.set(`${result.reference.agentName}\0${result.reference.sessionId}`, result);
594
547
  }
595
548
  return c.json({ results: [...deduped.values()].slice(0, searchOptions.limit ?? 50) });
596
549
  }
597
- function parseFileActivityKind(value) {
598
- if (value === "read" || value === "edit" || value === "write" || value === "delete") {
599
- return value;
600
- }
601
- return void 0;
602
- }
603
- function optionalQueryValue(value) {
604
- const normalized = value?.trim();
605
- return normalized ? normalized : void 0;
606
- }
607
- function parseProjectIdentityFilter(kindValue, keyValue) {
608
- const kind = optionalQueryValue(kindValue);
609
- const key = optionalQueryValue(keyValue);
610
- if (!kind && !key) return void 0;
611
- if (!kind || !key || !isProjectIdentityKind(kind)) return null;
612
- return { kind, key };
613
- }
614
550
  function handleGetFileActivity(c, defaults = {}) {
615
551
  const limitValue = Number(c.req.query("limit"));
616
552
  const limit = Number.isFinite(limitValue) && limitValue > 0 ? Math.min(limitValue, 200) : 50;
@@ -621,7 +557,7 @@ function handleGetFileActivity(c, defaults = {}) {
621
557
  if (projectIdentity === null) {
622
558
  return c.json({ error: "projectKind and projectKey must form a valid project identity" }, 400);
623
559
  }
624
- const aliases = loadSessionAliasMap();
560
+ const aliases = loadAliasView();
625
561
  return c.json({
626
562
  activity: listFileActivity({
627
563
  agent: optionalQueryValue(c.req.query("agent")),
@@ -635,12 +571,11 @@ function handleGetFileActivity(c, defaults = {}) {
635
571
  from: parseDateParam(c.req.query("from"), defaults.from),
636
572
  to: parseDateParam(c.req.query("to"), defaults.to),
637
573
  limit
638
- }).map((activity) => withFileActivityDisplayTitle(activity, aliases))
574
+ }).map((activity) => decorateFileActivity(activity, aliases))
639
575
  });
640
576
  }
641
577
  async function handleGetSessionData(c, scanSource) {
642
578
  const startedAt = performance.now();
643
- const scanResult = scanSource.getSnapshot();
644
579
  const agentName = c.req.param("agent");
645
580
  const sessionId = c.req.param("id");
646
581
  if (!agentName) {
@@ -649,19 +584,15 @@ async function handleGetSessionData(c, scanSource) {
649
584
  if (!sessionId) {
650
585
  return c.json({ error: "Missing session ID" }, 400);
651
586
  }
652
- const agent = scanResult.agents.find((a) => a.name === agentName);
653
- if (!agent) {
654
- return c.json({ error: `Unknown agent: ${agentName}` }, 404);
655
- }
656
587
  try {
657
- const head = scanResult.byAgent[agentName]?.find((item) => item.id === sessionId);
658
- const loadStartedAt = performance.now();
659
- const cachedData = loadCachedSessionData(agentName, sessionId);
660
- const cachedMessageCount = cachedData?.stats.message_count ?? 0;
661
- const cacheHasExpectedMessages = cachedData !== null && (cachedData.messages.length > 0 || cachedMessageCount === 0);
662
- const data = cacheHasExpectedMessages ? cachedData : head ? agent.getSessionData(sessionId) : null;
663
- const loadDuration = performance.now() - loadStartedAt;
664
- if (!data) {
588
+ const result = materializeSessionDetailResponse(scanSource.getSnapshot(), {
589
+ agentName,
590
+ sessionId
591
+ });
592
+ if (result.status === "unknown-agent") {
593
+ return c.json({ error: `Unknown agent: ${agentName}` }, 404);
594
+ }
595
+ if (result.status === "not-ready") {
665
596
  appLogger.warn("api.session_data.cache_miss", {
666
597
  agent: agentName,
667
598
  session_id: sessionId,
@@ -669,27 +600,20 @@ async function handleGetSessionData(c, scanSource) {
669
600
  });
670
601
  return c.json({ error: "Session cache not ready" }, 404);
671
602
  }
672
- const tagStartedAt = performance.now();
673
- const smartTags = data.smart_tags ?? classifySessionTags(data);
674
- const tagDuration = performance.now() - tagStartedAt;
675
- const projectIdentity = data.project_identity ?? head?.project_identity ?? computeIdentity(data.directory, realFs);
676
- const fileActivity = data.file_activity ?? (cacheHasExpectedMessages && cachedData ? listSessionFileActivity(agentName, sessionId) : extractSessionFileActivity(agentName, sessionId, projectIdentity.key, data.messages));
677
603
  appLogger.info("api.session_data", {
678
604
  agent: agentName,
679
605
  session_id: sessionId,
680
- messages: data.messages.length,
681
- load_duration_ms: Math.round(loadDuration),
682
- tag_duration_ms: Math.round(tagDuration),
606
+ messages: result.status === "found-json" ? result.messageCount : result.data.messages.length,
683
607
  duration_ms: Math.round(performance.now() - startedAt)
684
608
  });
685
- const aliases = loadSessionAliasMap();
686
- return c.json({
687
- ...withDisplayTitle(data, agentName, aliases),
688
- project_identity: projectIdentity,
689
- smart_tags: smartTags,
690
- smart_tags_source_updated_at: getSmartTagSourceTimestamp(data),
691
- file_activity: fileActivity
692
- });
609
+ const aliases = loadAliasView();
610
+ if (result.status === "found-json") {
611
+ return createSessionDetailJsonResponse(
612
+ aliases.decorate(result.data, result.data.reference),
613
+ result.messages
614
+ );
615
+ }
616
+ return c.json(aliases.decorate(result.data, result.data.reference));
693
617
  } catch (err) {
694
618
  const message = err instanceof Error ? err.message : "Failed to load session";
695
619
  appLogger.error("api.session_data.error", {
@@ -712,32 +636,26 @@ async function handlePostClientLog(c) {
712
636
  return c.json({ ok: true });
713
637
  }
714
638
  function handleGetBookmarks(c) {
715
- try {
716
- const aliases = loadSessionAliasMap();
717
- return c.json({
718
- bookmarks: listBookmarks().map((bookmark) => withBookmarkDisplayTitle(bookmark, aliases)),
719
- storageAvailable: true
720
- });
721
- } catch (error) {
722
- if (error instanceof StateStorageUnavailableError) {
723
- return c.json({ bookmarks: [], storageAvailable: false });
724
- }
725
- throw error;
726
- }
639
+ return withStorageErrors(
640
+ () => {
641
+ const aliases = loadAliasView();
642
+ return c.json({
643
+ bookmarks: listBookmarks().map((bookmark) => decorateBookmark(bookmark, aliases)),
644
+ storageAvailable: true
645
+ });
646
+ },
647
+ () => c.json({ bookmarks: [], storageAvailable: false })
648
+ );
727
649
  }
728
650
  async function handlePutBookmark(c) {
729
651
  const payload = parseBookmarkPayload(await c.req.json().catch(() => null));
730
652
  if (!payload) {
731
653
  return c.json({ error: "Invalid bookmark payload" }, 400);
732
654
  }
733
- try {
734
- return c.json({ bookmark: upsertBookmark(payload), storageAvailable: true });
735
- } catch (error) {
736
- if (error instanceof StateStorageUnavailableError) {
737
- return c.json({ error: "Bookmark storage is unavailable" }, 503);
738
- }
739
- throw error;
740
- }
655
+ return withStorageErrors(
656
+ () => c.json({ bookmark: upsertBookmark(payload), storageAvailable: true }),
657
+ () => c.json({ error: "Bookmark storage is unavailable" }, 503)
658
+ );
741
659
  }
742
660
  async function handleImportBookmarks(c) {
743
661
  const payload = await c.req.json().catch(() => null);
@@ -748,14 +666,10 @@ async function handleImportBookmarks(c) {
748
666
  if (bookmarks.length !== payload.length) {
749
667
  return c.json({ error: "Invalid bookmark payload" }, 400);
750
668
  }
751
- try {
752
- return c.json({ bookmarks: importBookmarks(bookmarks), storageAvailable: true });
753
- } catch (error) {
754
- if (error instanceof StateStorageUnavailableError) {
755
- return c.json({ error: "Bookmark storage is unavailable" }, 503);
756
- }
757
- throw error;
758
- }
669
+ return withStorageErrors(
670
+ () => c.json({ bookmarks: importBookmarks(bookmarks), storageAvailable: true }),
671
+ () => c.json({ error: "Bookmark storage is unavailable" }, 503)
672
+ );
759
673
  }
760
674
  function handleDeleteBookmark(c) {
761
675
  const agentKey = c.req.param("agent");
@@ -763,32 +677,35 @@ function handleDeleteBookmark(c) {
763
677
  if (!agentKey || !sessionId) {
764
678
  return c.json({ error: "Missing bookmark identifier" }, 400);
765
679
  }
766
- try {
767
- deleteBookmark(agentKey, sessionId);
768
- return c.json({ ok: true, storageAvailable: true });
769
- } catch (error) {
770
- if (error instanceof StateStorageUnavailableError) {
771
- return c.json({ error: "Bookmark storage is unavailable" }, 503);
772
- }
773
- throw error;
774
- }
680
+ return withStorageErrors(
681
+ () => {
682
+ deleteBookmark({ agentName: agentKey, sessionId });
683
+ return c.json({ ok: true, storageAvailable: true });
684
+ },
685
+ () => c.json({ error: "Bookmark storage is unavailable" }, 503)
686
+ );
775
687
  }
776
688
  async function handlePutSessionAlias(c) {
777
689
  const agentKey = c.req.param("agent");
778
690
  const sessionId = c.req.param("id");
779
691
  const payload = await c.req.json().catch(() => null);
780
- if (!agentKey || !sessionId || typeof payload?.alias !== "string") {
692
+ const aliasValue = payload?.alias;
693
+ if (!agentKey || !sessionId || typeof aliasValue !== "string") {
781
694
  return c.json({ error: "Invalid session alias payload" }, 400);
782
695
  }
783
696
  try {
784
- return c.json({ alias: upsertSessionAlias(agentKey, sessionId, payload.alias) });
697
+ return withStorageErrors(
698
+ () => {
699
+ const alias = upsertSessionAlias({ agentName: agentKey, sessionId }, aliasValue);
700
+ invalidateAliasView();
701
+ return c.json({ alias });
702
+ },
703
+ () => c.json({ error: "Session alias storage is unavailable" }, 503)
704
+ );
785
705
  } catch (error) {
786
- if (error instanceof TypeError) {
706
+ if (error instanceof SessionAliasValidationError) {
787
707
  return c.json({ error: "Session alias must be non-empty and at most 160 characters" }, 400);
788
708
  }
789
- if (isStateStorageUnavailable(error)) {
790
- return c.json({ error: "Session alias storage is unavailable" }, 503);
791
- }
792
709
  throw error;
793
710
  }
794
711
  }
@@ -798,15 +715,14 @@ function handleDeleteSessionAlias(c) {
798
715
  if (!agentKey || !sessionId) {
799
716
  return c.json({ error: "Missing session alias identifier" }, 400);
800
717
  }
801
- try {
802
- deleteSessionAlias(agentKey, sessionId);
803
- return c.json({ ok: true });
804
- } catch (error) {
805
- if (isStateStorageUnavailable(error)) {
806
- return c.json({ error: "Session alias storage is unavailable" }, 503);
807
- }
808
- throw error;
809
- }
718
+ return withStorageErrors(
719
+ () => {
720
+ deleteSessionAlias({ agentName: agentKey, sessionId });
721
+ invalidateAliasView();
722
+ return c.json({ ok: true });
723
+ },
724
+ () => c.json({ error: "Session alias storage is unavailable" }, 503)
725
+ );
810
726
  }
811
727
  function handleGetDashboard(c, scanSource, defaults = {}) {
812
728
  const scanResult = scanSource.getSnapshot();
@@ -831,15 +747,30 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
831
747
  projectKind: projectIdentity?.kind,
832
748
  projectKey: projectIdentity?.key
833
749
  };
834
- const agentInfo = getAgentInfoMap({});
835
- const agentInfoMap = new Map(agentInfo.map((a) => [a.name, a]));
836
- const aggregate = buildDashboard(scanResult.sessions, {
837
- byAgentNames: Object.keys(scanResult.byAgent),
838
- scope,
839
- from,
840
- to,
841
- agentInfoMap
842
- });
750
+ const fixedTo = parseDateParam(c.req.query("to"), defaults.to);
751
+ const aggregate = getSnapshotAggregation(
752
+ scanSource,
753
+ scanResult.sessions,
754
+ [
755
+ "dashboard",
756
+ scope.agent,
757
+ scope.projectKind,
758
+ scope.projectKey,
759
+ from,
760
+ fixedTo ?? startOfLocalDay(to)
761
+ ],
762
+ () => {
763
+ const agentInfo = getAgentInfoMap({});
764
+ const agentInfoMap = new Map(agentInfo.map((agent) => [agent.name, agent]));
765
+ return buildDashboard(scanResult.sessions, {
766
+ byAgentNames: Object.keys(scanResult.byAgent),
767
+ scope,
768
+ from,
769
+ to,
770
+ agentInfoMap
771
+ });
772
+ }
773
+ );
843
774
  const data = {
844
775
  ...aggregate,
845
776
  recentFileActivities: listFileActivity({
@@ -852,14 +783,15 @@ function handleGetDashboard(c, scanSource, defaults = {}) {
852
783
  }),
853
784
  window: { from, to, days }
854
785
  };
855
- const aliases = loadSessionAliasMap();
786
+ const aliases = loadAliasView();
856
787
  return c.json({
857
788
  ...data,
858
- recentSessions: data.recentSessions.map(
859
- (session) => withDisplayTitle(session, session.agentName, aliases)
860
- ),
789
+ recentSessions: data.recentSessions.map((item) => ({
790
+ ...item,
791
+ session: aliases.decorate(item.session, item.reference)
792
+ })),
861
793
  recentFileActivities: data.recentFileActivities.map(
862
- (activity) => withFileActivityDisplayTitle(activity, aliases)
794
+ (activity) => decorateFileActivity(activity, aliases)
863
795
  )
864
796
  });
865
797
  }
@@ -997,11 +929,11 @@ var MAX_API_REQUEST_BYTES = 1024 * 1024;
997
929
  function findWebDistPath() {
998
930
  const __dirname2 = dirname(fileURLToPath(import.meta.url));
999
931
  const packagedPath = resolve(__dirname2, "web");
1000
- if (existsSync2(packagedPath)) {
932
+ if (existsSync(packagedPath)) {
1001
933
  return packagedPath;
1002
934
  }
1003
935
  const devPath = resolve(__dirname2, "../../../apps/web/dist");
1004
- if (existsSync2(devPath)) {
936
+ if (existsSync(devPath)) {
1005
937
  return devPath;
1006
938
  }
1007
939
  return null;
@@ -1073,6 +1005,7 @@ async function createServer(port, store, options = {}) {
1073
1005
  onError: (c) => c.json({ error: "Request body too large" }, 413)
1074
1006
  })
1075
1007
  );
1008
+ app.use("/api/*", compress());
1076
1009
  const routeOptions = {
1077
1010
  defaultSessionFrom: options.defaultSessionFrom,
1078
1011
  defaultSessionTo: options.defaultSessionTo,
@@ -1140,11 +1073,232 @@ async function createServer(port, store, options = {}) {
1140
1073
  }
1141
1074
 
1142
1075
  // src/live-scan.ts
1143
- import { existsSync as existsSync5 } from "fs";
1076
+ import { existsSync as existsSync4 } from "fs";
1144
1077
  import { fileURLToPath as fileURLToPath3 } from "url";
1145
1078
 
1079
+ // src/agent-operation-scheduler.ts
1080
+ var PENDING_REFRESH_DELAY_MS = 100;
1081
+ var MAX_ADAPTIVE_REFRESH_DELAY_MS = 3e4;
1082
+ var ADAPTIVE_REFRESH_DELAY_MULTIPLIER = 4;
1083
+ var AgentOperationScheduler = class {
1084
+ constructor(runRefresh) {
1085
+ this.runRefresh = runRefresh;
1086
+ }
1087
+ runRefresh;
1088
+ states = /* @__PURE__ */ new Map();
1089
+ operationGenerations = /* @__PURE__ */ new Map();
1090
+ operationTails = /* @__PURE__ */ new Map();
1091
+ isStopped = false;
1092
+ notify(agentName, delayMs) {
1093
+ if (this.isStopped) return;
1094
+ this.state(agentName).pendingSignalCount += 1;
1095
+ this.schedule(agentName, delayMs);
1096
+ }
1097
+ schedule(agentName, delayMs) {
1098
+ if (this.isStopped) return;
1099
+ const state = this.state(agentName);
1100
+ const adaptiveDelayMs = Math.min(
1101
+ state.lastRefreshDurationMs * ADAPTIVE_REFRESH_DELAY_MULTIPLIER,
1102
+ MAX_ADAPTIVE_REFRESH_DELAY_MS
1103
+ );
1104
+ const effectiveDelayMs = Math.max(delayMs, adaptiveDelayMs);
1105
+ const deadline = Date.now() + effectiveDelayMs;
1106
+ if (state.timer) {
1107
+ if (deadline >= state.timerDeadline) return;
1108
+ clearTimeout(state.timer);
1109
+ }
1110
+ appLogger.debug("scan.refresh.schedule", { agent: agentName, delay_ms: effectiveDelayMs });
1111
+ state.timerDeadline = deadline;
1112
+ state.timer = setTimeout(() => {
1113
+ state.timer = null;
1114
+ state.timerDeadline = 0;
1115
+ void this.refresh(agentName);
1116
+ }, effectiveDelayMs);
1117
+ }
1118
+ async refresh(agentName) {
1119
+ if (this.isStopped) return;
1120
+ const state = this.state(agentName);
1121
+ if (state.isRefreshRunning) {
1122
+ appLogger.debug("scan.refresh.pending", { agent: agentName });
1123
+ state.hasPendingRefresh = true;
1124
+ return;
1125
+ }
1126
+ state.isRefreshRunning = true;
1127
+ try {
1128
+ await this.run(agentName, "refresh", () => this.runRefresh(agentName));
1129
+ } finally {
1130
+ state.isRefreshRunning = false;
1131
+ if (state.hasPendingRefresh && !this.isStopped) {
1132
+ state.hasPendingRefresh = false;
1133
+ this.schedule(agentName, PENDING_REFRESH_DELAY_MS);
1134
+ }
1135
+ }
1136
+ }
1137
+ run(agentName, kind, operation) {
1138
+ const previous = this.operationTails.get(agentName) ?? Promise.resolve();
1139
+ const run = previous.then(async () => {
1140
+ if (this.isStopped) return "skipped";
1141
+ const lifecycle = this.beginOperation(agentName, kind);
1142
+ try {
1143
+ const result = await operation();
1144
+ this.completeOperation(lifecycle, result);
1145
+ return result;
1146
+ } catch (error) {
1147
+ this.completeOperation(lifecycle, "failed");
1148
+ throw error;
1149
+ }
1150
+ });
1151
+ const tail = run.then(
1152
+ () => void 0,
1153
+ () => void 0
1154
+ );
1155
+ this.operationTails.set(agentName, tail);
1156
+ void tail.finally(() => {
1157
+ if (this.operationTails.get(agentName) === tail) this.operationTails.delete(agentName);
1158
+ });
1159
+ return run;
1160
+ }
1161
+ takePendingSignalCount(agentName) {
1162
+ const state = this.state(agentName);
1163
+ const count = state.pendingSignalCount;
1164
+ state.pendingSignalCount = 0;
1165
+ return count;
1166
+ }
1167
+ recordRefreshDuration(agentName, durationMs) {
1168
+ this.state(agentName).lastRefreshDurationMs = durationMs;
1169
+ }
1170
+ snapshot() {
1171
+ return {
1172
+ activeOperations: this.operationTails.size,
1173
+ activeRefreshes: [...this.states.values()].filter((state) => state.isRefreshRunning).length
1174
+ };
1175
+ }
1176
+ stop() {
1177
+ this.isStopped = true;
1178
+ for (const state of this.states.values()) {
1179
+ if (state.timer) clearTimeout(state.timer);
1180
+ state.timer = null;
1181
+ state.timerDeadline = 0;
1182
+ state.hasPendingRefresh = false;
1183
+ state.pendingSignalCount = 0;
1184
+ }
1185
+ }
1186
+ async waitForIdle() {
1187
+ await Promise.allSettled(this.operationTails.values());
1188
+ }
1189
+ state(agentName) {
1190
+ const existing = this.states.get(agentName);
1191
+ if (existing) return existing;
1192
+ const state = {
1193
+ timer: null,
1194
+ timerDeadline: 0,
1195
+ isRefreshRunning: false,
1196
+ hasPendingRefresh: false,
1197
+ lastRefreshDurationMs: 0,
1198
+ pendingSignalCount: 0
1199
+ };
1200
+ this.states.set(agentName, state);
1201
+ return state;
1202
+ }
1203
+ beginOperation(agentName, kind) {
1204
+ const generation = (this.operationGenerations.get(agentName) ?? 0) + 1;
1205
+ const startedAt = Date.now();
1206
+ this.operationGenerations.set(agentName, generation);
1207
+ appLogger.info("scan.agent_operation.started", {
1208
+ agent: agentName,
1209
+ operation: kind,
1210
+ generation,
1211
+ started_at: startedAt
1212
+ });
1213
+ return { agentName, kind, generation, startedAt };
1214
+ }
1215
+ completeOperation(lifecycle, result) {
1216
+ const completedAt = Date.now();
1217
+ appLogger.info("scan.agent_operation.completed", {
1218
+ agent: lifecycle.agentName,
1219
+ operation: lifecycle.kind,
1220
+ generation: lifecycle.generation,
1221
+ started_at: lifecycle.startedAt,
1222
+ completed_at: completedAt,
1223
+ duration_ms: completedAt - lifecycle.startedAt,
1224
+ result
1225
+ });
1226
+ }
1227
+ };
1228
+
1229
+ // src/live-session-index.ts
1230
+ var LiveSessionIndex = class {
1231
+ agents = [];
1232
+ agentsByName = /* @__PURE__ */ new Map();
1233
+ byAgent = {};
1234
+ sessions = [];
1235
+ signatureCaches = /* @__PURE__ */ new Map();
1236
+ initialize(snapshot, options = {}) {
1237
+ const agentMap = /* @__PURE__ */ new Map();
1238
+ for (const agent of snapshot.agents) agentMap.set(agent.name, agent);
1239
+ for (const agent of options.registeredAgents ?? []) {
1240
+ if (!agentMap.has(agent.name)) agentMap.set(agent.name, agent);
1241
+ }
1242
+ this.agents = [...agentMap.values()].filter(
1243
+ (agent) => !options.allowedAgents || options.allowedAgents.has(agent.name.toLowerCase())
1244
+ );
1245
+ this.agentsByName = new Map(this.agents.map((agent) => [agent.name, agent]));
1246
+ this.byAgent = Object.fromEntries(
1247
+ this.agents.map((agent) => [agent.name, sortSessions(snapshot.byAgent[agent.name] ?? [])])
1248
+ );
1249
+ this.sessions = mergeSortedSessions(Object.values(this.byAgent));
1250
+ this.signatureCaches.clear();
1251
+ }
1252
+ snapshot() {
1253
+ return {
1254
+ agents: this.agents,
1255
+ byAgent: this.byAgent,
1256
+ sessions: this.sessions
1257
+ };
1258
+ }
1259
+ findAgent(agentName) {
1260
+ return this.agentsByName.get(agentName);
1261
+ }
1262
+ commitAgentSessions(agentName, nextSessions, candidateChangedIds = []) {
1263
+ const previousSessions = this.byAgent[agentName] ?? [];
1264
+ const signatureCache = this.signatureCache(agentName);
1265
+ const { changes, removedSessionIds, counts } = computeSessionDiff(
1266
+ previousSessions,
1267
+ nextSessions,
1268
+ candidateChangedIds,
1269
+ sessionSignature,
1270
+ signatureCache
1271
+ );
1272
+ for (const removedId of removedSessionIds) signatureCache.delete(removedId);
1273
+ this.byAgent[agentName] = sortSessions(nextSessions);
1274
+ this.sessions = mergeSortedSessions(Object.values(this.byAgent));
1275
+ if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) return null;
1276
+ return {
1277
+ type: "sessions-updated",
1278
+ changedAgents: [agentName],
1279
+ newSessions: counts.new,
1280
+ updatedSessions: counts.updated,
1281
+ removedSessions: counts.removed,
1282
+ totalSessions: this.sessions.length,
1283
+ timestamp: Date.now(),
1284
+ changedSessionHeads: changes.map(({ session }) => ({
1285
+ reference: { agentName, sessionId: session.id },
1286
+ session
1287
+ })),
1288
+ removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
1289
+ };
1290
+ }
1291
+ signatureCache(agentName) {
1292
+ const existing = this.signatureCaches.get(agentName);
1293
+ if (existing) return existing;
1294
+ const cache = /* @__PURE__ */ new Map();
1295
+ this.signatureCaches.set(agentName, cache);
1296
+ return cache;
1297
+ }
1298
+ };
1299
+
1146
1300
  // src/search-index-job-runner.ts
1147
- import { existsSync as existsSync3 } from "fs";
1301
+ import { existsSync as existsSync2 } from "fs";
1148
1302
  import { fileURLToPath as fileURLToPath2 } from "url";
1149
1303
  import { Worker } from "worker_threads";
1150
1304
 
@@ -1295,7 +1449,6 @@ var SearchIndexJobRunner = class {
1295
1449
  nextBatchId = 1;
1296
1450
  pendingJobs = new PendingSearchIndexJobs();
1297
1451
  isShuttingDown = false;
1298
- hasCheckedFtsIntegrity = false;
1299
1452
  enqueue(context, jobs) {
1300
1453
  if (jobs.length === 0) return Promise.resolve();
1301
1454
  if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE));
@@ -1368,8 +1521,7 @@ var SearchIndexJobRunner = class {
1368
1521
  jobs: batch.jobs,
1369
1522
  agentNames: [],
1370
1523
  sessionsByAgent: {},
1371
- metaByAgent: {},
1372
- skipFtsIntegrityCheck: this.hasCheckedFtsIntegrity
1524
+ metaByAgent: {}
1373
1525
  }
1374
1526
  });
1375
1527
  worker.unref();
@@ -1385,7 +1537,6 @@ var SearchIndexJobRunner = class {
1385
1537
  duration_ms: Math.round(message.durationMs),
1386
1538
  sessions: message.sessions
1387
1539
  });
1388
- this.hasCheckedFtsIntegrity = true;
1389
1540
  this.settle(batch);
1390
1541
  });
1391
1542
  worker.on("error", (error) => {
@@ -1418,7 +1569,7 @@ var SearchIndexJobRunner = class {
1418
1569
  }
1419
1570
  workerUrl() {
1420
1571
  const workerUrl = new URL("./search-index-worker.js", import.meta.url);
1421
- if (workerUrl.protocol === "file:" && !existsSync3(fileURLToPath2(workerUrl))) return null;
1572
+ if (workerUrl.protocol === "file:" && !existsSync2(fileURLToPath2(workerUrl))) return null;
1422
1573
  return workerUrl;
1423
1574
  }
1424
1575
  };
@@ -1541,6 +1692,21 @@ var ScanStatusModel = class {
1541
1692
  updatedAt: now
1542
1693
  });
1543
1694
  }
1695
+ indexAgent(agentName) {
1696
+ const status = this.status.agentStatuses[agentName];
1697
+ if (!this.status.active || !status || status.status !== "scanning") return null;
1698
+ const now = Date.now();
1699
+ const agentStatuses = {
1700
+ ...this.status.agentStatuses,
1701
+ [agentName]: { ...status, status: "indexing", updatedAt: now }
1702
+ };
1703
+ return this.set({
1704
+ ...this.status,
1705
+ phase: this.activePhase(this.status.pendingAgents, this.status.scanningAgents, agentStatuses),
1706
+ agentStatuses,
1707
+ updatedAt: now
1708
+ });
1709
+ }
1544
1710
  finishAgent(agentName, sessionCount) {
1545
1711
  const pendingAgents = this.status.pendingAgents.filter((agent) => agent !== agentName);
1546
1712
  const scanningAgents = this.status.scanningAgents.filter((agent) => agent !== agentName);
@@ -1549,26 +1715,27 @@ var ScanStatusModel = class {
1549
1715
  const now = Date.now();
1550
1716
  const previousStatus = this.status.agentStatuses[agentName];
1551
1717
  const total = previousStatus?.total ?? previousStatus?.processed;
1718
+ const agentStatuses = {
1719
+ ...this.status.agentStatuses,
1720
+ [agentName]: {
1721
+ agentName,
1722
+ status: "complete",
1723
+ total,
1724
+ processed: total,
1725
+ sessions: sessionCount ?? previousStatus?.sessions ?? 0,
1726
+ startedAt: previousStatus?.startedAt,
1727
+ updatedAt: now,
1728
+ completedAt: now
1729
+ }
1730
+ };
1552
1731
  return this.set({
1553
1732
  ...this.status,
1554
1733
  active: isActive,
1555
- phase: isActive ? "scanning" : "idle",
1734
+ phase: isActive ? this.activePhase(pendingAgents, scanningAgents, agentStatuses) : "idle",
1556
1735
  pendingAgents,
1557
1736
  scanningAgents,
1558
1737
  completedAgents,
1559
- agentStatuses: {
1560
- ...this.status.agentStatuses,
1561
- [agentName]: {
1562
- agentName,
1563
- status: "complete",
1564
- total,
1565
- processed: total,
1566
- sessions: sessionCount ?? previousStatus?.sessions ?? 0,
1567
- startedAt: previousStatus?.startedAt,
1568
- updatedAt: now,
1569
- completedAt: now
1570
- }
1571
- },
1738
+ agentStatuses,
1572
1739
  updatedAt: now,
1573
1740
  completedAt: isActive ? void 0 : now
1574
1741
  });
@@ -1598,6 +1765,12 @@ var ScanStatusModel = class {
1598
1765
  updatedAt: Date.now()
1599
1766
  });
1600
1767
  }
1768
+ activePhase(pendingAgents, scanningAgents, agentStatuses) {
1769
+ if (pendingAgents.length === 0 && scanningAgents.length > 0 && scanningAgents.every((agentName) => agentStatuses[agentName]?.status === "indexing")) {
1770
+ return "indexing";
1771
+ }
1772
+ return this.status.phase === "initializing" ? "initializing" : "scanning";
1773
+ }
1601
1774
  set(status) {
1602
1775
  this.status = status;
1603
1776
  return this.snapshot();
@@ -1607,36 +1780,16 @@ var ScanStatusModel = class {
1607
1780
  // src/agent-sync-engine.ts
1608
1781
  var REFRESH_DEBOUNCE_MS = 200;
1609
1782
  var EMPTY_AGENT_REFRESH_DEBOUNCE_MS = 3e4;
1610
- var PENDING_REFRESH_DELAY_MS = 100;
1611
- var MAX_ADAPTIVE_REFRESH_DELAY_MS = 3e4;
1612
- var ADAPTIVE_REFRESH_DELAY_MULTIPLIER = 4;
1613
1783
  var SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD = 100;
1614
1784
  var BACKFILL_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1615
- function buildRefreshDiff(agentName, previousSessions, nextSessions, candidateChangedIds = []) {
1616
- const { changes, removedSessionIds, counts } = computeSessionDiff(
1785
+ function buildPersistenceDiff(previousSessions, nextSessions, candidateChangedIds = []) {
1786
+ const { changes, removedSessionIds } = computeSessionDiff(
1617
1787
  previousSessions,
1618
- nextSessions,
1619
- candidateChangedIds,
1620
- sessionSignature
1621
- );
1622
- if (counts.new === 0 && counts.updated === 0 && counts.removed === 0) {
1623
- return { event: null, changedSessions: changes, removedSessionIds };
1624
- }
1625
- return {
1626
- changedSessions: changes,
1627
- removedSessionIds,
1628
- event: {
1629
- type: "sessions-updated",
1630
- changedAgents: [agentName],
1631
- newSessions: counts.new,
1632
- updatedSessions: counts.updated,
1633
- removedSessions: counts.removed,
1634
- totalSessions: nextSessions.length,
1635
- timestamp: Date.now(),
1636
- changedSessionHeads: changes.map(({ session }) => ({ agentName, session })),
1637
- removedSessionRefs: removedSessionIds.map((sessionId) => ({ agentName, sessionId }))
1638
- }
1639
- };
1788
+ nextSessions,
1789
+ candidateChangedIds,
1790
+ sessionSignature
1791
+ );
1792
+ return { changedSessions: changes, removedSessionIds };
1640
1793
  }
1641
1794
  function restoreAgentCacheMeta(agent, cached) {
1642
1795
  agent.setSessionMetaMap(new Map(Object.entries(cached.meta)));
@@ -1644,11 +1797,12 @@ function restoreAgentCacheMeta(agent, cached) {
1644
1797
  var AgentSyncEngine = class {
1645
1798
  constructor(options) {
1646
1799
  this.options = options;
1800
+ this.scheduler = new AgentOperationScheduler((agentName) => this.performRefresh(agentName));
1647
1801
  }
1648
1802
  options;
1649
- refreshStates = /* @__PURE__ */ new Map();
1650
- operationGenerations = /* @__PURE__ */ new Map();
1651
- operationTails = /* @__PURE__ */ new Map();
1803
+ lastRefreshAtByAgent = /* @__PURE__ */ new Map();
1804
+ scheduler;
1805
+ sessionIndex = new LiveSessionIndex();
1652
1806
  backfillQueue = [];
1653
1807
  currentBackfillAgent;
1654
1808
  completedBackfillAgents = [];
@@ -1657,13 +1811,22 @@ var AgentSyncEngine = class {
1657
1811
  statusChangedListeners = /* @__PURE__ */ new Set();
1658
1812
  scanStatus = new ScanStatusModel();
1659
1813
  searchIndexJobs = new SearchIndexJobRunner();
1814
+ nextPublicationId = 1;
1660
1815
  backgroundRefreshTimer = null;
1661
1816
  isShuttingDown = false;
1662
- initialize(cacheTimestamps = {}) {
1663
- for (const agent of this.options.snapshot().agents) {
1664
- this.state(agent.name).lastRefreshAt = cacheTimestamps[agent.name] ?? Date.now();
1817
+ initialize(snapshot, options = {}) {
1818
+ this.sessionIndex.initialize(snapshot, options);
1819
+ this.lastRefreshAtByAgent.clear();
1820
+ for (const agent of this.sessionIndex.snapshot().agents) {
1821
+ this.lastRefreshAtByAgent.set(
1822
+ agent.name,
1823
+ options.cacheTimestamps?.[agent.name] ?? Date.now()
1824
+ );
1665
1825
  }
1666
1826
  }
1827
+ snapshot() {
1828
+ return this.sessionIndex.snapshot();
1829
+ }
1667
1830
  status() {
1668
1831
  return this.scanStatus.snapshot();
1669
1832
  }
@@ -1676,49 +1839,45 @@ var AgentSyncEngine = class {
1676
1839
  return () => this.statusChangedListeners.delete(listener);
1677
1840
  }
1678
1841
  async syncInitialIndex() {
1679
- await this.searchIndexJobs.enqueue(
1680
- "scan.initial",
1681
- this.buildFullSearchIndexJobs("scan.initial")
1682
- );
1842
+ const jobs = this.buildFullSearchIndexJobs("scan.initial");
1843
+ await this.commitSearchIndex("scan.initial", jobs, {
1844
+ publicationId: this.publicationId("scan.initial"),
1845
+ agents: jobs.map((job) => job.agentName)
1846
+ });
1683
1847
  }
1684
1848
  handleAgentsChanged(agentNames) {
1685
- const snapshot = this.options.snapshot();
1849
+ const snapshot = this.sessionIndex.snapshot();
1686
1850
  for (const agentName of agentNames) {
1687
- this.state(agentName).pendingPathCount += 1;
1688
1851
  const delayMs = (snapshot.byAgent[agentName]?.length ?? 0) === 0 ? EMPTY_AGENT_REFRESH_DEBOUNCE_MS : REFRESH_DEBOUNCE_MS;
1689
- this.scheduleRefresh(agentName, delayMs);
1852
+ this.scheduler.notify(agentName, delayMs);
1690
1853
  }
1691
1854
  }
1692
1855
  startBackgroundRefresh() {
1693
1856
  if (this.backgroundRefreshTimer) return;
1694
- const agentNames = this.options.snapshot().agents.map((agent) => agent.name);
1857
+ const agentNames = this.sessionIndex.snapshot().agents.map((agent) => agent.name);
1695
1858
  this.startScanBatch(agentNames, "scanning");
1696
1859
  this.backgroundRefreshTimer = setTimeout(() => {
1697
1860
  this.backgroundRefreshTimer = null;
1698
- for (const agentName of agentNames) this.scheduleRefresh(agentName, 0);
1861
+ for (const agentName of agentNames) this.scheduler.schedule(agentName, 0);
1699
1862
  if (agentNames.length === 0) this.finishScanBatch();
1700
1863
  }, 0);
1701
1864
  }
1702
1865
  async refresh(agentName) {
1703
- await this.runCoalescedRefresh(agentName);
1866
+ await this.scheduler.refresh(agentName);
1704
1867
  }
1705
1868
  async shutdown() {
1706
1869
  this.isShuttingDown = true;
1870
+ const schedulerSnapshot = this.scheduler.snapshot();
1707
1871
  const activeOperations = {
1708
- agent_operations: this.operationTails.size,
1709
- refreshes: [...this.refreshStates.values()].filter((state) => state.isRunning).length,
1872
+ agent_operations: schedulerSnapshot.activeOperations,
1873
+ refreshes: schedulerSnapshot.activeRefreshes,
1710
1874
  backfill_running: this.currentBackfillAgent != null || void 0,
1711
1875
  scan_workers: this.options.workerRunner.activeCount
1712
1876
  };
1713
1877
  if (activeOperations.agent_operations > 0 || activeOperations.scan_workers > 0) {
1714
1878
  appLogger.warn("scan.shutdown.active_operations", activeOperations);
1715
1879
  }
1716
- for (const state of this.refreshStates.values()) {
1717
- if (!state.timer) continue;
1718
- clearTimeout(state.timer);
1719
- state.timer = null;
1720
- state.timerDeadline = 0;
1721
- }
1880
+ this.scheduler.stop();
1722
1881
  if (this.backgroundRefreshTimer) {
1723
1882
  clearTimeout(this.backgroundRefreshTimer);
1724
1883
  this.backgroundRefreshTimer = null;
@@ -1732,7 +1891,7 @@ var AgentSyncEngine = class {
1732
1891
  });
1733
1892
  await this.searchIndexJobs.shutdown();
1734
1893
  await this.options.workerRunner.shutdown();
1735
- await Promise.allSettled(this.operationTails.values());
1894
+ await this.scheduler.waitForIdle();
1736
1895
  const stoppedSearchIndexSnapshot = this.searchIndexJobs.snapshot();
1737
1896
  appLogger.info("search_index.shutdown.completed", {
1738
1897
  active_batch_id: searchIndexSnapshot.activeBatchId,
@@ -1740,7 +1899,7 @@ var AgentSyncEngine = class {
1740
1899
  });
1741
1900
  }
1742
1901
  startScanBatch(agentNames, phase) {
1743
- const snapshot = this.options.snapshot();
1902
+ const snapshot = this.sessionIndex.snapshot();
1744
1903
  const sessionCounts = Object.fromEntries(
1745
1904
  agentNames.map((agentName) => [agentName, snapshot.byAgent[agentName]?.length ?? 0])
1746
1905
  );
@@ -1750,7 +1909,7 @@ var AgentSyncEngine = class {
1750
1909
  this.publishStatus(this.scanStatus.setPhase(phase));
1751
1910
  }
1752
1911
  beginAgentScan(agentName) {
1753
- const snapshot = this.options.snapshot();
1912
+ const snapshot = this.sessionIndex.snapshot();
1754
1913
  if (!this.scanStatus.snapshot().active) this.startScanBatch([agentName], "scanning");
1755
1914
  this.publishStatus(
1756
1915
  this.scanStatus.beginAgent(agentName, snapshot.byAgent[agentName]?.length ?? 0)
@@ -1759,8 +1918,11 @@ var AgentSyncEngine = class {
1759
1918
  updateAgentScanProgress(agentName, progress) {
1760
1919
  this.publishStatus(this.scanStatus.updateAgent(agentName, progress));
1761
1920
  }
1921
+ beginAgentIndexing(agentName) {
1922
+ this.publishStatus(this.scanStatus.indexAgent(agentName));
1923
+ }
1762
1924
  finishAgentScan(agentName) {
1763
- const count = this.options.snapshot().byAgent[agentName]?.length;
1925
+ const count = this.sessionIndex.snapshot().byAgent[agentName]?.length;
1764
1926
  this.publishStatus(this.scanStatus.finishAgent(agentName, count));
1765
1927
  }
1766
1928
  finishScanBatch() {
@@ -1777,44 +1939,6 @@ var AgentSyncEngine = class {
1777
1939
  if (this.isShuttingDown) return;
1778
1940
  for (const listener of this.sessionsChangedListeners) listener(change);
1779
1941
  }
1780
- scheduleRefresh(agentName, delayMs) {
1781
- if (this.isShuttingDown) return;
1782
- const state = this.state(agentName);
1783
- const adaptiveDelayMs = Math.min(
1784
- state.lastRefreshDurationMs * ADAPTIVE_REFRESH_DELAY_MULTIPLIER,
1785
- MAX_ADAPTIVE_REFRESH_DELAY_MS
1786
- );
1787
- const effectiveDelayMs = Math.max(delayMs, adaptiveDelayMs);
1788
- const deadline = Date.now() + effectiveDelayMs;
1789
- if (state.timer) {
1790
- if (deadline >= state.timerDeadline) return;
1791
- clearTimeout(state.timer);
1792
- }
1793
- appLogger.debug("scan.refresh.schedule", { agent: agentName, delay_ms: effectiveDelayMs });
1794
- state.timerDeadline = deadline;
1795
- state.timer = setTimeout(() => {
1796
- state.timer = null;
1797
- void this.runCoalescedRefresh(agentName);
1798
- }, effectiveDelayMs);
1799
- }
1800
- async runCoalescedRefresh(agentName) {
1801
- const state = this.state(agentName);
1802
- if (state.isRunning) {
1803
- appLogger.debug("scan.refresh.pending", { agent: agentName });
1804
- state.hasPendingRerun = true;
1805
- return;
1806
- }
1807
- state.isRunning = true;
1808
- try {
1809
- await this.serialize(agentName, "refresh", () => this.performRefresh(agentName));
1810
- } finally {
1811
- state.isRunning = false;
1812
- if (state.hasPendingRerun && !this.isShuttingDown) {
1813
- state.hasPendingRerun = false;
1814
- this.scheduleRefresh(agentName, PENDING_REFRESH_DELAY_MS);
1815
- }
1816
- }
1817
- }
1818
1942
  async performRefresh(agentName) {
1819
1943
  this.beginAgentScan(agentName);
1820
1944
  try {
@@ -1831,18 +1955,16 @@ var AgentSyncEngine = class {
1831
1955
  }
1832
1956
  async runRefresh(agentName) {
1833
1957
  const startedAt = performance.now();
1834
- const state = this.state(agentName);
1835
- const pendingPathCount = state.pendingPathCount;
1836
- state.pendingPathCount = 0;
1958
+ const pendingPathCount = this.scheduler.takePendingSignalCount(agentName);
1837
1959
  const agent = this.findAgent(agentName);
1838
1960
  if (!agent) {
1839
1961
  appLogger.warn("scan.refresh.missing_agent", { agent: agentName });
1840
1962
  return "skipped";
1841
1963
  }
1842
- const previousSessions = this.options.snapshot().byAgent[agentName] ?? [];
1964
+ const previousSessions = this.sessionIndex.snapshot().byAgent[agentName] ?? [];
1843
1965
  const cached = loadCachedSessions(agentName);
1844
1966
  const refreshBaseline = cached?.sessions ?? previousSessions;
1845
- const cacheTimestamp = cached?.timestamp ?? state.lastRefreshAt;
1967
+ const cacheTimestamp = cached?.timestamp ?? this.lastRefreshAtByAgent.get(agentName) ?? 0;
1846
1968
  if (cached) restoreAgentCacheMeta(agent, cached);
1847
1969
  const isInitialized = isAgentCacheInitialized(agentName);
1848
1970
  const availabilityStartedAt = performance.now();
@@ -1867,73 +1989,59 @@ var AgentSyncEngine = class {
1867
1989
  }
1868
1990
  if (strategyResult.status === "unchanged") return "unchanged";
1869
1991
  const nextSessions = attachMissingProjectIdentities(strategyResult.nextSessions);
1870
- const diffStartedAt = performance.now();
1871
- const diff = buildRefreshDiff(
1872
- agentName,
1873
- previousSessions,
1874
- nextSessions,
1875
- strategyResult.preciseChangedIds ?? []
1876
- );
1877
- const diffDuration = performance.now() - diffStartedAt;
1878
1992
  const searchIndexOptions = pendingPathCount >= SEARCH_INDEX_BULK_PENDING_PATH_THRESHOLD ? { isBulk: true } : void 0;
1879
- const persistentChanges = strategyResult.persistenceDiff?.changedSessions ?? diff.changedSessions;
1880
- const persistentRemovedSessionIds = strategyResult.persistenceDiff?.removedSessionIds ?? diff.removedSessionIds;
1881
- const changedSessionIds = strategyResult.usedIncrementalScan ? new Set(persistentChanges.map(({ session }) => session.id)) : void 0;
1993
+ const persistenceDiff = strategyResult.persistenceDiff;
1994
+ const changedSessionIds = persistenceDiff ? new Set(persistenceDiff.changedSessions.map(({ session }) => session.id)) : void 0;
1882
1995
  const persistStartedAt = performance.now();
1883
- const persistentJob = strategyResult.usedIncrementalScan ? {
1996
+ const persistentJob = persistenceDiff ? {
1884
1997
  kind: "changes",
1885
1998
  context: "scan.refresh",
1886
1999
  agentName,
1887
- changes: persistentChanges,
1888
- removedSessionIds: persistentRemovedSessionIds,
2000
+ changes: persistenceDiff.changedSessions,
2001
+ removedSessionIds: persistenceDiff.removedSessionIds,
1889
2002
  meta: buildAgentCacheMeta(agent, changedSessionIds),
1890
2003
  ...searchIndexOptions ? { searchIndexOptions } : {}
1891
- } : strategyResult.fullScanSessions ? {
2004
+ } : {
1892
2005
  kind: "full",
1893
2006
  context: "scan.refresh",
1894
2007
  agentName,
1895
- sessions: strategyResult.fullScanSessions,
2008
+ sessions: strategyResult.fullScanSessions ?? nextSessions,
1896
2009
  meta: buildAgentCacheMeta(agent),
1897
2010
  saveCache: true,
1898
2011
  ...searchIndexOptions ? { searchIndexOptions } : {}
1899
- } : null;
1900
- if (persistentJob) {
1901
- const persist = this.searchIndexJobs.enqueue("scan.refresh", [persistentJob]);
1902
- if (!isInitialized && persistentJob.kind === "full") {
1903
- await persist;
1904
- } else {
1905
- void persist.catch((error) => {
1906
- appLogger.error("scan.refresh.persist.error", { agent: agentName, error });
1907
- console.error(`[${agentName}] Session persistence failed:`, error);
1908
- });
1909
- }
1910
- }
2012
+ };
2013
+ this.beginAgentIndexing(agentName);
2014
+ const publication = await this.commitSessionPublication({
2015
+ context: "scan.refresh",
2016
+ agentName,
2017
+ sessions: nextSessions,
2018
+ candidateChangedIds: strategyResult.preciseChangedIds ?? [],
2019
+ indexJob: persistentJob
2020
+ });
1911
2021
  const persistDuration = performance.now() - persistStartedAt;
1912
2022
  logSearchIndexSync("scan.refresh", null, { pending_paths: pendingPathCount });
1913
- this.emitSessionsChanged({ agentName, sessions: nextSessions, event: diff.event });
1914
2023
  const totalDurationMs = performance.now() - startedAt;
1915
- state.lastRefreshDurationMs = totalDurationMs;
2024
+ this.scheduler.recordRefreshDuration(agentName, totalDurationMs);
1916
2025
  appLogger.info("scan.refresh.done", {
1917
2026
  agent: agentName,
1918
2027
  duration_ms: Math.round(totalDurationMs),
1919
2028
  sessions: nextSessions.length,
1920
- new_sessions: diff.event?.newSessions ?? 0,
1921
- updated_sessions: diff.event?.updatedSessions ?? 0,
1922
- removed_sessions: diff.event?.removedSessions ?? 0,
2029
+ new_sessions: publication.event?.newSessions ?? 0,
2030
+ updated_sessions: publication.event?.updatedSessions ?? 0,
2031
+ removed_sessions: publication.event?.removedSessions ?? 0,
1923
2032
  pending_paths: pendingPathCount,
1924
2033
  availability_ms: Math.round(availabilityDuration),
1925
2034
  check_ms: Math.round(strategyResult.checkDuration),
1926
2035
  scan_ms: Math.round(strategyResult.scanDuration),
1927
- diff_ms: Math.round(diffDuration),
2036
+ diff_ms: Math.round(publication.diffDuration),
1928
2037
  persist_ms: Math.round(persistDuration),
1929
- search_index_ms: 0,
1930
- persistent_index_worker_job: persistentJob?.kind,
1931
- persistent_index_skipped: !persistentJob || void 0
2038
+ search_index_ms: Math.round(persistDuration),
2039
+ persistent_index_worker_job: persistentJob.kind
1932
2040
  });
1933
2041
  return "committed";
1934
2042
  }
1935
2043
  refreshUnavailableAgent(agentName) {
1936
- this.state(agentName).lastRefreshAt = Date.now();
2044
+ this.lastRefreshAtByAgent.set(agentName, Date.now());
1937
2045
  return this.refreshStrategyResult([]);
1938
2046
  }
1939
2047
  async initializeAgent(agent, previousSessions) {
@@ -1942,7 +2050,7 @@ var AgentSyncEngine = class {
1942
2050
  const result = await this.runWorker(agent, previousSessions, null, this.startupScanOptions());
1943
2051
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
1944
2052
  const sessions = attachMissingProjectIdentities(result.sessions);
1945
- this.state(agent.name).lastRefreshAt = Date.now();
2053
+ this.lastRefreshAtByAgent.set(agent.name, Date.now());
1946
2054
  return this.refreshStrategyResult(sessions, {
1947
2055
  fullScanSessions: sessions,
1948
2056
  scanDuration: performance.now() - scanStartedAt
@@ -1957,17 +2065,17 @@ var AgentSyncEngine = class {
1957
2065
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
1958
2066
  const sessions = attachMissingProjectIdentities(result.sessions);
1959
2067
  const preciseChangedIds = result.changedIds ?? [];
1960
- const persistenceDiff = buildRefreshDiff(
1961
- agent.name,
1962
- cached.sessions,
1963
- sessions,
1964
- preciseChangedIds
1965
- );
1966
- this.state(agent.name).lastRefreshAt = Date.now();
1967
- if (preciseChangedIds.length === 0) this.logUnchangedRefresh(agent.name, refreshStartedAt);
2068
+ const persistenceDiff = buildPersistenceDiff(cached.sessions, sessions, preciseChangedIds);
2069
+ this.lastRefreshAtByAgent.set(agent.name, Date.now());
2070
+ if (persistenceDiff.changedSessions.length === 0 && persistenceDiff.removedSessionIds.length === 0) {
2071
+ this.logUnchangedRefresh(agent.name, refreshStartedAt);
2072
+ return this.refreshStrategyResult(sessions, {
2073
+ status: "unchanged",
2074
+ scanDuration: performance.now() - scanStartedAt
2075
+ });
2076
+ }
1968
2077
  return this.refreshStrategyResult(sessions, {
1969
2078
  preciseChangedIds,
1970
- usedIncrementalScan: true,
1971
2079
  persistenceDiff,
1972
2080
  scanDuration: performance.now() - scanStartedAt
1973
2081
  });
@@ -1976,20 +2084,29 @@ var AgentSyncEngine = class {
1976
2084
  const checkStartedAt = performance.now();
1977
2085
  const checkResult = await Promise.resolve(agent.checkForChanges(cacheTimestamp, baseline));
1978
2086
  const checkDuration = performance.now() - checkStartedAt;
1979
- this.state(agent.name).lastRefreshAt = checkResult.timestamp;
2087
+ this.lastRefreshAtByAgent.set(agent.name, checkResult.timestamp);
1980
2088
  if (!checkResult.hasChanges) {
1981
2089
  this.logUnchangedRefresh(agent.name, refreshStartedAt);
1982
2090
  return this.refreshStrategyResult(baseline, { status: "unchanged", checkDuration });
1983
2091
  }
1984
2092
  const preciseChangedIds = checkResult.changedIds ?? null;
1985
2093
  const scanStartedAt = performance.now();
2094
+ if (preciseChangedIds === null) {
2095
+ const result = await this.runWorker(agent, baseline, null, {});
2096
+ agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
2097
+ const sessions2 = attachMissingProjectIdentities(result.sessions);
2098
+ return this.refreshStrategyResult(sessions2, {
2099
+ persistenceDiff: buildPersistenceDiff(baseline, sessions2),
2100
+ checkDuration,
2101
+ scanDuration: performance.now() - scanStartedAt
2102
+ });
2103
+ }
1986
2104
  const sessions = attachMissingProjectIdentities(
1987
- await Promise.resolve(agent.incrementalScan(baseline, checkResult.changedIds ?? []))
2105
+ await Promise.resolve(agent.incrementalScan(baseline, preciseChangedIds, checkResult.refs))
1988
2106
  );
1989
2107
  return this.refreshStrategyResult(sessions, {
1990
2108
  preciseChangedIds,
1991
- usedIncrementalScan: Array.isArray(checkResult.changedIds),
1992
- persistenceDiff: buildRefreshDiff(agent.name, baseline, sessions, preciseChangedIds ?? []),
2109
+ persistenceDiff: buildPersistenceDiff(baseline, sessions, preciseChangedIds),
1993
2110
  checkDuration,
1994
2111
  scanDuration: performance.now() - scanStartedAt
1995
2112
  });
@@ -1999,7 +2116,7 @@ var AgentSyncEngine = class {
1999
2116
  const result = await this.runWorker(agent, previousSessions, null, {});
2000
2117
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
2001
2118
  const sessions = attachMissingProjectIdentities(result.sessions);
2002
- this.state(agent.name).lastRefreshAt = Date.now();
2119
+ this.lastRefreshAtByAgent.set(agent.name, Date.now());
2003
2120
  return this.refreshStrategyResult(sessions, {
2004
2121
  fullScanSessions: sessions,
2005
2122
  scanDuration: performance.now() - scanStartedAt
@@ -2011,7 +2128,6 @@ var AgentSyncEngine = class {
2011
2128
  nextSessions,
2012
2129
  fullScanSessions: null,
2013
2130
  preciseChangedIds: null,
2014
- usedIncrementalScan: false,
2015
2131
  persistenceDiff: null,
2016
2132
  checkDuration: 0,
2017
2133
  scanDuration: 0,
@@ -2049,30 +2165,31 @@ var AgentSyncEngine = class {
2049
2165
  if (!agentName) return;
2050
2166
  this.currentBackfillAgent = agentName;
2051
2167
  this.publishBackfillStatus();
2052
- void this.serialize(agentName, "backfill", () => this.performBackfill(agentName)).then(
2053
- (result) => {
2054
- if (this.isShuttingDown) return;
2055
- this.currentBackfillAgent = void 0;
2056
- if (result === "committed") {
2057
- if (!this.completedBackfillAgents.includes(agentName)) {
2058
- this.completedBackfillAgents.push(agentName);
2059
- }
2060
- this.failedBackfillAgents = this.failedBackfillAgents.filter(
2061
- (failedAgent) => failedAgent !== agentName
2062
- );
2063
- } else if (!this.failedBackfillAgents.includes(agentName)) {
2064
- this.failedBackfillAgents.push(agentName);
2168
+ void this.runBackfill(agentName).then((result) => {
2169
+ if (this.isShuttingDown) return;
2170
+ this.currentBackfillAgent = void 0;
2171
+ if (result === "committed") {
2172
+ if (!this.completedBackfillAgents.includes(agentName)) {
2173
+ this.completedBackfillAgents.push(agentName);
2065
2174
  }
2066
- this.publishBackfillStatus();
2067
- this.pumpBackfillQueue();
2175
+ this.failedBackfillAgents = this.failedBackfillAgents.filter(
2176
+ (failedAgent) => failedAgent !== agentName
2177
+ );
2178
+ } else if (!this.failedBackfillAgents.includes(agentName)) {
2179
+ this.failedBackfillAgents.push(agentName);
2068
2180
  }
2069
- );
2181
+ this.publishBackfillStatus();
2182
+ this.pumpBackfillQueue();
2183
+ });
2184
+ }
2185
+ runBackfill(agentName) {
2186
+ return this.scheduler.run(agentName, "backfill", () => this.performBackfill(agentName));
2070
2187
  }
2071
2188
  async performBackfill(agentName) {
2072
2189
  const startedAt = performance.now();
2073
2190
  const agent = this.findAgent(agentName);
2074
2191
  if (!agent || !agent.isAvailable()) return "skipped";
2075
- const snapshot = this.options.snapshot();
2192
+ const snapshot = this.sessionIndex.snapshot();
2076
2193
  const cached = loadCachedSessions(agentName);
2077
2194
  const baseline = cached?.sessions ?? snapshot.byAgent[agentName] ?? [];
2078
2195
  const meta = cached?.meta ?? buildAgentCacheMeta(agent);
@@ -2090,14 +2207,12 @@ var AgentSyncEngine = class {
2090
2207
  );
2091
2208
  agent.setSessionMetaMap(new Map(Object.entries(result.meta)));
2092
2209
  const fullSessions = attachMissingProjectIdentities(result.sessions);
2093
- const diff = buildRefreshDiff(
2210
+ await this.commitSessionPublication({
2211
+ context: "scan.backfill",
2094
2212
  agentName,
2095
- snapshot.byAgent[agentName] ?? [],
2096
- fullSessions,
2097
- result.changedIds ?? []
2098
- );
2099
- await this.searchIndexJobs.enqueue("scan.backfill", [
2100
- {
2213
+ sessions: fullSessions,
2214
+ candidateChangedIds: result.changedIds ?? [],
2215
+ indexJob: {
2101
2216
  kind: "full",
2102
2217
  context: "scan.backfill",
2103
2218
  agentName,
@@ -2105,9 +2220,8 @@ var AgentSyncEngine = class {
2105
2220
  meta: buildAgentCacheMeta(agent),
2106
2221
  saveCache: true
2107
2222
  }
2108
- ]);
2223
+ });
2109
2224
  markAgentFullSyncCompleted(agentName);
2110
- this.emitSessionsChanged({ agentName, sessions: fullSessions, event: diff.event });
2111
2225
  appLogger.info("scan.backfill.done", {
2112
2226
  agent: agentName,
2113
2227
  duration_ms: Math.round(performance.now() - startedAt),
@@ -2121,69 +2235,6 @@ var AgentSyncEngine = class {
2121
2235
  return "failed";
2122
2236
  }
2123
2237
  }
2124
- serialize(agentName, kind, operation) {
2125
- const previous = this.operationTails.get(agentName) ?? Promise.resolve();
2126
- const run = previous.then(async () => {
2127
- if (this.isShuttingDown) return "skipped";
2128
- const lifecycle = this.beginOperation(agentName, kind);
2129
- try {
2130
- const result = await operation();
2131
- this.completeOperation(lifecycle, result);
2132
- return result;
2133
- } catch (error) {
2134
- this.completeOperation(lifecycle, "failed");
2135
- throw error;
2136
- }
2137
- });
2138
- const tail = run.then(
2139
- () => void 0,
2140
- () => void 0
2141
- );
2142
- this.operationTails.set(agentName, tail);
2143
- void tail.finally(() => {
2144
- if (this.operationTails.get(agentName) === tail) this.operationTails.delete(agentName);
2145
- });
2146
- return run;
2147
- }
2148
- state(agentName) {
2149
- const existing = this.refreshStates.get(agentName);
2150
- if (existing) return existing;
2151
- const state = {
2152
- timer: null,
2153
- timerDeadline: 0,
2154
- isRunning: false,
2155
- hasPendingRerun: false,
2156
- lastRefreshAt: 0,
2157
- lastRefreshDurationMs: 0,
2158
- pendingPathCount: 0
2159
- };
2160
- this.refreshStates.set(agentName, state);
2161
- return state;
2162
- }
2163
- beginOperation(agentName, kind) {
2164
- const generation = (this.operationGenerations.get(agentName) ?? 0) + 1;
2165
- const startedAt = Date.now();
2166
- this.operationGenerations.set(agentName, generation);
2167
- appLogger.info("scan.agent_operation.started", {
2168
- agent: agentName,
2169
- operation: kind,
2170
- generation,
2171
- started_at: startedAt
2172
- });
2173
- return { agentName, kind, generation, startedAt };
2174
- }
2175
- completeOperation(lifecycle, result) {
2176
- const completedAt = Date.now();
2177
- appLogger.info("scan.agent_operation.completed", {
2178
- agent: lifecycle.agentName,
2179
- operation: lifecycle.kind,
2180
- generation: lifecycle.generation,
2181
- started_at: lifecycle.startedAt,
2182
- completed_at: completedAt,
2183
- duration_ms: completedAt - lifecycle.startedAt,
2184
- result
2185
- });
2186
- }
2187
2238
  backfillStatus() {
2188
2239
  return {
2189
2240
  active: this.currentBackfillAgent != null || this.backfillQueue.length > 0,
@@ -2194,7 +2245,7 @@ var AgentSyncEngine = class {
2194
2245
  };
2195
2246
  }
2196
2247
  buildFullSearchIndexJobs(context) {
2197
- const snapshot = this.options.snapshot();
2248
+ const snapshot = this.sessionIndex.snapshot();
2198
2249
  return snapshot.agents.map((agent) => {
2199
2250
  const cached = loadCachedSessions(agent.name);
2200
2251
  return cached ? {
@@ -2212,8 +2263,65 @@ var AgentSyncEngine = class {
2212
2263
  };
2213
2264
  });
2214
2265
  }
2266
+ publicationId(context, agentName) {
2267
+ const id = this.nextPublicationId++;
2268
+ return agentName ? `${context}:${agentName}:${id}` : `${context}:${id}`;
2269
+ }
2270
+ async commitSearchIndex(context, jobs, details) {
2271
+ appLogger.info("session.publication.prepared", {
2272
+ publication_id: details.publicationId,
2273
+ context,
2274
+ agent: details.agent,
2275
+ agents: details.agents,
2276
+ jobs: jobs.length
2277
+ });
2278
+ try {
2279
+ await this.searchIndexJobs.enqueue(context, jobs);
2280
+ } catch (error) {
2281
+ appLogger.error("session.publication.failed", {
2282
+ publication_id: details.publicationId,
2283
+ context,
2284
+ agent: details.agent,
2285
+ stage: "search_index",
2286
+ error
2287
+ });
2288
+ throw error;
2289
+ }
2290
+ appLogger.info("session.publication.index_committed", {
2291
+ publication_id: details.publicationId,
2292
+ context,
2293
+ agent: details.agent
2294
+ });
2295
+ }
2296
+ async commitSessionPublication(publication) {
2297
+ const publicationId = this.publicationId(publication.context, publication.agentName);
2298
+ await this.commitSearchIndex(publication.context, [publication.indexJob], {
2299
+ publicationId,
2300
+ agent: publication.agentName
2301
+ });
2302
+ const diffStartedAt = performance.now();
2303
+ const event = this.sessionIndex.commitAgentSessions(
2304
+ publication.agentName,
2305
+ publication.sessions,
2306
+ publication.candidateChangedIds
2307
+ );
2308
+ const diffDuration = performance.now() - diffStartedAt;
2309
+ this.emitSessionsChanged({
2310
+ agentName: publication.agentName,
2311
+ sessions: this.sessionIndex.snapshot().byAgent[publication.agentName] ?? [],
2312
+ event
2313
+ });
2314
+ appLogger.info("session.publication.published", {
2315
+ publication_id: publicationId,
2316
+ context: publication.context,
2317
+ agent: publication.agentName,
2318
+ sessions: publication.sessions.length,
2319
+ has_event: event != null
2320
+ });
2321
+ return { event, diffDuration };
2322
+ }
2215
2323
  findAgent(agentName) {
2216
- return this.options.snapshot().agents.find((agent) => agent.name === agentName);
2324
+ return this.sessionIndex.findAgent(agentName);
2217
2325
  }
2218
2326
  startupScanOptions() {
2219
2327
  return this.options.startupScanOptions ?? {};
@@ -2227,19 +2335,19 @@ var AgentSyncEngine = class {
2227
2335
  };
2228
2336
 
2229
2337
  // src/session-watcher.ts
2230
- import { existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, watch } from "fs";
2231
- import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
2338
+ import { existsSync as existsSync3, readdirSync, statSync, watch } from "fs";
2339
+ import { dirname as dirname2, isAbsolute, join, relative, resolve as resolve2 } from "path";
2232
2340
  var WRITE_STABILITY_THRESHOLD_MS = 250;
2233
2341
  var WRITE_STABILITY_POLL_MS = 100;
2234
2342
  function toAbsolutePath(path) {
2235
2343
  return isAbsolute(path) ? path : resolve2(path);
2236
2344
  }
2237
2345
  function closestWatchablePath(targetPath) {
2238
- if (!isAbsolute(targetPath) && !existsSync4(targetPath)) {
2346
+ if (!isAbsolute(targetPath) && !existsSync3(targetPath)) {
2239
2347
  return null;
2240
2348
  }
2241
2349
  let current = toAbsolutePath(targetPath);
2242
- while (!existsSync4(current)) {
2350
+ while (!existsSync3(current)) {
2243
2351
  const parent = dirname2(current);
2244
2352
  if (parent === current) {
2245
2353
  return null;
@@ -2249,7 +2357,7 @@ function closestWatchablePath(targetPath) {
2249
2357
  return current;
2250
2358
  }
2251
2359
  function getWatchRoot(path) {
2252
- const stat = statSync2(path);
2360
+ const stat = statSync(path);
2253
2361
  return stat.isDirectory() ? path : dirname2(path);
2254
2362
  }
2255
2363
  function isRecursiveWatchSupported(platform = process.platform, nodeVersion = process.versions.node) {
@@ -2286,50 +2394,7 @@ function resolveWatchEventPath(watchPath, filename) {
2286
2394
  if (!filenameText) {
2287
2395
  return watchPath;
2288
2396
  }
2289
- return isAbsolute(filenameText) ? filenameText : join2(watchPath, filenameText);
2290
- }
2291
- function resolveAgentWatchTargets(agentName) {
2292
- const roots = resolveProviderRoots();
2293
- const cursorDataPath = getCursorDataPath();
2294
- switch (agentName) {
2295
- case "claudecode":
2296
- return [
2297
- { root: roots.claudeRoot, path: join2(roots.claudeRoot, "projects") },
2298
- { path: "data/claudecode" }
2299
- ];
2300
- case "codex":
2301
- return [
2302
- { path: join2(roots.codexRoot, "sessions") },
2303
- { path: join2(roots.codexRoot, "session_index.jsonl") }
2304
- ];
2305
- case "pi":
2306
- return [
2307
- { root: roots.piRoot, path: join2(roots.piRoot, "agent", "sessions") },
2308
- { root: "data/pi", path: "data/pi" }
2309
- ];
2310
- case "cursor":
2311
- return cursorDataPath ? [
2312
- {
2313
- root: cursorDataPath,
2314
- path: join2(cursorDataPath, "globalStorage", "state.vscdb")
2315
- },
2316
- { root: cursorDataPath, path: join2(cursorDataPath, "workspaceStorage") }
2317
- ] : [];
2318
- case "kimi":
2319
- return [
2320
- { root: roots.kimiRoot, path: join2(roots.kimiRoot, "sessions") },
2321
- { path: "data/kimi" }
2322
- ];
2323
- case "opencode":
2324
- return [
2325
- { root: roots.opencodeRoot, path: join2(roots.opencodeRoot, "opencode.db") },
2326
- { root: "data/opencode", path: "data/opencode/opencode.db" }
2327
- ];
2328
- case "zcode":
2329
- return roots.zcodeRoot ? [{ root: roots.zcodeRoot, path: join2(roots.zcodeRoot, "cli", "db", "db.sqlite") }] : [];
2330
- default:
2331
- return [];
2332
- }
2397
+ return isAbsolute(filenameText) ? filenameText : join(watchPath, filenameText);
2333
2398
  }
2334
2399
  var SessionWatcher = class {
2335
2400
  watchers = [];
@@ -2343,16 +2408,24 @@ var SessionWatcher = class {
2343
2408
  this.listeners.delete(cb);
2344
2409
  };
2345
2410
  }
2346
- /** Begin watching the given agent names' data directories. */
2347
- start(agentNames) {
2411
+ /** Begin watching the session sources declared by each agent adapter. */
2412
+ start(agents) {
2348
2413
  const scopesByRoot = /* @__PURE__ */ new Map();
2349
- for (const agentName of agentNames) {
2350
- const watchTargets = resolveAgentWatchTargets(agentName);
2351
- if (watchTargets.length === 0) {
2352
- appLogger.debug("watch.skip", { agent: agentName });
2414
+ for (const agent of agents) {
2415
+ const plan = agent.getSessionWatchPlan();
2416
+ if (plan.status !== "supported") {
2417
+ appLogger.debug("watch.skip", {
2418
+ agent: agent.name,
2419
+ status: plan.status,
2420
+ reason: plan.reason
2421
+ });
2422
+ continue;
2423
+ }
2424
+ if (plan.targets.length === 0) {
2425
+ appLogger.debug("watch.skip", { agent: agent.name, status: plan.status });
2353
2426
  continue;
2354
2427
  }
2355
- for (const target of watchTargets) {
2428
+ for (const target of plan.targets) {
2356
2429
  const watchRootPath = closestWatchablePath(target.root ?? target.path);
2357
2430
  if (!watchRootPath) continue;
2358
2431
  let rootPath;
@@ -2364,17 +2437,17 @@ var SessionWatcher = class {
2364
2437
  }
2365
2438
  const targetPath = toAbsolutePath(target.path);
2366
2439
  const scopes = scopesByRoot.get(rootPath) ?? [];
2367
- if (!scopes.some((scope) => scope.agentName === agentName && scope.targetPath === targetPath)) {
2368
- scopes.push({ agentName, targetPath });
2440
+ if (!scopes.some((scope) => scope.agentName === agent.name && scope.targetPath === targetPath)) {
2441
+ scopes.push({ agentName: agent.name, targetPath });
2369
2442
  }
2370
2443
  scopesByRoot.set(rootPath, scopes);
2371
2444
  }
2372
2445
  }
2373
2446
  for (const [rootPath, scopes] of scopesByRoot.entries()) {
2374
- const agents = Array.from(new Set(scopes.map((scope) => scope.agentName)));
2447
+ const agents2 = Array.from(new Set(scopes.map((scope) => scope.agentName)));
2375
2448
  appLogger.info("watch.start", {
2376
2449
  root: rootPath,
2377
- agents,
2450
+ agents: agents2,
2378
2451
  targets: scopes.map((scope) => ({
2379
2452
  agent: scope.agentName,
2380
2453
  path: scope.targetPath
@@ -2437,9 +2510,9 @@ var SessionWatcher = class {
2437
2510
  const dirPath = pending.pop();
2438
2511
  this.watchFallbackDirectory(dirPath, scopes);
2439
2512
  try {
2440
- for (const entry of readdirSync2(dirPath, { withFileTypes: true })) {
2513
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
2441
2514
  if (entry.isDirectory()) {
2442
- pending.push(join2(dirPath, entry.name));
2515
+ pending.push(join(dirPath, entry.name));
2443
2516
  }
2444
2517
  }
2445
2518
  } catch (error) {
@@ -2462,7 +2535,7 @@ var SessionWatcher = class {
2462
2535
  watchNewDirectories(watchPath, filename, scopes) {
2463
2536
  const path = resolveWatchEventPath(watchPath, filename);
2464
2537
  try {
2465
- if (statSync2(path).isDirectory()) {
2538
+ if (statSync(path).isDirectory()) {
2466
2539
  this.watchDirectoryTree(path, scopes);
2467
2540
  }
2468
2541
  } catch {
@@ -2510,7 +2583,7 @@ var SessionWatcher = class {
2510
2583
  let size;
2511
2584
  let mtimeMs;
2512
2585
  try {
2513
- const stat = statSync2(path);
2586
+ const stat = statSync(path);
2514
2587
  size = stat.size;
2515
2588
  mtimeMs = stat.mtimeMs;
2516
2589
  } catch {
@@ -2546,71 +2619,154 @@ var SessionWatcher = class {
2546
2619
 
2547
2620
  // src/worker-runner.ts
2548
2621
  import { Worker as Worker2 } from "worker_threads";
2622
+ var SHUTDOWN_ERROR_MESSAGE2 = "Scan refresh worker shut down";
2623
+ function applySessionChanges2(previousSessions, changes, removedSessionIds) {
2624
+ const replacedIds = new Set(removedSessionIds);
2625
+ for (const { session } of changes) replacedIds.add(session.id);
2626
+ const retained = previousSessions.filter((session) => !replacedIds.has(session.id));
2627
+ const next = Array.from({
2628
+ length: retained.length + changes.length
2629
+ });
2630
+ for (const { session, sortIndex } of changes) {
2631
+ if (sortIndex < 0 || sortIndex >= next.length || next[sortIndex]) {
2632
+ throw new Error(`Invalid scan refresh sort index: ${sortIndex}`);
2633
+ }
2634
+ next[sortIndex] = session;
2635
+ }
2636
+ let retainedIndex = 0;
2637
+ for (let index = 0; index < next.length; index += 1) {
2638
+ if (!next[index]) next[index] = retained[retainedIndex++];
2639
+ }
2640
+ if (retainedIndex !== retained.length || next.some((session) => !session)) {
2641
+ throw new Error("Invalid scan refresh delta");
2642
+ }
2643
+ return next;
2644
+ }
2645
+ function applyMetaChanges(previous, changed, replacedSessionIds) {
2646
+ const next = { ...previous };
2647
+ for (const id of replacedSessionIds) delete next[id];
2648
+ return Object.assign(next, changed);
2649
+ }
2549
2650
  var ThreadWorkerRunner = class {
2550
2651
  constructor(workerUrl) {
2551
2652
  this.workerUrl = workerUrl;
2552
2653
  }
2553
2654
  workerUrl;
2554
- workers = /* @__PURE__ */ new Set();
2655
+ workers = /* @__PURE__ */ new Map();
2656
+ nextRequestId = 1;
2657
+ isShuttingDown = false;
2555
2658
  get activeCount() {
2556
- return this.workers.size;
2659
+ let count = 0;
2660
+ for (const slot of this.workers.values()) count += slot.pending.size;
2661
+ return count;
2557
2662
  }
2558
2663
  run(agentName, payload) {
2664
+ if (this.isShuttingDown) return Promise.reject(new Error(SHUTDOWN_ERROR_MESSAGE2));
2665
+ const request = {
2666
+ type: "run",
2667
+ requestId: this.nextRequestId++,
2668
+ agentName,
2669
+ previousSessions: payload.previousSessions,
2670
+ changedIds: payload.changedIds,
2671
+ sourceSync: payload.sourceSync,
2672
+ scanOptions: payload.scanOptions,
2673
+ meta: payload.meta
2674
+ };
2559
2675
  return new Promise((resolve4, reject) => {
2560
- const worker = new Worker2(this.workerUrl, {
2561
- workerData: {
2562
- agentName,
2563
- previousSessions: payload.previousSessions,
2564
- changedIds: payload.changedIds,
2565
- sourceSync: payload.sourceSync,
2566
- scanOptions: payload.scanOptions,
2567
- meta: payload.meta
2568
- }
2569
- });
2570
- worker.unref();
2571
- this.workers.add(worker);
2572
- let settled = false;
2573
- const finish = (callback, terminate = true) => {
2574
- if (settled) return;
2575
- settled = true;
2576
- this.workers.delete(worker);
2577
- if (terminate) void worker.terminate();
2578
- callback();
2579
- };
2580
- worker.on("message", (message) => {
2581
- if (message.type === "progress") {
2582
- payload.onProgress?.(message.progress);
2583
- return;
2584
- }
2585
- if (message.type === "done") {
2586
- finish(
2587
- () => resolve4({
2588
- sessions: message.sessions,
2589
- meta: message.meta,
2590
- changedIds: message.changedIds
2591
- })
2592
- );
2676
+ let slot = this.workers.get(agentName);
2677
+ const isNewWorker = !slot;
2678
+ if (!slot) {
2679
+ try {
2680
+ slot = this.createWorker(agentName, request);
2681
+ } catch (error) {
2682
+ reject(error instanceof Error ? error : new Error(String(error)));
2593
2683
  return;
2594
2684
  }
2595
- finish(() => reject(new Error(message.error)));
2596
- });
2597
- worker.once("error", (error) => {
2598
- finish(() => reject(error));
2599
- });
2600
- worker.once("exit", (code) => {
2601
- if (settled) return;
2602
- appLogger.warn("scan.refresh_worker.exit_before_done", { agent: agentName, code });
2603
- finish(
2604
- () => reject(new Error(`Scan refresh worker exited before completing (code ${code})`)),
2605
- false
2606
- );
2685
+ }
2686
+ slot.pending.set(request.requestId, {
2687
+ resolve: resolve4,
2688
+ reject,
2689
+ payload,
2690
+ onProgress: payload.onProgress
2607
2691
  });
2692
+ if (!isNewWorker) {
2693
+ try {
2694
+ slot.worker.postMessage(request);
2695
+ } catch (error) {
2696
+ slot.pending.delete(request.requestId);
2697
+ reject(error instanceof Error ? error : new Error(String(error)));
2698
+ }
2699
+ }
2608
2700
  });
2609
2701
  }
2610
2702
  async shutdown() {
2611
- const workers = [...this.workers];
2612
- await Promise.allSettled(workers.map((worker) => worker.terminate()));
2703
+ this.isShuttingDown = true;
2704
+ const slots = [...this.workers.values()];
2613
2705
  this.workers.clear();
2706
+ const shutdownError = new Error(SHUTDOWN_ERROR_MESSAGE2);
2707
+ for (const slot of slots) {
2708
+ slot.closed = true;
2709
+ for (const pending of slot.pending.values()) pending.reject(shutdownError);
2710
+ slot.pending.clear();
2711
+ }
2712
+ await Promise.allSettled(slots.map((slot) => slot.worker.terminate()));
2713
+ }
2714
+ createWorker(agentName, request) {
2715
+ const worker = new Worker2(this.workerUrl, { workerData: request });
2716
+ const slot = { worker, pending: /* @__PURE__ */ new Map(), closed: false };
2717
+ worker.unref();
2718
+ this.workers.set(agentName, slot);
2719
+ worker.on("message", (message) => {
2720
+ this.handleMessage(slot, message);
2721
+ });
2722
+ worker.on("error", (error) => {
2723
+ this.closeWorker(agentName, slot, error);
2724
+ });
2725
+ worker.on("exit", (code) => {
2726
+ if (slot.closed) return;
2727
+ const error = new Error(`Scan refresh worker exited before completing (code ${code})`);
2728
+ if (slot.pending.size > 0) {
2729
+ appLogger.warn("scan.refresh_worker.exit_before_done", { agent: agentName, code });
2730
+ }
2731
+ this.closeWorker(agentName, slot, error);
2732
+ });
2733
+ return slot;
2734
+ }
2735
+ handleMessage(slot, message) {
2736
+ const pending = slot.pending.get(message.requestId);
2737
+ if (!pending) return;
2738
+ if (message.type === "progress") {
2739
+ pending.onProgress?.(message.progress);
2740
+ return;
2741
+ }
2742
+ slot.pending.delete(message.requestId);
2743
+ if (message.type === "error") {
2744
+ pending.reject(new Error(message.error));
2745
+ return;
2746
+ }
2747
+ const changedIds = message.changes.map(({ session }) => session.id);
2748
+ const replacedSessionIds = [...changedIds, ...message.removedSessionIds];
2749
+ const removedMetaIds = [...message.removedSessionIds, ...message.removedMetaIds];
2750
+ try {
2751
+ pending.resolve({
2752
+ sessions: applySessionChanges2(
2753
+ pending.payload.previousSessions,
2754
+ message.changes,
2755
+ message.removedSessionIds
2756
+ ),
2757
+ meta: applyMetaChanges(pending.payload.meta, message.meta, removedMetaIds),
2758
+ changedIds: pending.payload.sourceSync ? replacedSessionIds : void 0
2759
+ });
2760
+ } catch (error) {
2761
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
2762
+ }
2763
+ }
2764
+ closeWorker(agentName, slot, error) {
2765
+ if (slot.closed) return;
2766
+ slot.closed = true;
2767
+ if (this.workers.get(agentName) === slot) this.workers.delete(agentName);
2768
+ for (const pending of slot.pending.values()) pending.reject(error);
2769
+ slot.pending.clear();
2614
2770
  }
2615
2771
  };
2616
2772
 
@@ -2621,7 +2777,7 @@ function mergeEvents(previous, next) {
2621
2777
  const removedSessionRefs = /* @__PURE__ */ new Map();
2622
2778
  const sessionKey = (agentName, sessionId) => `${agentName}\0${sessionId}`;
2623
2779
  const addChanged = (item) => {
2624
- const key = sessionKey(item.agentName, item.session.id);
2780
+ const key = sessionKey(item.reference.agentName, item.reference.sessionId);
2625
2781
  removedSessionRefs.delete(key);
2626
2782
  changedSessionHeads.set(key, item);
2627
2783
  };
@@ -2652,9 +2808,6 @@ var LiveScanStore = class {
2652
2808
  startupScanOptions;
2653
2809
  deferInitialRefresh;
2654
2810
  syncEngine;
2655
- agents = [];
2656
- byAgent = {};
2657
- sessions = [];
2658
2811
  listeners = /* @__PURE__ */ new Set();
2659
2812
  watcher = null;
2660
2813
  pendingEvent = null;
@@ -2668,11 +2821,12 @@ var LiveScanStore = class {
2668
2821
  this.deferInitialRefresh = options.deferInitialRefresh === true;
2669
2822
  const workerRunner = options.workerRunner ?? new ThreadWorkerRunner(new URL("./scan-refresh-worker.js", import.meta.url));
2670
2823
  this.syncEngine = new AgentSyncEngine({
2671
- snapshot: () => this.getSnapshot(),
2672
2824
  startupScanOptions: this.startupScanOptions,
2673
2825
  workerRunner
2674
2826
  });
2675
- this.syncEngine.subscribeSessionsChanged((change) => this.applySessionsChanged(change));
2827
+ this.syncEngine.subscribeSessionsChanged((change) => {
2828
+ if (change.event) this.emit(change.event);
2829
+ });
2676
2830
  }
2677
2831
  async initialize() {
2678
2832
  const startedAt = performance.now();
@@ -2693,8 +2847,12 @@ var LiveScanStore = class {
2693
2847
  smartTagWorkerUrl: this.getSmartTagWorkerUrl() ?? void 0,
2694
2848
  includeSmartTags: this.deferInitialRefresh ? false : void 0
2695
2849
  });
2696
- this.applyScanResult(initialResult);
2697
- this.syncEngine.initialize(initialResult.cacheTimestamps);
2850
+ this.syncEngine.initialize(initialResult, {
2851
+ cacheTimestamps: initialResult.cacheTimestamps,
2852
+ registeredAgents: createRegisteredAgents(),
2853
+ allowedAgents: this.getAllowedAgents()
2854
+ });
2855
+ const snapshot = this.getSnapshot();
2698
2856
  const indexStartedAt = performance.now();
2699
2857
  if (!this.deferInitialRefresh) await this.syncEngine.syncInitialIndex();
2700
2858
  const indexDuration = performance.now() - indexStartedAt;
@@ -2702,9 +2860,9 @@ var LiveScanStore = class {
2702
2860
  duration_ms: Math.round(performance.now() - startedAt),
2703
2861
  index_ms: this.deferInitialRefresh ? void 0 : Math.round(indexDuration),
2704
2862
  deferred: this.deferInitialRefresh || void 0,
2705
- sessions: this.sessions.length,
2863
+ sessions: snapshot.sessions.length,
2706
2864
  agents: Object.fromEntries(
2707
- Object.entries(this.byAgent).map(([key, value]) => [key, value.length])
2865
+ Object.entries(snapshot.byAgent).map(([key, value]) => [key, value.length])
2708
2866
  ),
2709
2867
  agent_timings: initialResult.timings ? Object.fromEntries(
2710
2868
  Object.entries(initialResult.timings).map(([name, timing]) => [
@@ -2723,13 +2881,13 @@ var LiveScanStore = class {
2723
2881
  if (!this.watchEnabled) return;
2724
2882
  this.watcher = new SessionWatcher();
2725
2883
  this.watcher.onAgentsChanged((agentNames) => this.syncEngine.handleAgentsChanged(agentNames));
2726
- this.watcher.start(this.agents.map((agent) => agent.name));
2884
+ this.watcher.start(snapshot.agents);
2727
2885
  }
2728
2886
  startBackgroundRefresh() {
2729
2887
  this.syncEngine.startBackgroundRefresh();
2730
2888
  }
2731
2889
  getSnapshot() {
2732
- return { sessions: this.sessions, byAgent: this.byAgent, agents: this.agents };
2890
+ return this.syncEngine.snapshot();
2733
2891
  }
2734
2892
  getScanStatus() {
2735
2893
  return this.syncEngine.status();
@@ -2758,13 +2916,6 @@ var LiveScanStore = class {
2758
2916
  this.watcher = null;
2759
2917
  }
2760
2918
  }
2761
- applySessionsChanged(change) {
2762
- this.byAgent[change.agentName] = sortSessions(change.sessions);
2763
- this.rebuildSessions();
2764
- if (!change.event) return;
2765
- change.event.totalSessions = this.sessions.length;
2766
- this.emit(change.event);
2767
- }
2768
2919
  emit(event) {
2769
2920
  if (this.shuttingDown) return;
2770
2921
  if (this.pendingEvent || event.newSessions > 0) {
@@ -2786,29 +2937,11 @@ var LiveScanStore = class {
2786
2937
  if (pending) this.emitNow(pending);
2787
2938
  }, NEW_SESSION_EVENT_WINDOW_MS);
2788
2939
  }
2789
- rebuildSessions() {
2790
- this.sessions = sortSessions(Object.values(this.byAgent).flat());
2791
- }
2792
2940
  getSmartTagWorkerUrl() {
2793
2941
  const workerUrl = new URL("./smart-tag-worker.js", import.meta.url);
2794
- if (workerUrl.protocol === "file:" && !existsSync5(fileURLToPath3(workerUrl))) return null;
2942
+ if (workerUrl.protocol === "file:" && !existsSync4(fileURLToPath3(workerUrl))) return null;
2795
2943
  return workerUrl;
2796
2944
  }
2797
- applyScanResult(result) {
2798
- const agentMap = /* @__PURE__ */ new Map();
2799
- const allowedAgents = this.getAllowedAgents();
2800
- for (const agent of result.agents) agentMap.set(agent.name, agent);
2801
- for (const agent of createRegisteredAgents()) {
2802
- if (!agentMap.has(agent.name)) agentMap.set(agent.name, agent);
2803
- }
2804
- this.agents = [...agentMap.values()].filter(
2805
- (agent) => !allowedAgents || allowedAgents.has(agent.name.toLowerCase())
2806
- );
2807
- this.byAgent = Object.fromEntries(
2808
- this.agents.map((agent) => [agent.name, sortSessions(result.byAgent[agent.name] ?? [])])
2809
- );
2810
- this.rebuildSessions();
2811
- }
2812
2945
  getAllowedAgents() {
2813
2946
  if (!this.scanOptions.agents?.length) return null;
2814
2947
  return new Set(this.scanOptions.agents.map((agent) => agent.toLowerCase()));
@@ -2845,29 +2978,32 @@ function printScanResults(agents) {
2845
2978
  consola.log("");
2846
2979
  }
2847
2980
 
2848
- // src/ports.ts
2849
- var DEFAULT_PORT = 4521;
2850
- var DEFAULT_PORT_FALLBACK_ATTEMPTS = 20;
2851
- function parsePort(value) {
2852
- const port = parseInt(value ?? "", 10);
2853
- return Number.isNaN(port) ? DEFAULT_PORT : port;
2854
- }
2855
- function hasExplicitPortArg(argv) {
2856
- return argv.some((arg, index) => {
2857
- if (arg === "--port" || arg === "-p") return index < argv.length - 1;
2858
- return arg.startsWith("--port=") || /^-p\d+$/.test(arg);
2859
- });
2860
- }
2861
-
2862
- // src/index.ts
2981
+ // src/runtime-plan.ts
2863
2982
  function parseSessionUri(uri) {
2864
2983
  const match = uri.match(/^([a-z]+):\/\/(.+)$/i);
2865
2984
  if (!match) return null;
2866
2985
  return { agent: match[1], sessionId: match[2] };
2867
2986
  }
2868
- function appendStartupPath(startupUrl, path) {
2987
+ function buildCliRuntimePlan(input, environment) {
2988
+ const listWindow = resolveTimeWindow({
2989
+ mode: "cli",
2990
+ from: input.from,
2991
+ to: input.to,
2992
+ days: input.days,
2993
+ now: environment.now
2994
+ });
2995
+ const cwd = input.cwd === "." ? environment.currentWorkingDirectory : input.cwd;
2996
+ const agents = input.targetSession ? [input.targetSession.agent] : input.agent ? input.agent.split(",").map((agent) => agent.trim()) : void 0;
2997
+ return {
2998
+ listWindow,
2999
+ scanOptions: { agents, cwd, useCache: input.useCache },
3000
+ startupScanOptions: input.targetSession || input.jsonOnly ? {} : { from: listWindow.from, to: listWindow.to }
3001
+ };
3002
+ }
3003
+ function resolveStartupUrl(startupUrl, targetSession) {
3004
+ if (!targetSession) return startupUrl;
2869
3005
  const url = new URL(startupUrl);
2870
- url.pathname = path;
3006
+ url.pathname = `/${targetSession.agent.toLowerCase()}/${targetSession.sessionId}`;
2871
3007
  return url.toString();
2872
3008
  }
2873
3009
  function redactStartupUrl(startupUrl) {
@@ -2877,6 +3013,22 @@ function redactStartupUrl(startupUrl) {
2877
3013
  }
2878
3014
  return url.toString();
2879
3015
  }
3016
+
3017
+ // src/ports.ts
3018
+ var DEFAULT_PORT = 4521;
3019
+ var DEFAULT_PORT_FALLBACK_ATTEMPTS = 20;
3020
+ function parsePort(value) {
3021
+ const port = parseInt(value ?? "", 10);
3022
+ return Number.isNaN(port) ? DEFAULT_PORT : port;
3023
+ }
3024
+ function hasExplicitPortArg(argv) {
3025
+ return argv.some((arg, index) => {
3026
+ if (arg === "--port" || arg === "-p") return index < argv.length - 1;
3027
+ return arg.startsWith("--port=") || /^-p\d+$/.test(arg);
3028
+ });
3029
+ }
3030
+
3031
+ // src/index.ts
2880
3032
  var main = defineCommand({
2881
3033
  meta: {
2882
3034
  name: "codesesh",
@@ -2985,7 +3137,7 @@ var main = defineCommand({
2985
3137
  log_path: appLogger.getLogPath()
2986
3138
  });
2987
3139
  if (clearCache) {
2988
- const { clearCache: clear } = await import("./dist-5356XOFP.js");
3140
+ const { clearCache: clear } = await import("./dist-KEPJFHOC.js");
2989
3141
  clear();
2990
3142
  appLogger.info("cache.clear");
2991
3143
  console.log("Cache cleared.");
@@ -2999,26 +3151,20 @@ var main = defineCommand({
2999
3151
  process.exit(1);
3000
3152
  }
3001
3153
  }
3002
- let cwdFilter = args.cwd;
3003
- if (cwdFilter === ".") {
3004
- cwdFilter = process.cwd();
3005
- }
3006
- const {
3007
- from: listDefaultFrom,
3008
- to: listDefaultTo,
3009
- days: listDefaultDays
3010
- } = resolveTimeWindow({
3011
- mode: "cli",
3012
- from: args.from,
3013
- to: args.to,
3014
- days: args.days
3015
- });
3016
- const scanOptions = {
3017
- agents: targetSession ? [targetSession.agent] : args.agent ? args.agent.split(",").map((a) => a.trim()) : void 0,
3018
- cwd: cwdFilter,
3019
- useCache
3020
- };
3021
- const startupScanOptions = targetSession || jsonOnly ? {} : { from: listDefaultFrom, to: listDefaultTo };
3154
+ const { listWindow, scanOptions, startupScanOptions } = buildCliRuntimePlan(
3155
+ {
3156
+ agent: args.agent,
3157
+ cwd: args.cwd,
3158
+ from: args.from,
3159
+ to: args.to,
3160
+ days: args.days,
3161
+ jsonOnly,
3162
+ targetSession,
3163
+ useCache
3164
+ },
3165
+ { currentWorkingDirectory: process.cwd() }
3166
+ );
3167
+ const { from: listDefaultFrom, to: listDefaultTo, days: listDefaultDays } = listWindow;
3022
3168
  const store = new LiveScanStore({
3023
3169
  watchEnabled: !jsonOnly,
3024
3170
  scanOptions,
@@ -3108,7 +3254,7 @@ var main = defineCommand({
3108
3254
  });
3109
3255
  if (!noOpen) {
3110
3256
  const open = (await import("open")).default;
3111
- const targetUrl = targetSession ? appendStartupPath(url, `/${targetSession.agent.toLowerCase()}/${targetSession.sessionId}`) : url;
3257
+ const targetUrl = resolveStartupUrl(url, targetSession);
3112
3258
  appLogger.info("browser.open", { url: redactStartupUrl(targetUrl) });
3113
3259
  await open(targetUrl);
3114
3260
  }