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