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