bosun 0.42.0 → 0.42.2
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/.env.example +12 -0
- package/README.md +2 -0
- package/agent/agent-pool.mjs +34 -1
- package/agent/agent-work-report.mjs +89 -3
- package/agent/analyze-agent-work-helpers.mjs +14 -0
- package/agent/analyze-agent-work.mjs +23 -3
- package/agent/primary-agent.mjs +23 -1
- package/bosun-tui.mjs +4 -3
- package/bosun.schema.json +1 -1
- package/config/config.mjs +58 -0
- package/config/workspace-health.mjs +36 -6
- package/git/diff-stats.mjs +550 -124
- package/github/github-app-auth.mjs +9 -5
- package/infra/maintenance.mjs +13 -6
- package/infra/monitor.mjs +398 -10
- package/infra/runtime-accumulator.mjs +9 -1
- package/infra/session-tracker.mjs +163 -1
- package/infra/tui-bridge.mjs +415 -0
- package/infra/worktree-recovery-state.mjs +159 -0
- package/kanban/kanban-adapter.mjs +41 -8
- package/lib/repo-map.mjs +411 -0
- package/package.json +140 -137
- package/server/ui-server.mjs +953 -59
- package/shell/codex-config.mjs +34 -8
- package/task/task-cli.mjs +93 -19
- package/task/task-executor.mjs +397 -8
- package/task/task-store.mjs +194 -1
- package/telegram/telegram-bot.mjs +267 -18
- package/tools/vitest-runner.mjs +108 -0
- package/tui/app.mjs +252 -148
- package/tui/components/status-header.mjs +88 -131
- package/tui/lib/ws-bridge.mjs +125 -35
- package/tui/screens/agents-screen-helpers.mjs +219 -0
- package/tui/screens/agents.mjs +287 -270
- package/tui/screens/status.mjs +51 -189
- package/tui/screens/tasks.mjs +41 -253
- package/ui/app.js +52 -23
- package/ui/components/chat-view.js +263 -84
- package/ui/components/diff-viewer.js +324 -140
- package/ui/components/kanban-board.js +13 -9
- package/ui/components/session-list.js +111 -41
- package/ui/demo-defaults.js +481 -59
- package/ui/demo.html +32 -0
- package/ui/modules/session-api.js +320 -5
- package/ui/modules/stream-timeline.js +356 -0
- package/ui/modules/telegram.js +5 -2
- package/ui/modules/worktree-recovery.js +85 -0
- package/ui/styles.css +44 -0
- package/ui/tabs/chat.js +19 -4
- package/ui/tabs/dashboard.js +22 -0
- package/ui/tabs/infra.js +25 -0
- package/ui/tabs/tasks.js +119 -11
- package/voice/voice-auth-manager.mjs +10 -5
- package/workflow/workflow-engine.mjs +179 -1
- package/workflow/workflow-nodes.mjs +872 -16
- package/workflow/workflow-templates.mjs +4 -0
- package/workflow-templates/github.mjs +2 -1
- package/workflow-templates/planning.mjs +2 -1
- package/workflow-templates/sub-workflows.mjs +10 -0
- package/workflow-templates/task-batch.mjs +9 -8
- package/workflow-templates/task-execution.mjs +30 -12
- package/workflow-templates/task-lifecycle.mjs +59 -4
- package/workspace/shared-knowledge.mjs +409 -155
package/server/ui-server.mjs
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
} from "../kanban/kanban-adapter.mjs";
|
|
45
45
|
|
|
46
46
|
import {
|
|
47
|
+
addActiveSessionListener,
|
|
47
48
|
getActiveThreads,
|
|
48
49
|
launchEphemeralThread,
|
|
49
50
|
launchOrResumeThread,
|
|
@@ -132,6 +133,10 @@ import {
|
|
|
132
133
|
listActiveInstances,
|
|
133
134
|
selectCoordinator,
|
|
134
135
|
} from "../infra/presence.mjs";
|
|
136
|
+
import {
|
|
137
|
+
normalizeWorktreeRecoveryState,
|
|
138
|
+
readWorktreeRecoveryState,
|
|
139
|
+
} from "../infra/worktree-recovery-state.mjs";
|
|
135
140
|
import {
|
|
136
141
|
loadWorkspaceRegistry,
|
|
137
142
|
getLocalWorkspace,
|
|
@@ -155,10 +160,12 @@ import {
|
|
|
155
160
|
import {
|
|
156
161
|
getSessionTracker,
|
|
157
162
|
addSessionEventListener,
|
|
163
|
+
addSessionStateListener,
|
|
158
164
|
} from "../infra/session-tracker.mjs";
|
|
159
165
|
import { ensureTestRuntimeSandbox } from "../infra/test-runtime.mjs";
|
|
160
166
|
import {
|
|
161
167
|
addSessionAccumulationListener,
|
|
168
|
+
exportRuntimeData,
|
|
162
169
|
getCompletedSessions,
|
|
163
170
|
getRuntimeStats,
|
|
164
171
|
getTaskLifetimeTotals,
|
|
@@ -206,6 +213,17 @@ import {
|
|
|
206
213
|
mergeTaskAttachments,
|
|
207
214
|
} from "../task/task-attachments.mjs";
|
|
208
215
|
import { getVisionSessionState } from "../voice/vision-session-state.mjs";
|
|
216
|
+
import {
|
|
217
|
+
buildLogStreamPayload,
|
|
218
|
+
buildMonitorStatsPayload,
|
|
219
|
+
buildSessionEventPayload,
|
|
220
|
+
buildSessionsUpdatePayload,
|
|
221
|
+
buildTasksUpdatePayload,
|
|
222
|
+
buildWorkflowStatusPayload,
|
|
223
|
+
createTuiStatsEmitter,
|
|
224
|
+
persistCompatibleTuiAuthToken,
|
|
225
|
+
resolveTuiAuthToken,
|
|
226
|
+
} from "../infra/tui-bridge.mjs";
|
|
209
227
|
|
|
210
228
|
const TASK_STORE_MODULE_PATH = "../task/task-store.mjs";
|
|
211
229
|
const TASK_STORE_START_GUARD_EXPORTS = [
|
|
@@ -228,6 +246,10 @@ const TASK_STORE_DAG_EXPORTS = Object.freeze({
|
|
|
228
246
|
});
|
|
229
247
|
const TASK_STORE_GET_TASK_EXPORTS = ["getTaskById", "getTask"];
|
|
230
248
|
const TASK_STORE_COMMENT_EXPORTS = ["getTaskComments", "listTaskComments"];
|
|
249
|
+
const TASK_STORE_RUN_EXPORTS = {
|
|
250
|
+
list: ["getTaskRuns", "listTaskRuns"],
|
|
251
|
+
append: ["appendTaskRun", "addTaskRun"],
|
|
252
|
+
};
|
|
231
253
|
const TASK_STORE_DEPENDENCY_EXPORTS = {
|
|
232
254
|
add: ["addTaskDependency"],
|
|
233
255
|
remove: ["removeTaskDependency"],
|
|
@@ -2169,6 +2191,261 @@ function resolvePrimaryWorkspaceId() {
|
|
|
2169
2191
|
}
|
|
2170
2192
|
}
|
|
2171
2193
|
|
|
2194
|
+
function normalizeDiffTaskRef(value) {
|
|
2195
|
+
return String(value || "").trim();
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
function collectTaskWorkflowRunEntries(task) {
|
|
2199
|
+
return [
|
|
2200
|
+
...(Array.isArray(task?.workflowRuns) ? task.workflowRuns : []),
|
|
2201
|
+
...(Array.isArray(task?.workflowHistory) ? task.workflowHistory : []),
|
|
2202
|
+
...(Array.isArray(task?.workflows) ? task.workflows : []),
|
|
2203
|
+
...(Array.isArray(task?.meta?.workflowRuns) ? task.meta.workflowRuns : []),
|
|
2204
|
+
];
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
function collectTaskDiffSessionIds(task) {
|
|
2208
|
+
const ids = new Set();
|
|
2209
|
+
const push = (value) => {
|
|
2210
|
+
const normalized = normalizeDiffTaskRef(value);
|
|
2211
|
+
if (normalized) ids.add(normalized);
|
|
2212
|
+
};
|
|
2213
|
+
|
|
2214
|
+
push(task?.sessionId);
|
|
2215
|
+
push(task?.primarySessionId);
|
|
2216
|
+
push(task?.meta?.sessionId);
|
|
2217
|
+
push(task?.meta?.primarySessionId);
|
|
2218
|
+
|
|
2219
|
+
for (const entry of collectTaskWorkflowRunEntries(task)) {
|
|
2220
|
+
push(entry?.sessionId);
|
|
2221
|
+
push(entry?.primarySessionId);
|
|
2222
|
+
push(entry?.threadId);
|
|
2223
|
+
push(entry?.agentSessionId);
|
|
2224
|
+
push(entry?.meta?.sessionId);
|
|
2225
|
+
push(entry?.meta?.threadId);
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
return [...ids];
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
function collectTaskDiffCommitRefs(task) {
|
|
2232
|
+
const refs = new Set();
|
|
2233
|
+
const push = (value) => {
|
|
2234
|
+
const normalized = normalizeDiffTaskRef(value);
|
|
2235
|
+
if (normalized) refs.add(normalized);
|
|
2236
|
+
};
|
|
2237
|
+
|
|
2238
|
+
push(task?.commitSha);
|
|
2239
|
+
push(task?.sha);
|
|
2240
|
+
push(task?.headSha);
|
|
2241
|
+
push(task?.mergeCommitSha);
|
|
2242
|
+
push(task?.meta?.commitSha);
|
|
2243
|
+
push(task?.meta?.sha);
|
|
2244
|
+
push(task?.meta?.headSha);
|
|
2245
|
+
push(task?.meta?.mergeCommitSha);
|
|
2246
|
+
push(task?.meta?.pr?.headSha);
|
|
2247
|
+
push(task?.meta?.pr?.mergeCommitSha);
|
|
2248
|
+
|
|
2249
|
+
return [...refs];
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
function pickTaskDiffBranch(task) {
|
|
2253
|
+
return (
|
|
2254
|
+
normalizeDiffTaskRef(task?.branchName) ||
|
|
2255
|
+
normalizeDiffTaskRef(task?.branch) ||
|
|
2256
|
+
normalizeDiffTaskRef(task?.meta?.branchName) ||
|
|
2257
|
+
normalizeDiffTaskRef(task?.meta?.branch) ||
|
|
2258
|
+
normalizeDiffTaskRef(task?.meta?.pr?.headRefName) ||
|
|
2259
|
+
""
|
|
2260
|
+
);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
function pickTaskDiffBaseBranch(task) {
|
|
2264
|
+
return (
|
|
2265
|
+
normalizeDiffTaskRef(task?.baseBranch) ||
|
|
2266
|
+
normalizeDiffTaskRef(task?.base_branch) ||
|
|
2267
|
+
normalizeDiffTaskRef(task?.meta?.baseBranch) ||
|
|
2268
|
+
normalizeDiffTaskRef(task?.meta?.base_branch) ||
|
|
2269
|
+
normalizeDiffTaskRef(task?.meta?.pr?.baseRefName) ||
|
|
2270
|
+
"origin/main"
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
function resolveTaskWorkspaceEntry(task, workspaceContext = {}) {
|
|
2275
|
+
const configDir = resolveUiConfigDir();
|
|
2276
|
+
if (!configDir) return null;
|
|
2277
|
+
const listed = listManagedWorkspaces(configDir, { repoRoot });
|
|
2278
|
+
const taskWorkspace = normalizeDiffTaskRef(task?.workspace || task?.meta?.workspace);
|
|
2279
|
+
const taskWorkspacePath = normalizeCandidatePath(taskWorkspace);
|
|
2280
|
+
const workspaceId = normalizeDiffTaskRef(workspaceContext?.workspaceId || workspaceContext?.workspaceFilter);
|
|
2281
|
+
|
|
2282
|
+
return (
|
|
2283
|
+
(taskWorkspace
|
|
2284
|
+
? listed.find((entry) => {
|
|
2285
|
+
const id = normalizeDiffTaskRef(entry?.id);
|
|
2286
|
+
const entryPath = normalizeCandidatePath(entry?.path);
|
|
2287
|
+
return id === taskWorkspace || (taskWorkspacePath && entryPath && entryPath === taskWorkspacePath);
|
|
2288
|
+
})
|
|
2289
|
+
: null) ||
|
|
2290
|
+
(workspaceId
|
|
2291
|
+
? listed.find((entry) => normalizeDiffTaskRef(entry?.id) === workspaceId)
|
|
2292
|
+
: null) ||
|
|
2293
|
+
getActiveManagedWorkspace(configDir) ||
|
|
2294
|
+
listed[0] ||
|
|
2295
|
+
null
|
|
2296
|
+
);
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
function resolveTaskRepositoryDir(task, workspaceContext = {}) {
|
|
2300
|
+
const repositoryName = normalizeDiffTaskRef(task?.repository || task?.meta?.repository);
|
|
2301
|
+
const taskWorkspace = normalizeDiffTaskRef(task?.workspace || task?.meta?.workspace);
|
|
2302
|
+
const taskWorkspacePath = normalizeCandidatePath(taskWorkspace);
|
|
2303
|
+
const workspaceEntry = resolveTaskWorkspaceEntry(task, workspaceContext);
|
|
2304
|
+
const candidates = [];
|
|
2305
|
+
|
|
2306
|
+
const pushCandidate = (candidate) => {
|
|
2307
|
+
const normalized = normalizeCandidatePath(candidate);
|
|
2308
|
+
if (normalized && !candidates.includes(normalized)) candidates.push(normalized);
|
|
2309
|
+
};
|
|
2310
|
+
|
|
2311
|
+
if (taskWorkspacePath && repositoryName) pushCandidate(resolve(taskWorkspacePath, repositoryName));
|
|
2312
|
+
if (taskWorkspacePath) pushCandidate(taskWorkspacePath);
|
|
2313
|
+
|
|
2314
|
+
if (workspaceEntry) {
|
|
2315
|
+
const repos = Array.isArray(workspaceEntry.repos) ? workspaceEntry.repos : [];
|
|
2316
|
+
const matchedRepo = repositoryName
|
|
2317
|
+
? repos.find((repo) => {
|
|
2318
|
+
const values = [
|
|
2319
|
+
normalizeDiffTaskRef(repo?.name),
|
|
2320
|
+
normalizeDiffTaskRef(repo?.slug),
|
|
2321
|
+
normalizeCandidatePath(repo?.path),
|
|
2322
|
+
].filter(Boolean);
|
|
2323
|
+
return values.includes(repositoryName) || values.includes(normalizeCandidatePath(repositoryName));
|
|
2324
|
+
})
|
|
2325
|
+
: null;
|
|
2326
|
+
if (matchedRepo?.path) pushCandidate(matchedRepo.path);
|
|
2327
|
+
if (workspaceEntry.path && repositoryName) pushCandidate(resolve(workspaceEntry.path, repositoryName));
|
|
2328
|
+
pushCandidate(pickWorkspaceRepoDir(workspaceEntry));
|
|
2329
|
+
pushCandidate(workspaceEntry.path);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
if (workspaceContext?.workspaceDir && repositoryName) {
|
|
2333
|
+
pushCandidate(resolve(workspaceContext.workspaceDir, repositoryName));
|
|
2334
|
+
}
|
|
2335
|
+
pushCandidate(workspaceContext?.workspaceDir);
|
|
2336
|
+
pushCandidate(repoRoot);
|
|
2337
|
+
|
|
2338
|
+
for (const candidate of candidates) {
|
|
2339
|
+
if (!candidate || !existsSync(candidate)) continue;
|
|
2340
|
+
if (existsSync(resolve(candidate, ".git"))) return candidate;
|
|
2341
|
+
}
|
|
2342
|
+
return candidates.find((candidate) => candidate && existsSync(candidate)) || "";
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
function emptyTaskDiffPayload(formatted, source = {}) {
|
|
2346
|
+
return {
|
|
2347
|
+
diff: {
|
|
2348
|
+
files: [],
|
|
2349
|
+
totalFiles: 0,
|
|
2350
|
+
totalAdditions: 0,
|
|
2351
|
+
totalDeletions: 0,
|
|
2352
|
+
formatted,
|
|
2353
|
+
},
|
|
2354
|
+
summary: formatted,
|
|
2355
|
+
commits: [],
|
|
2356
|
+
source,
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
async function buildTaskDiffPayload(task, workspaceContext = {}) {
|
|
2361
|
+
if (!task) {
|
|
2362
|
+
return emptyTaskDiffPayload("(task not found)", { kind: "task", detail: "Task could not be loaded." });
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
const baseBranch = pickTaskDiffBaseBranch(task);
|
|
2366
|
+
|
|
2367
|
+
for (const sessionId of collectTaskDiffSessionIds(task)) {
|
|
2368
|
+
try {
|
|
2369
|
+
const session = tracker.getSession(sessionId);
|
|
2370
|
+
if (!session) continue;
|
|
2371
|
+
const worktreePath = await resolveSessionWorktreePath(session);
|
|
2372
|
+
if (!worktreePath || !existsSync(worktreePath)) continue;
|
|
2373
|
+
const diff = collectDiffStats(worktreePath, {
|
|
2374
|
+
baseBranch,
|
|
2375
|
+
includePatch: true,
|
|
2376
|
+
});
|
|
2377
|
+
if (diff.totalFiles > 0) {
|
|
2378
|
+
return {
|
|
2379
|
+
diff,
|
|
2380
|
+
summary: diff.formatted,
|
|
2381
|
+
commits: getRecentCommits(worktreePath),
|
|
2382
|
+
source: {
|
|
2383
|
+
kind: "session",
|
|
2384
|
+
label: diff.sourceRange || "session worktree",
|
|
2385
|
+
detail: worktreePath,
|
|
2386
|
+
},
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
} catch {
|
|
2390
|
+
// Fall through to branch or commit resolution.
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
const repoPath = resolveTaskRepositoryDir(task, workspaceContext);
|
|
2395
|
+
if (!repoPath || !existsSync(repoPath)) {
|
|
2396
|
+
return emptyTaskDiffPayload("(no repository for task diff)", {
|
|
2397
|
+
kind: "task",
|
|
2398
|
+
detail: "No repository or preserved worktree is available for this task.",
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
const attempts = [];
|
|
2403
|
+
const branch = pickTaskDiffBranch(task);
|
|
2404
|
+
if (branch) {
|
|
2405
|
+
attempts.push({
|
|
2406
|
+
kind: "branch",
|
|
2407
|
+
label: `${baseBranch}...${branch}`,
|
|
2408
|
+
options: { baseBranch, targetRef: branch, includePatch: true },
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
for (const commitRef of collectTaskDiffCommitRefs(task)) {
|
|
2412
|
+
attempts.push({
|
|
2413
|
+
kind: "commit",
|
|
2414
|
+
label: commitRef,
|
|
2415
|
+
options: { range: `${commitRef}^..${commitRef}`, includePatch: true },
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
if (!attempts.length) {
|
|
2419
|
+
attempts.push({
|
|
2420
|
+
kind: "task",
|
|
2421
|
+
label: `${baseBranch}...HEAD`,
|
|
2422
|
+
options: { baseBranch, includePatch: true },
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
for (const attempt of attempts) {
|
|
2427
|
+
const diff = collectDiffStats(repoPath, attempt.options);
|
|
2428
|
+
if (diff.totalFiles > 0) {
|
|
2429
|
+
return {
|
|
2430
|
+
diff,
|
|
2431
|
+
summary: diff.formatted,
|
|
2432
|
+
commits: getRecentCommits(repoPath),
|
|
2433
|
+
source: {
|
|
2434
|
+
kind: attempt.kind,
|
|
2435
|
+
label: diff.sourceRange || attempt.label,
|
|
2436
|
+
detail: repoPath,
|
|
2437
|
+
},
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
return emptyTaskDiffPayload("(no diff available)", {
|
|
2443
|
+
kind: branch ? "branch" : "task",
|
|
2444
|
+
label: branch ? `${baseBranch}...${branch}` : "",
|
|
2445
|
+
detail: repoPath,
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2172
2449
|
function taskMatchesWorkspaceContext(task, workspaceContext) {
|
|
2173
2450
|
const workspaceFilter = String(
|
|
2174
2451
|
workspaceContext?.workspaceFilter || workspaceContext?.workspaceId || "",
|
|
@@ -4623,14 +4900,22 @@ let _browserOpened = false;
|
|
|
4623
4900
|
const AUTO_OPEN_MARKER_FILE = "ui-auto-open.json";
|
|
4624
4901
|
const UI_INSTANCE_LOCK_FILE = "ui-server.instance.lock.json";
|
|
4625
4902
|
const UI_SESSION_TOKEN_FILE = "ui-session-token.json";
|
|
4903
|
+
const TUI_SESSION_TOKEN_FILE = "ui-token";
|
|
4626
4904
|
const UI_LAST_PORT_FILE = "ui-last-port.json";
|
|
4627
4905
|
const DEFAULT_AUTO_OPEN_COOLDOWN_MS = 12 * 60 * 60 * 1000; // 12h
|
|
4628
4906
|
const DEFAULT_SESSION_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
4629
4907
|
const wsClients = new Set();
|
|
4630
4908
|
let sessionListenerAttached = false;
|
|
4909
|
+
let sessionStateListenerAttached = false;
|
|
4910
|
+
let activeSessionListenerAttached = false;
|
|
4631
4911
|
let sessionAccumulatorListenerAttached = false;
|
|
4912
|
+
let removeSessionEventListener = null;
|
|
4913
|
+
let removeSessionStateListener = null;
|
|
4914
|
+
let removeActiveSessionListener = null;
|
|
4915
|
+
let removeSessionAccumulatorListener = null;
|
|
4632
4916
|
/** @type {ReturnType<typeof setInterval>|null} */
|
|
4633
4917
|
let wsHeartbeatTimer = null;
|
|
4918
|
+
let tuiStatsEmitter = null;
|
|
4634
4919
|
const WORKFLOW_WS_BATCH_MS = 80;
|
|
4635
4920
|
const workflowWsBatchByKey = new Map();
|
|
4636
4921
|
const workflowEngineListenerCleanup = new WeakMap();
|
|
@@ -4688,6 +4973,35 @@ async function resolveVoiceRelay() {
|
|
|
4688
4973
|
let _fallbackExecPrimaryPrompt = null;
|
|
4689
4974
|
/** Track in-flight chat turns so /api/sessions/:id/stop can abort them. */
|
|
4690
4975
|
const sessionRunAbortControllers = new Map();
|
|
4976
|
+
let _activeSessions = [];
|
|
4977
|
+
|
|
4978
|
+
function getLiveSessionSnapshot({ includeHidden = false } = {}) {
|
|
4979
|
+
const tracker = getSessionTracker();
|
|
4980
|
+
let sessions = tracker.listAllSessions();
|
|
4981
|
+
if (!includeHidden) {
|
|
4982
|
+
sessions = sessions.filter((session) => {
|
|
4983
|
+
const detailed = tracker.getSessionById(session.id) || session;
|
|
4984
|
+
return !shouldHideSessionFromDefaultList(detailed);
|
|
4985
|
+
});
|
|
4986
|
+
}
|
|
4987
|
+
return sessions;
|
|
4988
|
+
}
|
|
4989
|
+
|
|
4990
|
+
function broadcastSessionsSnapshot(sessions = getLiveSessionSnapshot()) {
|
|
4991
|
+
const normalized = Array.isArray(sessions) ? sessions : [];
|
|
4992
|
+
broadcastUiEvent(["sessions", "tui"], "sessions:update", {
|
|
4993
|
+
sessions: normalized,
|
|
4994
|
+
});
|
|
4995
|
+
}
|
|
4996
|
+
|
|
4997
|
+
function updateActiveSessions(sessions) {
|
|
4998
|
+
_activeSessions = Array.isArray(sessions) ? sessions : [];
|
|
4999
|
+
broadcastSessionsSnapshot(_activeSessions);
|
|
5000
|
+
for (const session of _activeSessions) {
|
|
5001
|
+
broadcastUiEvent(["sessions", "tui"], "session:update", session);
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
|
|
4691
5005
|
async function resolveExecPrimaryPrompt() {
|
|
4692
5006
|
if (typeof uiDeps.execPrimaryPrompt === "function") return uiDeps.execPrimaryPrompt;
|
|
4693
5007
|
if (_fallbackExecPrimaryPrompt) return _fallbackExecPrimaryPrompt;
|
|
@@ -4989,6 +5303,17 @@ function isValidSessionToken(token) {
|
|
|
4989
5303
|
}
|
|
4990
5304
|
|
|
4991
5305
|
function readPersistedSessionToken() {
|
|
5306
|
+
const envToken = resolveTuiAuthToken({ env: process.env, configDir: resolveUiConfigDir() });
|
|
5307
|
+
if (isValidSessionToken(envToken)) return envToken;
|
|
5308
|
+
try {
|
|
5309
|
+
const plainTokenPath = resolveUiCachePath(TUI_SESSION_TOKEN_FILE);
|
|
5310
|
+
if (existsSync(plainTokenPath)) {
|
|
5311
|
+
const plainToken = String(readFileSync(plainTokenPath, "utf8") || "").trim();
|
|
5312
|
+
if (isValidSessionToken(plainToken)) return plainToken;
|
|
5313
|
+
}
|
|
5314
|
+
} catch {
|
|
5315
|
+
// best effort
|
|
5316
|
+
}
|
|
4992
5317
|
try {
|
|
4993
5318
|
const tokenPath = resolveUiCachePath(UI_SESSION_TOKEN_FILE);
|
|
4994
5319
|
if (!existsSync(tokenPath)) return "";
|
|
@@ -5006,6 +5331,7 @@ function readPersistedSessionToken() {
|
|
|
5006
5331
|
|
|
5007
5332
|
function persistSessionToken(token) {
|
|
5008
5333
|
if (!isValidSessionToken(token)) return;
|
|
5334
|
+
process.env.BOSUN_UI_TOKEN = token;
|
|
5009
5335
|
try {
|
|
5010
5336
|
const tokenPath = resolveUiCachePath(UI_SESSION_TOKEN_FILE);
|
|
5011
5337
|
writeFileSync(
|
|
@@ -5021,6 +5347,7 @@ function persistSessionToken(token) {
|
|
|
5021
5347
|
),
|
|
5022
5348
|
"utf8",
|
|
5023
5349
|
);
|
|
5350
|
+
persistCompatibleTuiAuthToken(token, { configDir: resolveUiConfigDir() });
|
|
5024
5351
|
} catch {
|
|
5025
5352
|
// best effort
|
|
5026
5353
|
}
|
|
@@ -5040,6 +5367,7 @@ function ensureSessionToken() {
|
|
|
5040
5367
|
const persisted = readPersistedSessionToken();
|
|
5041
5368
|
if (persisted) {
|
|
5042
5369
|
sessionToken = persisted;
|
|
5370
|
+
persistSessionToken(sessionToken);
|
|
5043
5371
|
return sessionToken;
|
|
5044
5372
|
}
|
|
5045
5373
|
sessionToken = randomBytes(32).toString("hex");
|
|
@@ -9095,6 +9423,87 @@ function sendWsMessage(socket, payload) {
|
|
|
9095
9423
|
}
|
|
9096
9424
|
}
|
|
9097
9425
|
|
|
9426
|
+
function broadcastCanonicalEvent(channels, type, payload = {}) {
|
|
9427
|
+
const required = new Set(Array.isArray(channels) ? channels : [channels]);
|
|
9428
|
+
const message = {
|
|
9429
|
+
type,
|
|
9430
|
+
channels: Array.from(required),
|
|
9431
|
+
payload,
|
|
9432
|
+
ts: Date.now(),
|
|
9433
|
+
};
|
|
9434
|
+
for (const socket of wsClients) {
|
|
9435
|
+
const subscribed = socket.__channels || new Set(["*"]);
|
|
9436
|
+
const shouldSend = subscribed.has("*") || Array.from(required).some((channel) => subscribed.has(channel));
|
|
9437
|
+
if (shouldSend) sendWsMessage(socket, message);
|
|
9438
|
+
}
|
|
9439
|
+
}
|
|
9440
|
+
|
|
9441
|
+
function getCurrentSessionSnapshot() {
|
|
9442
|
+
try {
|
|
9443
|
+
const tracker = getSessionTracker();
|
|
9444
|
+
return buildSessionsUpdatePayload(tracker?.listAllSessions?.() || []);
|
|
9445
|
+
} catch {
|
|
9446
|
+
return [];
|
|
9447
|
+
}
|
|
9448
|
+
}
|
|
9449
|
+
|
|
9450
|
+
function broadcastTuiSessionsSnapshot(reason = "updated", detail = {}) {
|
|
9451
|
+
broadcastCanonicalEvent(["sessions", "tui"], "sessions:update", getCurrentSessionSnapshot());
|
|
9452
|
+
const sessionEvent = buildSessionEventPayload({
|
|
9453
|
+
sessionId: detail?.sessionId || detail?.session?.id || detail?.threadId || detail?.taskKey || "",
|
|
9454
|
+
taskId: detail?.taskId || detail?.session?.taskId || detail?.taskKey || "",
|
|
9455
|
+
session: detail?.session || detail,
|
|
9456
|
+
event: {
|
|
9457
|
+
kind: "state",
|
|
9458
|
+
reason,
|
|
9459
|
+
...(detail && typeof detail === "object" ? detail : {}),
|
|
9460
|
+
},
|
|
9461
|
+
});
|
|
9462
|
+
if (sessionEvent.sessionId && sessionEvent.taskId) {
|
|
9463
|
+
broadcastCanonicalEvent(["sessions", "tui"], "session:event", sessionEvent);
|
|
9464
|
+
}
|
|
9465
|
+
}
|
|
9466
|
+
|
|
9467
|
+
function buildCurrentTuiMonitorStats() {
|
|
9468
|
+
const executor = uiDeps.getInternalExecutor?.() || null;
|
|
9469
|
+
const injectedStats = uiDeps.getTuiMonitorStats?.() || {};
|
|
9470
|
+
const status = executor?.getStatus?.() || {};
|
|
9471
|
+
const slots = Array.isArray(status?.slots) ? status.slots : [];
|
|
9472
|
+
const runtimeStats = getRuntimeStats() || {};
|
|
9473
|
+
const runtimeSessions = Array.isArray(runtimeStats?.sessions) ? runtimeStats.sessions : [];
|
|
9474
|
+
|
|
9475
|
+
const pickNumericStat = (...candidates) => {
|
|
9476
|
+
for (const candidate of candidates) {
|
|
9477
|
+
if (candidate == null) continue;
|
|
9478
|
+
const numeric = Number(candidate);
|
|
9479
|
+
if (Number.isFinite(numeric)) {
|
|
9480
|
+
return numeric;
|
|
9481
|
+
}
|
|
9482
|
+
}
|
|
9483
|
+
return 0;
|
|
9484
|
+
};
|
|
9485
|
+
const tokensIn = pickNumericStat(injectedStats?.tokensIn, runtimeStats?.totalInputTokens);
|
|
9486
|
+
const tokensOut = pickNumericStat(injectedStats?.tokensOut, runtimeStats?.totalOutputTokens);
|
|
9487
|
+
|
|
9488
|
+
return buildMonitorStatsPayload({
|
|
9489
|
+
agentPool: {
|
|
9490
|
+
activeAgents: pickNumericStat(status?.activeSlots, slots.length, injectedStats?.activeAgents),
|
|
9491
|
+
maxAgents: pickNumericStat(status?.maxParallel, injectedStats?.maxAgents),
|
|
9492
|
+
tokensIn: pickNumericStat(injectedStats?.tokensIn, tokensIn),
|
|
9493
|
+
tokensOut: pickNumericStat(injectedStats?.tokensOut, tokensOut),
|
|
9494
|
+
throughputTps: injectedStats?.throughputTps,
|
|
9495
|
+
rateLimits: injectedStats?.rateLimits || {},
|
|
9496
|
+
},
|
|
9497
|
+
runtimeStats: {
|
|
9498
|
+
...runtimeStats,
|
|
9499
|
+
sessions: runtimeSessions,
|
|
9500
|
+
totalInputTokens: tokensIn,
|
|
9501
|
+
totalOutputTokens: tokensOut,
|
|
9502
|
+
},
|
|
9503
|
+
uptimeMs: runtimeStats?.startedAt ? Date.now() - Number(runtimeStats.startedAt) : process.uptime() * 1000,
|
|
9504
|
+
});
|
|
9505
|
+
}
|
|
9506
|
+
|
|
9098
9507
|
function normalizeWorkflowNodeStatus(status) {
|
|
9099
9508
|
const normalized = String(status || "").trim().toLowerCase();
|
|
9100
9509
|
if (normalized === "completed" || normalized === "success") return "success";
|
|
@@ -9138,6 +9547,106 @@ function summarizeOutputLines(value, maxLines = 3, maxChars = 140) {
|
|
|
9138
9547
|
return lines;
|
|
9139
9548
|
}
|
|
9140
9549
|
|
|
9550
|
+
function summarizeTrajectoryText(value, maxLength = 160) {
|
|
9551
|
+
const text = String(value ?? "")
|
|
9552
|
+
.replace(/\r\n/g, "\n")
|
|
9553
|
+
.replace(/\r/g, "\n")
|
|
9554
|
+
.split("\n")
|
|
9555
|
+
.map((line) => line.trim())
|
|
9556
|
+
.filter(Boolean)
|
|
9557
|
+
.join(" ")
|
|
9558
|
+
.replace(/\s{2,}/g, " ")
|
|
9559
|
+
.trim();
|
|
9560
|
+
if (!text) return null;
|
|
9561
|
+
if (text.length <= maxLength) return text;
|
|
9562
|
+
return `${text.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
|
|
9563
|
+
}
|
|
9564
|
+
|
|
9565
|
+
function classifyTrajectorySessionEvent(event = {}) {
|
|
9566
|
+
const payload = event && typeof event === "object" ? event : {};
|
|
9567
|
+
const role = String(payload?.role || payload?.message?.role || "").trim().toLowerCase();
|
|
9568
|
+
const type = String(payload?.type || payload?.message?.type || payload?.event?.kind || "").trim().toLowerCase();
|
|
9569
|
+
if (role === "user") return "user";
|
|
9570
|
+
if (role === "assistant") return "assistant";
|
|
9571
|
+
if (type.includes("tool") && (type.includes("result") || type.includes("output"))) return "tool_result";
|
|
9572
|
+
if (type.includes("tool")) return "tool_call";
|
|
9573
|
+
if (type.includes("reason") || type.includes("thinking")) return "reasoning";
|
|
9574
|
+
if (type === "error") return "status";
|
|
9575
|
+
return "event";
|
|
9576
|
+
}
|
|
9577
|
+
|
|
9578
|
+
function buildTrajectoryStepFromSessionEvent(event = {}) {
|
|
9579
|
+
const normalizedType = classifyTrajectorySessionEvent(event);
|
|
9580
|
+
const message = event?.message && typeof event.message === "object" ? event.message : event;
|
|
9581
|
+
const content = message?.content ?? event?.content ?? null;
|
|
9582
|
+
const summary = summarizeTrajectoryText(
|
|
9583
|
+
message?.summary || event?.summary || content || event?.label || event?.reason || event?.type || "Run event recorded.",
|
|
9584
|
+
) || "Run event recorded.";
|
|
9585
|
+
return {
|
|
9586
|
+
id: String(event?.id || message?.id || `step-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`),
|
|
9587
|
+
at: String(event?.timestamp || message?.timestamp || event?.at || new Date().toISOString()),
|
|
9588
|
+
type: normalizedType,
|
|
9589
|
+
summary,
|
|
9590
|
+
payload: {
|
|
9591
|
+
role: message?.role || event?.role || null,
|
|
9592
|
+
type: message?.type || event?.type || null,
|
|
9593
|
+
content: typeof content === "string" ? content : null,
|
|
9594
|
+
reason: event?.reason || null,
|
|
9595
|
+
},
|
|
9596
|
+
event: event && typeof event === "object" ? { ...event } : null,
|
|
9597
|
+
};
|
|
9598
|
+
}
|
|
9599
|
+
|
|
9600
|
+
function buildTaskRunSummary(steps = [], fallback = "Run recorded.") {
|
|
9601
|
+
const ordered = Array.isArray(steps) ? steps : [];
|
|
9602
|
+
for (let index = ordered.length - 1; index >= 0; index -= 1) {
|
|
9603
|
+
const summary = summarizeTrajectoryText(ordered[index]?.summary || "");
|
|
9604
|
+
if (summary) return summary;
|
|
9605
|
+
}
|
|
9606
|
+
return fallback;
|
|
9607
|
+
}
|
|
9608
|
+
|
|
9609
|
+
async function buildReplayableTaskRuns(task, tracker = null, limit = 5) {
|
|
9610
|
+
const sessionIds = collectTaskDiffSessionIds(task);
|
|
9611
|
+
const { value: storedRunsRaw } = await callTaskStoreFunction(TASK_STORE_RUN_EXPORTS.list, [task?.id]);
|
|
9612
|
+
const storedRuns = storedRunsRaw;
|
|
9613
|
+
const runs = Array.isArray(storedRuns) ? storedRuns.map((run) => ({
|
|
9614
|
+
...run,
|
|
9615
|
+
summary: run.summary || buildTaskRunSummary(run.steps, run.status === "failed" ? "Run failed." : "Run completed."),
|
|
9616
|
+
})) : [];
|
|
9617
|
+
const seenRunIds = new Set(runs.map((entry) => String(entry?.runId || "")).filter(Boolean));
|
|
9618
|
+
for (const sessionId of sessionIds) {
|
|
9619
|
+
const session = tracker?.getSession?.(sessionId);
|
|
9620
|
+
if (!session) continue;
|
|
9621
|
+
const events = Array.isArray(session?.events) ? session.events : [];
|
|
9622
|
+
const steps = events.map((event) => buildTrajectoryStepFromSessionEvent(event)).filter(Boolean);
|
|
9623
|
+
if (steps.length === 0) continue;
|
|
9624
|
+
const runId = String(sessionId || session?.id || "").trim();
|
|
9625
|
+
if (!runId || seenRunIds.has(runId)) continue;
|
|
9626
|
+
seenRunIds.add(runId);
|
|
9627
|
+
runs.push({
|
|
9628
|
+
runId,
|
|
9629
|
+
startedAt: String(session?.createdAt || steps[0]?.at || new Date().toISOString()),
|
|
9630
|
+
endedAt: session?.status === "active" ? null : String(session?.updatedAt || steps.at(-1)?.at || new Date().toISOString()),
|
|
9631
|
+
status: String(session?.status || "completed"),
|
|
9632
|
+
taskKey: String(task?.id || session?.taskId || "").trim() || null,
|
|
9633
|
+
sdk: String(session?.executor || session?.sdk || session?.metadata?.resolvedSdk || "").trim() || null,
|
|
9634
|
+
threadId: String(session?.threadId || session?.metadata?.threadId || "").trim() || null,
|
|
9635
|
+
resumeThreadId: String(session?.metadata?.resumeThreadId || session?.threadId || "").trim() || null,
|
|
9636
|
+
replayable: true,
|
|
9637
|
+
outcome: session?.status === "failed" ? "failed" : "completed",
|
|
9638
|
+
summary: buildTaskRunSummary(steps, session?.status === "failed" ? "Run failed." : "Run completed."),
|
|
9639
|
+
steps,
|
|
9640
|
+
meta: {
|
|
9641
|
+
sessionId: runId,
|
|
9642
|
+
source: "session-tracker",
|
|
9643
|
+
},
|
|
9644
|
+
});
|
|
9645
|
+
}
|
|
9646
|
+
runs.sort((left, right) => Date.parse(String(right?.startedAt || 0)) - Date.parse(String(left?.startedAt || 0)));
|
|
9647
|
+
return runs.slice(0, Math.max(1, Number(limit) || 5));
|
|
9648
|
+
}
|
|
9649
|
+
|
|
9141
9650
|
function buildWorkflowNodeOutputPreview(nodeType, output = null) {
|
|
9142
9651
|
if (output == null) return { lines: [] };
|
|
9143
9652
|
const type = String(nodeType || "").trim().toLowerCase();
|
|
@@ -9278,6 +9787,13 @@ function attachWorkflowEngineLiveBridge(engine) {
|
|
|
9278
9787
|
};
|
|
9279
9788
|
|
|
9280
9789
|
listen("run:start", (payload) => {
|
|
9790
|
+
broadcastCanonicalEvent(["workflows", "tui"], "workflow:status", buildWorkflowStatusPayload({
|
|
9791
|
+
...payload,
|
|
9792
|
+
workflowName: payload.name || payload.workflowName || null,
|
|
9793
|
+
eventType: "run:start",
|
|
9794
|
+
status: "running",
|
|
9795
|
+
timestamp: Date.now(),
|
|
9796
|
+
}));
|
|
9281
9797
|
queueWorkflowWsEvent({
|
|
9282
9798
|
kind: "run",
|
|
9283
9799
|
workflowId: payload.workflowId,
|
|
@@ -9289,6 +9805,14 @@ function attachWorkflowEngineLiveBridge(engine) {
|
|
|
9289
9805
|
});
|
|
9290
9806
|
});
|
|
9291
9807
|
listen("run:end", (payload) => {
|
|
9808
|
+
broadcastCanonicalEvent(["workflows", "tui"], "workflow:status", buildWorkflowStatusPayload({
|
|
9809
|
+
...payload,
|
|
9810
|
+
workflowName: payload.workflowName || payload.name || null,
|
|
9811
|
+
eventType: "run:end",
|
|
9812
|
+
durationMs: Number(payload.duration) || null,
|
|
9813
|
+
status: String(payload.status || "").trim().toLowerCase() || "completed",
|
|
9814
|
+
timestamp: Date.now(),
|
|
9815
|
+
}));
|
|
9292
9816
|
queueWorkflowWsEvent({
|
|
9293
9817
|
kind: "run",
|
|
9294
9818
|
workflowId: payload.workflowId,
|
|
@@ -9301,6 +9825,13 @@ function attachWorkflowEngineLiveBridge(engine) {
|
|
|
9301
9825
|
});
|
|
9302
9826
|
});
|
|
9303
9827
|
listen("run:error", (payload) => {
|
|
9828
|
+
broadcastCanonicalEvent(["workflows", "tui"], "workflow:status", buildWorkflowStatusPayload({
|
|
9829
|
+
...payload,
|
|
9830
|
+
workflowName: payload.workflowName || payload.name || null,
|
|
9831
|
+
eventType: "run:error",
|
|
9832
|
+
status: "failed",
|
|
9833
|
+
timestamp: Date.now(),
|
|
9834
|
+
}));
|
|
9304
9835
|
queueWorkflowWsEvent({
|
|
9305
9836
|
kind: "run",
|
|
9306
9837
|
workflowId: payload.workflowId,
|
|
@@ -9338,6 +9869,13 @@ function attachWorkflowEngineLiveBridge(engine) {
|
|
|
9338
9869
|
});
|
|
9339
9870
|
});
|
|
9340
9871
|
listen("node:complete", (payload) => {
|
|
9872
|
+
broadcastCanonicalEvent(["workflows", "tui"], "workflow:status", buildWorkflowStatusPayload({
|
|
9873
|
+
...payload,
|
|
9874
|
+
workflowName: payload.workflowName || null,
|
|
9875
|
+
eventType: "node:complete",
|
|
9876
|
+
status: "success",
|
|
9877
|
+
timestamp: Date.now(),
|
|
9878
|
+
}));
|
|
9341
9879
|
const preview = buildWorkflowNodeOutputPreview(payload.nodeType, payload.output);
|
|
9342
9880
|
queueWorkflowWsEvent({
|
|
9343
9881
|
kind: "node",
|
|
@@ -9408,21 +9946,13 @@ function attachWorkflowEngineLiveBridge(engine) {
|
|
|
9408
9946
|
}
|
|
9409
9947
|
|
|
9410
9948
|
function broadcastUiEvent(channels, type, payload = {}) {
|
|
9411
|
-
const required =
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
for (const socket of wsClients) {
|
|
9419
|
-
const subscribed = socket.__channels || new Set(["*"]);
|
|
9420
|
-
const shouldSend =
|
|
9421
|
-
subscribed.has("*") ||
|
|
9422
|
-
Array.from(required).some((channel) => subscribed.has(channel));
|
|
9423
|
-
if (shouldSend) {
|
|
9424
|
-
sendWsMessage(socket, message);
|
|
9425
|
-
}
|
|
9949
|
+
const required = Array.isArray(channels) ? channels : [channels];
|
|
9950
|
+
broadcastCanonicalEvent(required, type, payload);
|
|
9951
|
+
if (required.includes("sessions") && type !== "sessions:update" && type !== "session:event") {
|
|
9952
|
+
broadcastTuiSessionsSnapshot(type, payload);
|
|
9953
|
+
}
|
|
9954
|
+
if (required.includes("tasks") && type !== "tasks:update") {
|
|
9955
|
+
broadcastCanonicalEvent(["tasks", "tui"], "tasks:update", buildTasksUpdatePayload(payload, { sourceEvent: type }));
|
|
9426
9956
|
}
|
|
9427
9957
|
}
|
|
9428
9958
|
|
|
@@ -9443,6 +9973,20 @@ function broadcastSessionMessage(payload) {
|
|
|
9443
9973
|
sendWsMessage(socket, message);
|
|
9444
9974
|
}
|
|
9445
9975
|
}
|
|
9976
|
+
|
|
9977
|
+
const sessionEvent = buildSessionEventPayload({
|
|
9978
|
+
sessionId: payload?.sessionId || payload?.session?.id || payload?.taskId || "",
|
|
9979
|
+
taskId: payload?.taskId || payload?.session?.taskId || payload?.sessionId || "",
|
|
9980
|
+
session: payload?.session || {},
|
|
9981
|
+
event: {
|
|
9982
|
+
kind: "message",
|
|
9983
|
+
message: payload?.message ?? null,
|
|
9984
|
+
},
|
|
9985
|
+
});
|
|
9986
|
+
if (sessionEvent.sessionId && sessionEvent.taskId) {
|
|
9987
|
+
broadcastCanonicalEvent(["sessions", "tui"], "session:event", sessionEvent);
|
|
9988
|
+
}
|
|
9989
|
+
broadcastCanonicalEvent(["sessions", "tui"], "sessions:update", getCurrentSessionSnapshot());
|
|
9446
9990
|
}
|
|
9447
9991
|
|
|
9448
9992
|
async function collectUiStats() {
|
|
@@ -9698,7 +10242,6 @@ function startLogStream(socket, logType, query) {
|
|
|
9698
10242
|
// File was truncated/rotated — reset
|
|
9699
10243
|
streamState.offset = 0;
|
|
9700
10244
|
}
|
|
9701
|
-
|
|
9702
10245
|
if (size <= streamState.offset) return;
|
|
9703
10246
|
|
|
9704
10247
|
// Read only new bytes
|
|
@@ -10490,7 +11033,13 @@ async function handleDeviceFlowPoll(req, res) {
|
|
|
10490
11033
|
async function readStatusSnapshot() {
|
|
10491
11034
|
try {
|
|
10492
11035
|
const raw = await readFile(statusPath, "utf8");
|
|
10493
|
-
|
|
11036
|
+
const parsed = JSON.parse(raw);
|
|
11037
|
+
if (parsed && typeof parsed === "object") {
|
|
11038
|
+
parsed.worktreeRecovery = normalizeWorktreeRecoveryState(
|
|
11039
|
+
parsed.worktreeRecovery || parsed.worktree_recovery || null,
|
|
11040
|
+
);
|
|
11041
|
+
}
|
|
11042
|
+
return parsed;
|
|
10494
11043
|
} catch {
|
|
10495
11044
|
return null;
|
|
10496
11045
|
}
|
|
@@ -10918,9 +11467,9 @@ async function tailFile(filePath, lineCount, maxBytes = 1_000_000) {
|
|
|
10918
11467
|
};
|
|
10919
11468
|
}
|
|
10920
11469
|
|
|
10921
|
-
async function readJsonlTail(filePath, maxLines = 2000) {
|
|
11470
|
+
async function readJsonlTail(filePath, maxLines = 2000, maxBytes = 1_000_000) {
|
|
10922
11471
|
if (!existsSync(filePath)) return [];
|
|
10923
|
-
const tail = await tailFile(filePath, maxLines);
|
|
11472
|
+
const tail = await tailFile(filePath, maxLines, maxBytes);
|
|
10924
11473
|
return (tail.lines || [])
|
|
10925
11474
|
.map((line) => {
|
|
10926
11475
|
try {
|
|
@@ -10994,7 +11543,7 @@ async function readCompletedSessionEntries(maxLines = 100_000) {
|
|
|
10994
11543
|
}
|
|
10995
11544
|
} catch { /* stat failed, skip */ }
|
|
10996
11545
|
}
|
|
10997
|
-
const entries = await readJsonlTail(sessionLogPath, maxLines);
|
|
11546
|
+
const entries = await readJsonlTail(sessionLogPath, maxLines, 50_000_000);
|
|
10998
11547
|
return {
|
|
10999
11548
|
sessionLogPath,
|
|
11000
11549
|
entries: entries.filter((entry) => String(entry?.type || "completed_session") === "completed_session"),
|
|
@@ -11138,7 +11687,7 @@ async function buildUsageAnalytics(days) {
|
|
|
11138
11687
|
const streamPath = resolve(logDir, "agent-work-stream.jsonl");
|
|
11139
11688
|
const [{ entries: completedSessions }, events] = await Promise.all([
|
|
11140
11689
|
readCompletedSessionEntries(100_000),
|
|
11141
|
-
readJsonlTail(streamPath, 100_000),
|
|
11690
|
+
readJsonlTail(streamPath, 100_000, 50_000_000),
|
|
11142
11691
|
]);
|
|
11143
11692
|
|
|
11144
11693
|
const cutoff = days ? Date.now() - days * 24 * 60 * 60 * 1000 : 0;
|
|
@@ -11306,6 +11855,162 @@ function resolveAgentWorkLogDir() {
|
|
|
11306
11855
|
return candidates[0];
|
|
11307
11856
|
}
|
|
11308
11857
|
|
|
11858
|
+
function clampRunSummaryText(value, maxLength = 220) {
|
|
11859
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
11860
|
+
if (!text) return "";
|
|
11861
|
+
if (text.length <= maxLength) return text;
|
|
11862
|
+
return `${text.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
11863
|
+
}
|
|
11864
|
+
|
|
11865
|
+
function summarizeTrajectoryEvent(event = {}) {
|
|
11866
|
+
const type = String(event?.event_type || event?.type || "event").trim() || "event";
|
|
11867
|
+
const data = event?.data && typeof event.data === "object" ? event.data : {};
|
|
11868
|
+
if (type === "session_start") {
|
|
11869
|
+
return clampRunSummaryText(`Started ${String(event.executor || event.model || "agent").trim() || "agent"} run for ${String(event.task_title || event.taskId || "task").trim() || "task"}`);
|
|
11870
|
+
}
|
|
11871
|
+
if (type === "session_end") {
|
|
11872
|
+
const status = String(data?.completion_status || event?.status || "completed").trim() || "completed";
|
|
11873
|
+
return clampRunSummaryText(`Finished run with status ${status}`);
|
|
11874
|
+
}
|
|
11875
|
+
if (type === "tool_call") {
|
|
11876
|
+
const toolName = String(data?.tool_name || event?.tool_name || "tool").trim() || "tool";
|
|
11877
|
+
return clampRunSummaryText(`Called ${toolName}`);
|
|
11878
|
+
}
|
|
11879
|
+
if (type === "tool_result") {
|
|
11880
|
+
const toolName = String(data?.tool_name || event?.tool_name || "tool").trim() || "tool";
|
|
11881
|
+
const status = String(data?.status || event?.status || "completed").trim() || "completed";
|
|
11882
|
+
return clampRunSummaryText(`${toolName} returned ${status}`);
|
|
11883
|
+
}
|
|
11884
|
+
if (type === "error") {
|
|
11885
|
+
const message = String(data?.error_message || event?.error_message || event?.message || "error").trim() || "error";
|
|
11886
|
+
return clampRunSummaryText(`Error: ${message}`);
|
|
11887
|
+
}
|
|
11888
|
+
if (type === "usage") {
|
|
11889
|
+
const totalTokens = numberOrZero(data?.total_tokens || data?.tokens || event?.total_tokens);
|
|
11890
|
+
if (totalTokens > 0) return clampRunSummaryText(`Used ${totalTokens} tokens`);
|
|
11891
|
+
}
|
|
11892
|
+
if (type === "agent_output") {
|
|
11893
|
+
const output = String(data?.output || event?.output || "").trim();
|
|
11894
|
+
if (output) return clampRunSummaryText(output);
|
|
11895
|
+
}
|
|
11896
|
+
return clampRunSummaryText(type.replace(/_/g, " "));
|
|
11897
|
+
}
|
|
11898
|
+
|
|
11899
|
+
function toTrajectoryReplayEvent(event = {}, index = 0) {
|
|
11900
|
+
const timestamp = event?.timestamp || event?.recordedAt || null;
|
|
11901
|
+
const eventType = String(event?.event_type || event?.type || "event").trim() || "event";
|
|
11902
|
+
const data = event?.data && typeof event.data === "object" ? event.data : {};
|
|
11903
|
+
return {
|
|
11904
|
+
index,
|
|
11905
|
+
timestamp,
|
|
11906
|
+
type: eventType,
|
|
11907
|
+
summary: summarizeTrajectoryEvent(event),
|
|
11908
|
+
attemptId: String(event?.attempt_id || event?.attemptId || "").trim() || null,
|
|
11909
|
+
taskId: String(event?.taskId || "").trim() || null,
|
|
11910
|
+
taskTitle: String(event?.task_title || event?.taskTitle || "").trim() || null,
|
|
11911
|
+
executor: String(event?.executor || event?.model || "").trim() || null,
|
|
11912
|
+
data: makeJsonSafe(data, { maxDepth: 4 }),
|
|
11913
|
+
};
|
|
11914
|
+
}
|
|
11915
|
+
|
|
11916
|
+
function buildReplayOverview(events = []) {
|
|
11917
|
+
const totals = {
|
|
11918
|
+
events: events.length,
|
|
11919
|
+
toolCalls: 0,
|
|
11920
|
+
toolResults: 0,
|
|
11921
|
+
outputs: 0,
|
|
11922
|
+
errors: 0,
|
|
11923
|
+
usageEvents: 0,
|
|
11924
|
+
};
|
|
11925
|
+
const shortSteps = [];
|
|
11926
|
+
for (const event of events) {
|
|
11927
|
+
if (event.type === "tool_call") totals.toolCalls += 1;
|
|
11928
|
+
if (event.type === "tool_result") totals.toolResults += 1;
|
|
11929
|
+
if (event.type === "agent_output") totals.outputs += 1;
|
|
11930
|
+
if (event.type === "error") totals.errors += 1;
|
|
11931
|
+
if (event.type === "usage") totals.usageEvents += 1;
|
|
11932
|
+
if (shortSteps.length >= 12) continue;
|
|
11933
|
+
if (["session_start", "tool_call", "tool_result", "error", "session_end"].includes(event.type)) {
|
|
11934
|
+
shortSteps.push(event.summary);
|
|
11935
|
+
continue;
|
|
11936
|
+
}
|
|
11937
|
+
if (event.type === "agent_output" && shortSteps.length < 6) shortSteps.push(event.summary);
|
|
11938
|
+
}
|
|
11939
|
+
return { totals, shortSteps };
|
|
11940
|
+
}
|
|
11941
|
+
|
|
11942
|
+
async function listReplayableAgentRuns(options = {}) {
|
|
11943
|
+
const logDir = resolveAgentWorkLogDir();
|
|
11944
|
+
const sessionsDir = resolve(logDir, "agent-sessions");
|
|
11945
|
+
const limit = Math.max(1, Math.min(100, Number(options.limit) || 25));
|
|
11946
|
+
const taskIdFilter = String(options.taskId || "").trim();
|
|
11947
|
+
const files = await readdir(sessionsDir).catch(() => []);
|
|
11948
|
+
const runs = [];
|
|
11949
|
+
|
|
11950
|
+
for (const name of files) {
|
|
11951
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
11952
|
+
const attemptId = name.replace(/\.jsonl$/i, "");
|
|
11953
|
+
const filePath = resolve(sessionsDir, name);
|
|
11954
|
+
const entries = await readJsonlTail(filePath, 5000, 10000000).catch(() => []);
|
|
11955
|
+
if (!entries.length) continue;
|
|
11956
|
+
const replayEvents = entries.map((entry, index) => toTrajectoryReplayEvent(entry, index));
|
|
11957
|
+
const first = replayEvents[0] || null;
|
|
11958
|
+
const last = replayEvents[replayEvents.length - 1] || null;
|
|
11959
|
+
const taskId = String(first?.taskId || last?.taskId || "").trim() || null;
|
|
11960
|
+
if (taskIdFilter && taskId !== taskIdFilter) continue;
|
|
11961
|
+
const startedAt = first?.timestamp || null;
|
|
11962
|
+
const endedAt = last?.type === "session_end" ? last.timestamp : null;
|
|
11963
|
+
const overview = buildReplayOverview(replayEvents);
|
|
11964
|
+
runs.push({
|
|
11965
|
+
attemptId,
|
|
11966
|
+
taskId,
|
|
11967
|
+
taskTitle: first?.taskTitle || last?.taskTitle || null,
|
|
11968
|
+
executor: first?.executor || last?.executor || null,
|
|
11969
|
+
startedAt,
|
|
11970
|
+
endedAt,
|
|
11971
|
+
status: last?.type === "session_end"
|
|
11972
|
+
? String(last?.data?.completion_status || "completed")
|
|
11973
|
+
: "in_progress",
|
|
11974
|
+
eventCount: replayEvents.length,
|
|
11975
|
+
shortSteps: overview.shortSteps,
|
|
11976
|
+
totals: overview.totals,
|
|
11977
|
+
});
|
|
11978
|
+
}
|
|
11979
|
+
|
|
11980
|
+
runs.sort((a, b) => {
|
|
11981
|
+
const aTs = Date.parse(String(a.endedAt || a.startedAt || 0)) || 0;
|
|
11982
|
+
const bTs = Date.parse(String(b.endedAt || b.startedAt || 0)) || 0;
|
|
11983
|
+
return bTs - aTs;
|
|
11984
|
+
});
|
|
11985
|
+
return runs.slice(0, limit);
|
|
11986
|
+
}
|
|
11987
|
+
|
|
11988
|
+
async function readReplayableAgentRun(attemptId) {
|
|
11989
|
+
const normalizedAttemptId = String(attemptId || "").trim();
|
|
11990
|
+
if (!normalizedAttemptId) return null;
|
|
11991
|
+
const filePath = resolve(resolveAgentWorkLogDir(), "agent-sessions", `${normalizedAttemptId}.jsonl`);
|
|
11992
|
+
if (!existsSync(filePath)) return null;
|
|
11993
|
+
const entries = await readJsonlTail(filePath, 20000, 25000000).catch(() => []);
|
|
11994
|
+
if (!entries.length) return null;
|
|
11995
|
+
const events = entries.map((entry, index) => toTrajectoryReplayEvent(entry, index));
|
|
11996
|
+
const first = events[0] || null;
|
|
11997
|
+
const last = events[events.length - 1] || null;
|
|
11998
|
+
const overview = buildReplayOverview(events);
|
|
11999
|
+
return {
|
|
12000
|
+
attemptId: normalizedAttemptId,
|
|
12001
|
+
taskId: first?.taskId || last?.taskId || null,
|
|
12002
|
+
taskTitle: first?.taskTitle || last?.taskTitle || null,
|
|
12003
|
+
executor: first?.executor || last?.executor || null,
|
|
12004
|
+
startedAt: first?.timestamp || null,
|
|
12005
|
+
endedAt: last?.type === "session_end" ? last.timestamp : null,
|
|
12006
|
+
status: last?.type === "session_end"
|
|
12007
|
+
? String(last?.data?.completion_status || "completed")
|
|
12008
|
+
: "in_progress",
|
|
12009
|
+
shortSteps: overview.shortSteps,
|
|
12010
|
+
totals: overview.totals,
|
|
12011
|
+
events,
|
|
12012
|
+
};
|
|
12013
|
+
}
|
|
11309
12014
|
async function listAgentLogFiles(query = "", limit = 60) {
|
|
11310
12015
|
const entries = [];
|
|
11311
12016
|
const agentLogsDir = await resolveAgentLogsDir();
|
|
@@ -12166,6 +12871,25 @@ async function handleApi(req, res, url) {
|
|
|
12166
12871
|
return;
|
|
12167
12872
|
}
|
|
12168
12873
|
|
|
12874
|
+
if (path === "/api/tasks/diff") {
|
|
12875
|
+
try {
|
|
12876
|
+
const taskId =
|
|
12877
|
+
url.searchParams.get("taskId") || url.searchParams.get("id") || "";
|
|
12878
|
+
if (!taskId) {
|
|
12879
|
+
jsonResponse(res, 400, { ok: false, error: "taskId required" });
|
|
12880
|
+
return;
|
|
12881
|
+
}
|
|
12882
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
12883
|
+
const adapter = getKanbanAdapter();
|
|
12884
|
+
const task = await adapter.getTask(taskId);
|
|
12885
|
+
const payload = await buildTaskDiffPayload(task, workspaceContext);
|
|
12886
|
+
jsonResponse(res, 200, { ok: true, ...payload });
|
|
12887
|
+
} catch (err) {
|
|
12888
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
12889
|
+
}
|
|
12890
|
+
return;
|
|
12891
|
+
}
|
|
12892
|
+
|
|
12169
12893
|
if (path === "/api/tasks/detail") {
|
|
12170
12894
|
try {
|
|
12171
12895
|
const taskId =
|
|
@@ -12215,6 +12939,15 @@ async function handleApi(req, res, url) {
|
|
|
12215
12939
|
workspaceDir: workspaceContext?.workspaceDir || repoRoot,
|
|
12216
12940
|
});
|
|
12217
12941
|
const diagnostics = buildTaskDiagnostics(detailTask, supervisorDiagnostics);
|
|
12942
|
+
const replayRuns = await buildReplayableTaskRuns(detailTask, getSessionTracker(), 8);
|
|
12943
|
+
if (replayRuns.length > 0) {
|
|
12944
|
+
detailTask.runs = replayRuns;
|
|
12945
|
+
detailTask.meta = {
|
|
12946
|
+
...(detailTask.meta || {}),
|
|
12947
|
+
latestRunSummary: replayRuns[0]?.summary || null,
|
|
12948
|
+
replayRunCount: replayRuns.length,
|
|
12949
|
+
};
|
|
12950
|
+
}
|
|
12218
12951
|
|
|
12219
12952
|
detailTask.meta = {
|
|
12220
12953
|
...(detailTask.meta || {}),
|
|
@@ -15266,7 +15999,15 @@ async function handleApi(req, res, url) {
|
|
|
15266
15999
|
try {
|
|
15267
16000
|
const worktrees = listActiveWorktrees(repoRoot);
|
|
15268
16001
|
const stats = await getWorktreeStats(repoRoot);
|
|
15269
|
-
|
|
16002
|
+
const recovery = await readWorktreeRecoveryState(repoRoot);
|
|
16003
|
+
jsonResponse(res, 200, {
|
|
16004
|
+
ok: true,
|
|
16005
|
+
data: worktrees,
|
|
16006
|
+
stats: {
|
|
16007
|
+
...stats,
|
|
16008
|
+
recovery,
|
|
16009
|
+
},
|
|
16010
|
+
});
|
|
15270
16011
|
} catch (err) {
|
|
15271
16012
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
15272
16013
|
}
|
|
@@ -15497,25 +16238,42 @@ async function handleApi(req, res, url) {
|
|
|
15497
16238
|
const days = Number(url.searchParams.get("days") || "7");
|
|
15498
16239
|
const logDir = resolveAgentWorkLogDir();
|
|
15499
16240
|
const metricsPath = resolve(logDir, "agent-metrics.jsonl");
|
|
15500
|
-
const metrics = await readJsonlTail(metricsPath,
|
|
16241
|
+
const metrics = await readJsonlTail(metricsPath, 100_000, 50_000_000);
|
|
15501
16242
|
const summary = summarizeTelemetry(metrics, days) || {};
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
|
|
16243
|
+
|
|
16244
|
+
// Read lifetime totals from the full JSONL log (not the capped in-memory state)
|
|
16245
|
+
// so the count is accurate even when sessions exceed the in-memory cap.
|
|
16246
|
+
const { entries: allSessions } = await readCompletedSessionEntries(200_000);
|
|
16247
|
+
const lifetimeTotals = allSessions.reduce(
|
|
15505
16248
|
(acc, session) => {
|
|
15506
16249
|
acc.attemptsCount += 1;
|
|
15507
16250
|
acc.tokenCount += Number(session?.tokenCount || 0);
|
|
15508
16251
|
acc.inputTokens += Number(session?.inputTokens || 0);
|
|
15509
16252
|
acc.outputTokens += Number(session?.outputTokens || 0);
|
|
16253
|
+
acc.durationMs += Math.max(0, Number(session?.durationMs || 0));
|
|
15510
16254
|
return acc;
|
|
15511
16255
|
},
|
|
15512
|
-
{ attemptsCount: 0, tokenCount: 0, inputTokens: 0, outputTokens: 0 },
|
|
16256
|
+
{ attemptsCount: 0, tokenCount: 0, inputTokens: 0, outputTokens: 0, durationMs: 0 },
|
|
15513
16257
|
);
|
|
15514
|
-
|
|
15515
|
-
|
|
15516
|
-
|
|
15517
|
-
|
|
15518
|
-
|
|
16258
|
+
|
|
16259
|
+
// Supplement token counts from agent-metrics.jsonl which has actual LLM usage data
|
|
16260
|
+
// (prompt_tokens, completion_tokens, total_tokens) that the session log may lack.
|
|
16261
|
+
if (lifetimeTotals.tokenCount <= 0 && metrics.length > 0) {
|
|
16262
|
+
let metricsTokens = 0;
|
|
16263
|
+
let metricsInputTokens = 0;
|
|
16264
|
+
let metricsOutputTokens = 0;
|
|
16265
|
+
for (const m of metrics) {
|
|
16266
|
+
const met = m?.metrics || m;
|
|
16267
|
+
metricsTokens += Number(met?.total_tokens || 0);
|
|
16268
|
+
metricsInputTokens += Number(met?.prompt_tokens || 0);
|
|
16269
|
+
metricsOutputTokens += Number(met?.completion_tokens || 0);
|
|
16270
|
+
}
|
|
16271
|
+
lifetimeTotals.tokenCount = metricsTokens;
|
|
16272
|
+
lifetimeTotals.inputTokens = metricsInputTokens;
|
|
16273
|
+
lifetimeTotals.outputTokens = metricsOutputTokens;
|
|
16274
|
+
}
|
|
16275
|
+
|
|
16276
|
+
summary.lifetimeTotals = lifetimeTotals;
|
|
15519
16277
|
jsonResponse(res, 200, { ok: true, data: summary });
|
|
15520
16278
|
} catch (err) {
|
|
15521
16279
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
@@ -15807,7 +16565,33 @@ async function handleApi(req, res, url) {
|
|
|
15807
16565
|
return;
|
|
15808
16566
|
}
|
|
15809
16567
|
|
|
15810
|
-
|
|
16568
|
+
if (path === "/api/agent-runs") {
|
|
16569
|
+
try {
|
|
16570
|
+
const limit = Number(url.searchParams.get("limit") || "25");
|
|
16571
|
+
const taskId = String(url.searchParams.get("taskId") || "").trim();
|
|
16572
|
+
const data = await listReplayableAgentRuns({ limit, taskId });
|
|
16573
|
+
jsonResponse(res, 200, { ok: true, data });
|
|
16574
|
+
} catch (err) {
|
|
16575
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
16576
|
+
}
|
|
16577
|
+
return;
|
|
16578
|
+
}
|
|
16579
|
+
|
|
16580
|
+
if (path.startsWith("/api/agent-runs/")) {
|
|
16581
|
+
try {
|
|
16582
|
+
const attemptId = decodeURIComponent(path.slice("/api/agent-runs/".length));
|
|
16583
|
+
const data = await readReplayableAgentRun(attemptId);
|
|
16584
|
+
if (!data) {
|
|
16585
|
+
jsonResponse(res, 404, { ok: false, error: "run not found" });
|
|
16586
|
+
return;
|
|
16587
|
+
}
|
|
16588
|
+
jsonResponse(res, 200, { ok: true, data });
|
|
16589
|
+
} catch (err) {
|
|
16590
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
16591
|
+
}
|
|
16592
|
+
return;
|
|
16593
|
+
}
|
|
16594
|
+
if (path === "/api/agent-logs/context") {
|
|
15811
16595
|
try {
|
|
15812
16596
|
const query = url.searchParams.get("query") || "";
|
|
15813
16597
|
if (!query) {
|
|
@@ -15906,6 +16690,7 @@ async function handleApi(req, res, url) {
|
|
|
15906
16690
|
try {
|
|
15907
16691
|
const executor = uiDeps.getInternalExecutor?.();
|
|
15908
16692
|
const status = executor?.getStatus?.() || {};
|
|
16693
|
+
const worktreeRecovery = await readWorktreeRecoveryState(repoRoot);
|
|
15909
16694
|
const data = {
|
|
15910
16695
|
executor: {
|
|
15911
16696
|
mode: uiDeps.getExecutorMode?.() || "internal",
|
|
@@ -15913,6 +16698,7 @@ async function handleApi(req, res, url) {
|
|
|
15913
16698
|
activeSlots: status.activeSlots || 0,
|
|
15914
16699
|
paused: executor?.isPaused?.() || false,
|
|
15915
16700
|
},
|
|
16701
|
+
worktreeRecovery,
|
|
15916
16702
|
system: {
|
|
15917
16703
|
uptime: process.uptime(),
|
|
15918
16704
|
memoryMB: Math.round(process.memoryUsage.rss() / 1024 / 1024),
|
|
@@ -18833,7 +19619,23 @@ async function handleApi(req, res, url) {
|
|
|
18833
19619
|
const detailed = tracker.getSessionById(session.id) || session;
|
|
18834
19620
|
return sessionMatchesWorkspaceContext(detailed, workspaceContext);
|
|
18835
19621
|
});
|
|
18836
|
-
jsonResponse(res, 200, {
|
|
19622
|
+
jsonResponse(res, 200, {
|
|
19623
|
+
ok: true,
|
|
19624
|
+
sessions,
|
|
19625
|
+
loadMeta: {
|
|
19626
|
+
stale: false,
|
|
19627
|
+
lastSuccessAt: new Date().toISOString(),
|
|
19628
|
+
lastFailureAt: null,
|
|
19629
|
+
staleReason: null,
|
|
19630
|
+
staleReasonCode: null,
|
|
19631
|
+
staleReasonLabel: null,
|
|
19632
|
+
staleReasonMeta: null,
|
|
19633
|
+
retryAttempt: 0,
|
|
19634
|
+
retryDelayMs: 0,
|
|
19635
|
+
nextRetryAt: null,
|
|
19636
|
+
retriesExhausted: false,
|
|
19637
|
+
},
|
|
19638
|
+
});
|
|
18837
19639
|
} catch (err) {
|
|
18838
19640
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
18839
19641
|
}
|
|
@@ -18869,6 +19671,7 @@ async function handleApi(req, res, url) {
|
|
|
18869
19671
|
});
|
|
18870
19672
|
jsonResponse(res, 200, { ok: true, session: { id: session.id, type: session.type, status: session.status, metadata: session.metadata } });
|
|
18871
19673
|
broadcastUiEvent(["sessions"], "invalidate", { reason: "session-created", sessionId: id });
|
|
19674
|
+
broadcastSessionsSnapshot();
|
|
18872
19675
|
} catch (err) {
|
|
18873
19676
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
18874
19677
|
}
|
|
@@ -19019,6 +19822,7 @@ async function handleApi(req, res, url) {
|
|
|
19019
19822
|
reason: wasRunning ? "session-stop-requested" : "session-stop-noop",
|
|
19020
19823
|
sessionId,
|
|
19021
19824
|
});
|
|
19825
|
+
broadcastSessionsSnapshot();
|
|
19022
19826
|
} catch (err) {
|
|
19023
19827
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
19024
19828
|
}
|
|
@@ -19071,6 +19875,7 @@ async function handleApi(req, res, url) {
|
|
|
19071
19875
|
// Respond immediately so the UI doesn't block on agent execution
|
|
19072
19876
|
jsonResponse(res, 200, { ok: true, messageId });
|
|
19073
19877
|
broadcastUiEvent(["sessions"], "invalidate", { reason: "session-message", sessionId });
|
|
19878
|
+
broadcastSessionsSnapshot();
|
|
19074
19879
|
|
|
19075
19880
|
// Build an onEvent callback so intermediate SDK events (thinking,
|
|
19076
19881
|
// tool calls, code edits, etc.) are streamed to the UI in real-time
|
|
@@ -19122,6 +19927,7 @@ async function handleApi(req, res, url) {
|
|
|
19122
19927
|
// sessions from staying "active" forever and causing session bloat.
|
|
19123
19928
|
tracker.updateSessionStatus(sessionId, "completed");
|
|
19124
19929
|
broadcastUiEvent(["sessions"], "invalidate", { reason: "agent-response", sessionId });
|
|
19930
|
+
broadcastSessionsSnapshot();
|
|
19125
19931
|
}).catch((execErr) => {
|
|
19126
19932
|
const wasAborted =
|
|
19127
19933
|
abortController.signal.aborted ||
|
|
@@ -19139,6 +19945,7 @@ async function handleApi(req, res, url) {
|
|
|
19139
19945
|
reason: "agent-stopped",
|
|
19140
19946
|
sessionId,
|
|
19141
19947
|
});
|
|
19948
|
+
broadcastSessionsSnapshot();
|
|
19142
19949
|
return;
|
|
19143
19950
|
}
|
|
19144
19951
|
// Record error as system message so user sees feedback
|
|
@@ -19150,6 +19957,7 @@ async function handleApi(req, res, url) {
|
|
|
19150
19957
|
});
|
|
19151
19958
|
tracker.updateSessionStatus(sessionId, "failed");
|
|
19152
19959
|
broadcastUiEvent(["sessions"], "invalidate", { reason: "agent-error", sessionId });
|
|
19960
|
+
broadcastSessionsSnapshot();
|
|
19153
19961
|
}).finally(() => {
|
|
19154
19962
|
// Clear only if this turn still owns the session abort controller.
|
|
19155
19963
|
if (sessionRunAbortControllers.get(sessionId) === abortController) {
|
|
@@ -19176,6 +19984,7 @@ async function handleApi(req, res, url) {
|
|
|
19176
19984
|
});
|
|
19177
19985
|
jsonResponse(res, 200, { ok: true, messageId, warning: "no_agent_available" });
|
|
19178
19986
|
broadcastUiEvent(["sessions"], "invalidate", { reason: "session-message", sessionId });
|
|
19987
|
+
broadcastSessionsSnapshot();
|
|
19179
19988
|
}
|
|
19180
19989
|
} catch (err) {
|
|
19181
19990
|
console.error("[ui-server] session message failed for %s: %s", String(sessionId), String(err?.message || err || "unknown"));
|
|
@@ -19215,6 +20024,7 @@ async function handleApi(req, res, url) {
|
|
|
19215
20024
|
reason: "session-message-edited",
|
|
19216
20025
|
sessionId,
|
|
19217
20026
|
});
|
|
20027
|
+
broadcastSessionsSnapshot();
|
|
19218
20028
|
} catch (err) {
|
|
19219
20029
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
19220
20030
|
}
|
|
@@ -19234,6 +20044,24 @@ async function handleApi(req, res, url) {
|
|
|
19234
20044
|
reason: "session-archived",
|
|
19235
20045
|
sessionId,
|
|
19236
20046
|
});
|
|
20047
|
+
broadcastSessionsSnapshot();
|
|
20048
|
+
} catch (err) {
|
|
20049
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
20050
|
+
}
|
|
20051
|
+
return;
|
|
20052
|
+
}
|
|
20053
|
+
|
|
20054
|
+
if (action === "pause" && req.method === "POST") {
|
|
20055
|
+
try {
|
|
20056
|
+
const session = getScopedSession();
|
|
20057
|
+
if (!session) {
|
|
20058
|
+
jsonResponse(res, 404, { ok: false, error: "Session not found" });
|
|
20059
|
+
return;
|
|
20060
|
+
}
|
|
20061
|
+
tracker.updateSessionStatus(sessionId, "paused");
|
|
20062
|
+
jsonResponse(res, 200, { ok: true });
|
|
20063
|
+
broadcastUiEvent(["sessions"], "invalidate", { reason: "session-paused", sessionId });
|
|
20064
|
+
broadcastSessionsSnapshot();
|
|
19237
20065
|
} catch (err) {
|
|
19238
20066
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
19239
20067
|
}
|
|
@@ -19250,6 +20078,7 @@ async function handleApi(req, res, url) {
|
|
|
19250
20078
|
tracker.updateSessionStatus(sessionId, "active");
|
|
19251
20079
|
jsonResponse(res, 200, { ok: true });
|
|
19252
20080
|
broadcastUiEvent(["sessions"], "invalidate", { reason: "session-resumed", sessionId });
|
|
20081
|
+
broadcastSessionsSnapshot();
|
|
19253
20082
|
} catch (err) {
|
|
19254
20083
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
19255
20084
|
}
|
|
@@ -19269,6 +20098,7 @@ async function handleApi(req, res, url) {
|
|
|
19269
20098
|
reason: "session-deleted",
|
|
19270
20099
|
sessionId,
|
|
19271
20100
|
});
|
|
20101
|
+
broadcastSessionsSnapshot();
|
|
19272
20102
|
} catch (err) {
|
|
19273
20103
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
19274
20104
|
}
|
|
@@ -19291,6 +20121,7 @@ async function handleApi(req, res, url) {
|
|
|
19291
20121
|
tracker.renameSession(sessionId, title);
|
|
19292
20122
|
jsonResponse(res, 200, { ok: true });
|
|
19293
20123
|
broadcastUiEvent(["sessions"], "invalidate", { reason: "session-renamed", sessionId });
|
|
20124
|
+
broadcastSessionsSnapshot();
|
|
19294
20125
|
} catch (err) {
|
|
19295
20126
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
19296
20127
|
}
|
|
@@ -19320,10 +20151,20 @@ async function handleApi(req, res, url) {
|
|
|
19320
20151
|
jsonResponse(res, 200, { ok: true, diff: { files: [], totalFiles: 0, totalAdditions: 0, totalDeletions: 0, formatted: "(no worktree)" }, summary: "(no worktree)", commits: [] });
|
|
19321
20152
|
return;
|
|
19322
20153
|
}
|
|
19323
|
-
const stats = collectDiffStats(worktreePath);
|
|
19324
|
-
const summary = getCompactDiffSummary(worktreePath);
|
|
20154
|
+
const stats = collectDiffStats(worktreePath, { includePatch: true });
|
|
20155
|
+
const summary = stats.formatted || getCompactDiffSummary(worktreePath);
|
|
19325
20156
|
const commits = getRecentCommits(worktreePath);
|
|
19326
|
-
jsonResponse(res, 200, {
|
|
20157
|
+
jsonResponse(res, 200, {
|
|
20158
|
+
ok: true,
|
|
20159
|
+
diff: stats,
|
|
20160
|
+
summary,
|
|
20161
|
+
commits,
|
|
20162
|
+
source: {
|
|
20163
|
+
kind: "session",
|
|
20164
|
+
label: stats.sourceRange || "origin/main...HEAD",
|
|
20165
|
+
detail: worktreePath,
|
|
20166
|
+
},
|
|
20167
|
+
});
|
|
19327
20168
|
} catch (err) {
|
|
19328
20169
|
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
19329
20170
|
}
|
|
@@ -21052,13 +21893,45 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
21052
21893
|
wsServer = new WebSocketServer({ noServer: true });
|
|
21053
21894
|
if (!sessionListenerAttached) {
|
|
21054
21895
|
sessionListenerAttached = true;
|
|
21055
|
-
addSessionEventListener((payload) => {
|
|
21896
|
+
removeSessionEventListener = addSessionEventListener((payload) => {
|
|
21056
21897
|
broadcastSessionMessage(payload);
|
|
21057
21898
|
});
|
|
21058
21899
|
}
|
|
21900
|
+
if (!sessionStateListenerAttached) {
|
|
21901
|
+
sessionStateListenerAttached = true;
|
|
21902
|
+
removeSessionStateListener = addSessionStateListener((payload) => {
|
|
21903
|
+
broadcastTuiSessionsSnapshot(payload?.reason || payload?.event?.reason || "updated", payload || {});
|
|
21904
|
+
});
|
|
21905
|
+
}
|
|
21906
|
+
if (!activeSessionListenerAttached) {
|
|
21907
|
+
activeSessionListenerAttached = true;
|
|
21908
|
+
removeActiveSessionListener = addActiveSessionListener((sessions, detail = {}) => {
|
|
21909
|
+
const snapshot = getCurrentSessionSnapshot();
|
|
21910
|
+
broadcastCanonicalEvent(["sessions", "tui"], "sessions:update", snapshot);
|
|
21911
|
+
if (detail?.taskKey) {
|
|
21912
|
+
const session = snapshot.find((entry) => String(entry?.taskId || entry?.id || "").trim() === String(detail.taskKey || "").trim()) || {
|
|
21913
|
+
id: String(detail.taskKey || "").trim(),
|
|
21914
|
+
taskId: String(detail.taskKey || "").trim(),
|
|
21915
|
+
type: "task",
|
|
21916
|
+
status: "active",
|
|
21917
|
+
lastActiveAt: new Date().toISOString(),
|
|
21918
|
+
turnCount: 0,
|
|
21919
|
+
};
|
|
21920
|
+
broadcastCanonicalEvent(["sessions", "tui"], "session:event", buildSessionEventPayload({
|
|
21921
|
+
sessionId: session?.id || detail.taskKey,
|
|
21922
|
+
taskId: session?.taskId || detail.taskKey,
|
|
21923
|
+
session,
|
|
21924
|
+
event: {
|
|
21925
|
+
kind: "state",
|
|
21926
|
+
reason: detail?.reason || "update",
|
|
21927
|
+
},
|
|
21928
|
+
}));
|
|
21929
|
+
}
|
|
21930
|
+
});
|
|
21931
|
+
}
|
|
21059
21932
|
if (!sessionAccumulatorListenerAttached) {
|
|
21060
21933
|
sessionAccumulatorListenerAttached = true;
|
|
21061
|
-
addSessionAccumulationListener((payload) => {
|
|
21934
|
+
removeSessionAccumulatorListener = addSessionAccumulationListener((payload) => {
|
|
21062
21935
|
broadcastUiEvent(["tasks", "overview", "telemetry", "sessions"], "invalidate", {
|
|
21063
21936
|
reason: "session-accumulated",
|
|
21064
21937
|
taskId: payload?.taskId || null,
|
|
@@ -21068,22 +21941,16 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
21068
21941
|
});
|
|
21069
21942
|
}
|
|
21070
21943
|
|
|
21071
|
-
|
|
21072
|
-
|
|
21073
|
-
|
|
21074
|
-
|
|
21075
|
-
|
|
21076
|
-
|
|
21077
|
-
|
|
21078
|
-
|
|
21079
|
-
|
|
21080
|
-
|
|
21081
|
-
// best effort
|
|
21082
|
-
}
|
|
21083
|
-
}, intervalMs);
|
|
21084
|
-
statsBroadcastInterval.unref?.();
|
|
21085
|
-
}
|
|
21086
|
-
startStatsBroadcast();
|
|
21944
|
+
tuiStatsEmitter?.stop?.();
|
|
21945
|
+
tuiStatsEmitter = createTuiStatsEmitter({
|
|
21946
|
+
intervalMs: Number(process.env.BOSUN_STATS_BROADCAST_MS) || 2000,
|
|
21947
|
+
getPayload: () => buildCurrentTuiMonitorStats(),
|
|
21948
|
+
emit: (stats) => {
|
|
21949
|
+
broadcastCanonicalEvent(["monitor", "stats", "tui"], "monitor:stats", stats);
|
|
21950
|
+
broadcastUiEvent(["stats", "tui"], "stats", stats);
|
|
21951
|
+
},
|
|
21952
|
+
});
|
|
21953
|
+
tuiStatsEmitter.start();
|
|
21087
21954
|
|
|
21088
21955
|
// Retry queue tracking
|
|
21089
21956
|
let _retryQueue = { count: 0, items: [] };
|
|
@@ -21118,10 +21985,7 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
21118
21985
|
let _activeSessions = [];
|
|
21119
21986
|
function updateActiveSessions(sessions) {
|
|
21120
21987
|
_activeSessions = sessions || [];
|
|
21121
|
-
|
|
21122
|
-
for (const session of _activeSessions) {
|
|
21123
|
-
broadcastUiEvent(["sessions", "tui"], "session:update", session);
|
|
21124
|
-
}
|
|
21988
|
+
broadcastTuiSessionsSnapshot("active-sessions", { sessions: _activeSessions });
|
|
21125
21989
|
}
|
|
21126
21990
|
|
|
21127
21991
|
// Task CRUD events
|
|
@@ -21142,6 +22006,18 @@ export async function startTelegramUiServer(options = {}) {
|
|
|
21142
22006
|
payload: { connected: true },
|
|
21143
22007
|
ts: Date.now(),
|
|
21144
22008
|
});
|
|
22009
|
+
sendWsMessage(socket, {
|
|
22010
|
+
type: "sessions:update",
|
|
22011
|
+
channels: ["sessions", "tui"],
|
|
22012
|
+
payload: getCurrentSessionSnapshot(),
|
|
22013
|
+
ts: Date.now(),
|
|
22014
|
+
});
|
|
22015
|
+
sendWsMessage(socket, {
|
|
22016
|
+
type: "monitor:stats",
|
|
22017
|
+
channels: ["monitor", "stats", "tui"],
|
|
22018
|
+
payload: buildCurrentTuiMonitorStats(),
|
|
22019
|
+
ts: Date.now(),
|
|
22020
|
+
});
|
|
21145
22021
|
|
|
21146
22022
|
socket.on("message", (raw) => {
|
|
21147
22023
|
try {
|
|
@@ -21637,9 +22513,24 @@ export function stopTelegramUiServer() {
|
|
|
21637
22513
|
if (!uiServer) return;
|
|
21638
22514
|
stopTunnel();
|
|
21639
22515
|
stopWsHeartbeat();
|
|
22516
|
+
_activeSessions = [];
|
|
21640
22517
|
// Clear injected configDir so it does not leak between server lifecycles
|
|
21641
22518
|
// (tests start/stop servers repeatedly with different config directories).
|
|
21642
22519
|
delete uiDeps.configDir;
|
|
22520
|
+
tuiStatsEmitter?.stop?.();
|
|
22521
|
+
tuiStatsEmitter = null;
|
|
22522
|
+
removeSessionEventListener?.();
|
|
22523
|
+
removeSessionEventListener = null;
|
|
22524
|
+
sessionListenerAttached = false;
|
|
22525
|
+
removeSessionStateListener?.();
|
|
22526
|
+
removeSessionStateListener = null;
|
|
22527
|
+
sessionStateListenerAttached = false;
|
|
22528
|
+
removeActiveSessionListener?.();
|
|
22529
|
+
removeActiveSessionListener = null;
|
|
22530
|
+
activeSessionListenerAttached = false;
|
|
22531
|
+
removeSessionAccumulatorListener?.();
|
|
22532
|
+
removeSessionAccumulatorListener = null;
|
|
22533
|
+
sessionAccumulatorListenerAttached = false;
|
|
21643
22534
|
for (const socket of wsClients) {
|
|
21644
22535
|
try {
|
|
21645
22536
|
stopLogStream(socket);
|
|
@@ -21674,3 +22565,6 @@ export function stopTelegramUiServer() {
|
|
|
21674
22565
|
}
|
|
21675
22566
|
|
|
21676
22567
|
export { getLocalLanIp };
|
|
22568
|
+
|
|
22569
|
+
|
|
22570
|
+
|