bosun 0.40.3 → 0.40.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 +4 -0
- package/cli.mjs +41 -2
- package/config/config.mjs +35 -15
- package/desktop/package.json +2 -2
- package/infra/monitor.mjs +44 -13
- package/infra/session-tracker.mjs +1 -0
- package/infra/sync-engine.mjs +6 -1
- package/infra/update-check.mjs +15 -7
- package/kanban/kanban-adapter.mjs +19 -4
- package/kanban/ve-orchestrator.ps1 +25 -0
- package/package.json +1 -1
- package/server/ui-server.mjs +385 -39
- package/ui/components/kanban-board.js +137 -9
- package/ui/components/shared.js +107 -45
- package/ui/demo-defaults.js +20 -20
- package/ui/modules/mui.js +600 -397
- package/ui/styles/kanban.css +66 -11
- package/ui/styles.monolith.css +89 -0
- package/ui/tabs/agents.js +194 -20
- package/ui/tabs/tasks.js +291 -70
- package/workflow/workflow-engine.mjs +0 -24
- package/workflow/workflow-nodes.mjs +219 -20
- package/workflow/workflow-templates.mjs +1 -1
- package/workflow-templates/task-batch.mjs +10 -10
- package/workspace/workspace-manager.mjs +11 -0
- package/workspace/worktree-manager.mjs +6 -0
package/server/ui-server.mjs
CHANGED
|
@@ -6422,6 +6422,150 @@ function applyInternalLifecycleTransition(taskId, action, options = {}) {
|
|
|
6422
6422
|
return null;
|
|
6423
6423
|
}
|
|
6424
6424
|
|
|
6425
|
+
async function persistTaskStatusForExecution(adapter, taskId, nextStatus, source) {
|
|
6426
|
+
if (!taskId || !nextStatus || !adapter) return null;
|
|
6427
|
+
const normalized = String(nextStatus || "").trim();
|
|
6428
|
+
if (!normalized) return null;
|
|
6429
|
+
let updated = null;
|
|
6430
|
+
if (typeof adapter.updateTaskStatus === "function") {
|
|
6431
|
+
updated = await adapter.updateTaskStatus(taskId, normalized, { source });
|
|
6432
|
+
} else if (typeof adapter.updateTask === "function") {
|
|
6433
|
+
updated = await adapter.updateTask(taskId, { status: normalized });
|
|
6434
|
+
}
|
|
6435
|
+
return withTaskMetadataTopLevel(updated);
|
|
6436
|
+
}
|
|
6437
|
+
|
|
6438
|
+
async function persistTaskExecutionMeta(adapter, taskId, executionPatch = {}) {
|
|
6439
|
+
if (!taskId || !adapter || typeof adapter.updateTask !== "function") return null;
|
|
6440
|
+
const current = typeof adapter.getTask === "function"
|
|
6441
|
+
? await adapter.getTask(taskId).catch(() => null)
|
|
6442
|
+
: null;
|
|
6443
|
+
const currentMeta = current?.meta && typeof current.meta === "object" ? current.meta : {};
|
|
6444
|
+
const currentExecution = currentMeta.execution && typeof currentMeta.execution === "object"
|
|
6445
|
+
? currentMeta.execution
|
|
6446
|
+
: {};
|
|
6447
|
+
const nextExecution = {
|
|
6448
|
+
...currentExecution,
|
|
6449
|
+
...executionPatch,
|
|
6450
|
+
};
|
|
6451
|
+
if (nextExecution.queueState == null) delete nextExecution.queueState;
|
|
6452
|
+
return withTaskMetadataTopLevel(await adapter.updateTask(taskId, {
|
|
6453
|
+
meta: {
|
|
6454
|
+
...currentMeta,
|
|
6455
|
+
execution: nextExecution,
|
|
6456
|
+
},
|
|
6457
|
+
}));
|
|
6458
|
+
}
|
|
6459
|
+
|
|
6460
|
+
function resolveFallbackStatusAfterFailedDispatch(previousStatus, startDispatch) {
|
|
6461
|
+
if (startDispatch?.reason === "no_free_slots") return "queued";
|
|
6462
|
+
const previous = String(previousStatus || "").trim();
|
|
6463
|
+
return previous || "todo";
|
|
6464
|
+
}
|
|
6465
|
+
|
|
6466
|
+
async function reconcileTaskAfterDispatchAttempt({
|
|
6467
|
+
adapter,
|
|
6468
|
+
taskId,
|
|
6469
|
+
previousStatus,
|
|
6470
|
+
requestedStatus,
|
|
6471
|
+
lifecycleAction,
|
|
6472
|
+
startDispatch,
|
|
6473
|
+
source,
|
|
6474
|
+
}) {
|
|
6475
|
+
const action = normalizeLifecycleAction(lifecycleAction);
|
|
6476
|
+
if (action !== "start" && action !== "resume") return null;
|
|
6477
|
+
const requested = normalizeTaskStatusKey(requestedStatus);
|
|
6478
|
+
if (requested !== "inprogress") return null;
|
|
6479
|
+
const targetStatus = startDispatch?.started
|
|
6480
|
+
? "inprogress"
|
|
6481
|
+
: resolveFallbackStatusAfterFailedDispatch(previousStatus, startDispatch);
|
|
6482
|
+
return persistTaskStatusForExecution(adapter, taskId, targetStatus, source);
|
|
6483
|
+
}
|
|
6484
|
+
|
|
6485
|
+
function buildTaskRuntimeSnapshot(task) {
|
|
6486
|
+
const runtimeExecutor = uiDeps.getInternalExecutor?.() || null;
|
|
6487
|
+
const status = runtimeExecutor?.getStatus?.() || {};
|
|
6488
|
+
const activeSlots = Array.isArray(status?.slots) ? status.slots : [];
|
|
6489
|
+
const taskId = String(task?.id || task?.taskId || "").trim();
|
|
6490
|
+
const slot = taskId
|
|
6491
|
+
? activeSlots.find((entry) => String(entry?.taskId || entry?.task_id || "").trim() === taskId)
|
|
6492
|
+
: null;
|
|
6493
|
+
const normalizedStatus = normalizeTaskStatusKey(task?.status);
|
|
6494
|
+
const queuedFlag = task?.meta?.execution?.queued === true
|
|
6495
|
+
|| normalizeTaskStatusKey(task?.meta?.execution?.queueState) === "queued";
|
|
6496
|
+
if (slot) {
|
|
6497
|
+
return {
|
|
6498
|
+
state: "running",
|
|
6499
|
+
isLive: true,
|
|
6500
|
+
taskId,
|
|
6501
|
+
taskStatus: task?.status || null,
|
|
6502
|
+
statusLabel: "Live execution",
|
|
6503
|
+
slot: {
|
|
6504
|
+
taskId,
|
|
6505
|
+
branch: slot?.branch || slot?.branchName || null,
|
|
6506
|
+
sdk: slot?.sdk || slot?.executor || null,
|
|
6507
|
+
model: slot?.model || null,
|
|
6508
|
+
startedAt: slot?.startedAt || slot?.started_at || null,
|
|
6509
|
+
completedCount: slot?.completedCount || 0,
|
|
6510
|
+
},
|
|
6511
|
+
executor: {
|
|
6512
|
+
activeSlots: Number(status?.activeSlots || activeSlots.length || 0),
|
|
6513
|
+
maxParallel: Number(status?.maxParallel || 0),
|
|
6514
|
+
paused: runtimeExecutor?.isPaused?.() === true,
|
|
6515
|
+
},
|
|
6516
|
+
};
|
|
6517
|
+
}
|
|
6518
|
+
if (queuedFlag || normalizedStatus === "queued") {
|
|
6519
|
+
return {
|
|
6520
|
+
state: "queued",
|
|
6521
|
+
isLive: false,
|
|
6522
|
+
taskId,
|
|
6523
|
+
taskStatus: task?.status || null,
|
|
6524
|
+
statusLabel: "Queued for execution",
|
|
6525
|
+
reason: "no_free_slots",
|
|
6526
|
+
};
|
|
6527
|
+
}
|
|
6528
|
+
if (normalizedStatus === "inprogress") {
|
|
6529
|
+
return {
|
|
6530
|
+
state: "pending",
|
|
6531
|
+
isLive: false,
|
|
6532
|
+
taskId,
|
|
6533
|
+
taskStatus: task?.status || null,
|
|
6534
|
+
statusLabel: "No live execution detected",
|
|
6535
|
+
reason: "no_active_executor_slot",
|
|
6536
|
+
};
|
|
6537
|
+
}
|
|
6538
|
+
if (normalizedStatus === "inreview") {
|
|
6539
|
+
return {
|
|
6540
|
+
state: "review",
|
|
6541
|
+
isLive: false,
|
|
6542
|
+
taskId,
|
|
6543
|
+
taskStatus: task?.status || null,
|
|
6544
|
+
statusLabel: "Awaiting review",
|
|
6545
|
+
};
|
|
6546
|
+
}
|
|
6547
|
+
return {
|
|
6548
|
+
state: "idle",
|
|
6549
|
+
isLive: false,
|
|
6550
|
+
taskId,
|
|
6551
|
+
taskStatus: task?.status || null,
|
|
6552
|
+
statusLabel: "No active execution",
|
|
6553
|
+
};
|
|
6554
|
+
}
|
|
6555
|
+
|
|
6556
|
+
function withTaskRuntimeSnapshot(task) {
|
|
6557
|
+
if (!task || typeof task !== "object") return task;
|
|
6558
|
+
const runtimeSnapshot = buildTaskRuntimeSnapshot(task);
|
|
6559
|
+
return {
|
|
6560
|
+
...task,
|
|
6561
|
+
runtimeSnapshot,
|
|
6562
|
+
meta: {
|
|
6563
|
+
...(task.meta || {}),
|
|
6564
|
+
runtimeSnapshot,
|
|
6565
|
+
},
|
|
6566
|
+
};
|
|
6567
|
+
}
|
|
6568
|
+
|
|
6425
6569
|
async function maybeStartTaskFromLifecycleAction({
|
|
6426
6570
|
taskId,
|
|
6427
6571
|
updatedTask,
|
|
@@ -6916,14 +7060,13 @@ async function resolveLogPath(logType, query) {
|
|
|
6916
7060
|
return resolvePreferredSystemLogPath();
|
|
6917
7061
|
}
|
|
6918
7062
|
if (logType === "agent") {
|
|
7063
|
+
const matches = await listAgentLogFiles(query, 1);
|
|
7064
|
+
if (matches.length > 0) {
|
|
7065
|
+
return resolve(matches[0].source, matches[0].name);
|
|
7066
|
+
}
|
|
6919
7067
|
const agentLogsDir = await resolveAgentLogsDir();
|
|
6920
7068
|
const files = await readdir(agentLogsDir).catch(() => []);
|
|
6921
|
-
|
|
6922
|
-
if (query) {
|
|
6923
|
-
const q = query.toLowerCase();
|
|
6924
|
-
const filtered = candidates.filter((f) => f.toLowerCase().includes(q));
|
|
6925
|
-
if (filtered.length) candidates = filtered;
|
|
6926
|
-
}
|
|
7069
|
+
const candidates = files.filter((f) => f.endsWith(".log")).sort().reverse();
|
|
6927
7070
|
return candidates.length ? resolve(agentLogsDir, candidates[0]) : null;
|
|
6928
7071
|
}
|
|
6929
7072
|
return null;
|
|
@@ -7952,6 +8095,13 @@ function buildTaskMetadataPatch(input = {}) {
|
|
|
7952
8095
|
}
|
|
7953
8096
|
}
|
|
7954
8097
|
|
|
8098
|
+
if (hasOwn(input, "type")) {
|
|
8099
|
+
const type = normalizeTaskTypeInput(input?.type);
|
|
8100
|
+
if (type) {
|
|
8101
|
+
topLevel.type = type;
|
|
8102
|
+
}
|
|
8103
|
+
}
|
|
8104
|
+
|
|
7955
8105
|
if (hasOwn(input, "epicId")) {
|
|
7956
8106
|
const epicId = normalizeOptionalStringInput(input?.epicId);
|
|
7957
8107
|
if (epicId) {
|
|
@@ -7987,6 +8137,13 @@ function buildTaskMetadataPatch(input = {}) {
|
|
|
7987
8137
|
return { topLevel, meta };
|
|
7988
8138
|
}
|
|
7989
8139
|
|
|
8140
|
+
const TASK_TYPE_VALUES = new Set(["epic", "task", "subtask"]);
|
|
8141
|
+
|
|
8142
|
+
function normalizeTaskTypeInput(input) {
|
|
8143
|
+
const normalized = String(input ?? "").trim().toLowerCase();
|
|
8144
|
+
return TASK_TYPE_VALUES.has(normalized) ? normalized : null;
|
|
8145
|
+
}
|
|
8146
|
+
|
|
7990
8147
|
const SPRINT_EXECUTION_MODES = new Set(["sequential", "parallel"]);
|
|
7991
8148
|
|
|
7992
8149
|
function normalizeSprintExecutionMode(input) {
|
|
@@ -8327,11 +8484,40 @@ async function listAgentLogFiles(query = "", limit = 60) {
|
|
|
8327
8484
|
const entries = [];
|
|
8328
8485
|
const agentLogsDir = await resolveAgentLogsDir();
|
|
8329
8486
|
const files = await readdir(agentLogsDir).catch(() => []);
|
|
8487
|
+
const normalizedQuery = String(query || "").trim().toLowerCase();
|
|
8488
|
+
const queryTerms = Array.from(new Set([
|
|
8489
|
+
normalizedQuery,
|
|
8490
|
+
...normalizedQuery
|
|
8491
|
+
.split(/[^a-z0-9]+/i)
|
|
8492
|
+
.map((part) => part.trim())
|
|
8493
|
+
.filter((part) => part.length >= 3),
|
|
8494
|
+
].filter(Boolean)));
|
|
8495
|
+
|
|
8496
|
+
const scoreAgentLogMatch = (name, lines = []) => {
|
|
8497
|
+
if (!queryTerms.length) return 0;
|
|
8498
|
+
const fileName = String(name || "").toLowerCase();
|
|
8499
|
+
const joined = lines.join("\n").toLowerCase();
|
|
8500
|
+
let score = 0;
|
|
8501
|
+
for (const term of queryTerms) {
|
|
8502
|
+
if (fileName.includes(term)) score += 120;
|
|
8503
|
+
if (joined.includes(term)) score += 80;
|
|
8504
|
+
}
|
|
8505
|
+
if (joined.includes("task id:")) score += 8;
|
|
8506
|
+
if (/(error|warn|failed|exception|timeout|anomal)/i.test(joined)) score += 6;
|
|
8507
|
+
return score;
|
|
8508
|
+
};
|
|
8509
|
+
|
|
8330
8510
|
for (const name of files) {
|
|
8331
8511
|
if (!name.endsWith(".log")) continue;
|
|
8332
|
-
if (query && !name.toLowerCase().includes(query.toLowerCase())) continue;
|
|
8333
8512
|
try {
|
|
8334
|
-
const
|
|
8513
|
+
const filePath = resolve(agentLogsDir, name);
|
|
8514
|
+
const info = await stat(filePath);
|
|
8515
|
+
let score = 0;
|
|
8516
|
+
if (queryTerms.length) {
|
|
8517
|
+
const sample = await tailFile(filePath, 160, 250_000).catch(() => ({ lines: [] }));
|
|
8518
|
+
score = scoreAgentLogMatch(name, sample?.lines || []);
|
|
8519
|
+
if (score <= 0) continue;
|
|
8520
|
+
}
|
|
8335
8521
|
entries.push({
|
|
8336
8522
|
name,
|
|
8337
8523
|
source: agentLogsDir,
|
|
@@ -8339,15 +8525,115 @@ async function listAgentLogFiles(query = "", limit = 60) {
|
|
|
8339
8525
|
mtime:
|
|
8340
8526
|
info.mtime?.toISOString?.() || new Date(info.mtime).toISOString(),
|
|
8341
8527
|
mtimeMs: info.mtimeMs,
|
|
8528
|
+
score,
|
|
8342
8529
|
});
|
|
8343
8530
|
} catch {
|
|
8344
8531
|
// ignore
|
|
8345
8532
|
}
|
|
8346
8533
|
}
|
|
8347
|
-
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
8534
|
+
entries.sort((a, b) => (b.score || 0) - (a.score || 0) || b.mtimeMs - a.mtimeMs);
|
|
8348
8535
|
return entries.slice(0, limit);
|
|
8349
8536
|
}
|
|
8350
8537
|
|
|
8538
|
+
function buildLogQueryTerms(query = "") {
|
|
8539
|
+
const normalized = String(query || "").trim().toLowerCase();
|
|
8540
|
+
if (!normalized) return [];
|
|
8541
|
+
return Array.from(new Set([
|
|
8542
|
+
normalized,
|
|
8543
|
+
...normalized
|
|
8544
|
+
.split(/[^a-z0-9]+/i)
|
|
8545
|
+
.map((part) => part.trim())
|
|
8546
|
+
.filter((part) => part.length >= 3),
|
|
8547
|
+
].filter(Boolean)));
|
|
8548
|
+
}
|
|
8549
|
+
|
|
8550
|
+
function isHighSignalLogLine(line = "") {
|
|
8551
|
+
return /(error|warn|failed|exception|timeout|anomal|retry|blocked|fatal)/i.test(String(line || ""));
|
|
8552
|
+
}
|
|
8553
|
+
|
|
8554
|
+
function filterRelevantLogLines(lines = [], query = "", limit = 200) {
|
|
8555
|
+
const sourceLines = Array.isArray(lines)
|
|
8556
|
+
? lines.map((line) => String(line || "")).filter(Boolean)
|
|
8557
|
+
: [];
|
|
8558
|
+
if (!sourceLines.length) return [];
|
|
8559
|
+
|
|
8560
|
+
const terms = buildLogQueryTerms(query);
|
|
8561
|
+
if (!terms.length) return sourceLines.slice(-limit);
|
|
8562
|
+
|
|
8563
|
+
const picked = new Set();
|
|
8564
|
+
const addWithContext = (index, radius = 1) => {
|
|
8565
|
+
for (let cursor = Math.max(0, index - radius); cursor <= Math.min(sourceLines.length - 1, index + radius); cursor += 1) {
|
|
8566
|
+
picked.add(cursor);
|
|
8567
|
+
}
|
|
8568
|
+
};
|
|
8569
|
+
|
|
8570
|
+
sourceLines.forEach((line, index) => {
|
|
8571
|
+
const lower = line.toLowerCase();
|
|
8572
|
+
const termHit = terms.some((term) => lower.includes(term));
|
|
8573
|
+
if (termHit) {
|
|
8574
|
+
addWithContext(index, isHighSignalLogLine(line) ? 2 : 1);
|
|
8575
|
+
return;
|
|
8576
|
+
}
|
|
8577
|
+
if (isHighSignalLogLine(line)) {
|
|
8578
|
+
addWithContext(index, 1);
|
|
8579
|
+
}
|
|
8580
|
+
});
|
|
8581
|
+
|
|
8582
|
+
if (!picked.size) {
|
|
8583
|
+
return sourceLines.slice(-limit);
|
|
8584
|
+
}
|
|
8585
|
+
|
|
8586
|
+
const filtered = [...picked]
|
|
8587
|
+
.sort((a, b) => a - b)
|
|
8588
|
+
.map((index) => sourceLines[index]);
|
|
8589
|
+
return filtered.slice(-limit);
|
|
8590
|
+
}
|
|
8591
|
+
|
|
8592
|
+
async function resolveSessionWorktreePath(session) {
|
|
8593
|
+
if (!session || typeof session !== "object") return null;
|
|
8594
|
+
const directCandidates = [
|
|
8595
|
+
session?.metadata?.worktreePath,
|
|
8596
|
+
session?.metadata?.workspaceDir,
|
|
8597
|
+
session?.metadata?.workspacePath,
|
|
8598
|
+
session?.metadata?.cwd,
|
|
8599
|
+
]
|
|
8600
|
+
.map((value) => String(value || "").trim())
|
|
8601
|
+
.filter(Boolean);
|
|
8602
|
+
for (const candidate of directCandidates) {
|
|
8603
|
+
if (existsSync(candidate)) return candidate;
|
|
8604
|
+
}
|
|
8605
|
+
|
|
8606
|
+
const branchHints = [
|
|
8607
|
+
session?.metadata?.branch,
|
|
8608
|
+
session?.metadata?.branchName,
|
|
8609
|
+
session?.branch,
|
|
8610
|
+
]
|
|
8611
|
+
.map((value) => String(value || "").trim().replace(/^refs\/heads\//, ""))
|
|
8612
|
+
.filter(Boolean);
|
|
8613
|
+
const taskHints = [session?.taskId, session?.id]
|
|
8614
|
+
.map((value) => String(value || "").trim().toLowerCase())
|
|
8615
|
+
.filter(Boolean);
|
|
8616
|
+
|
|
8617
|
+
try {
|
|
8618
|
+
const active = await listActiveWorktrees(repoRoot);
|
|
8619
|
+
const matched = (active || []).find((worktree) => {
|
|
8620
|
+
const worktreePath = String(worktree?.path || "").trim();
|
|
8621
|
+
const worktreeTaskKey = String(worktree?.taskKey || "").trim().toLowerCase();
|
|
8622
|
+
const worktreeBranch = String(worktree?.branch || "")
|
|
8623
|
+
.trim()
|
|
8624
|
+
.replace(/^refs\/heads\//, "");
|
|
8625
|
+
if (worktreePath && directCandidates.includes(worktreePath)) return true;
|
|
8626
|
+
if (worktreeTaskKey && taskHints.includes(worktreeTaskKey)) return true;
|
|
8627
|
+
return branchHints.some((hint) =>
|
|
8628
|
+
hint && (worktreeBranch === hint || worktreeBranch.endsWith(`/${hint}`)),
|
|
8629
|
+
);
|
|
8630
|
+
});
|
|
8631
|
+
return matched?.path || null;
|
|
8632
|
+
} catch {
|
|
8633
|
+
return null;
|
|
8634
|
+
}
|
|
8635
|
+
}
|
|
8636
|
+
|
|
8351
8637
|
async function ensurePresenceLoaded() {
|
|
8352
8638
|
const loaded = await loadWorkspaceRegistry().catch(() => null);
|
|
8353
8639
|
const registry = loaded?.registry || loaded || null;
|
|
@@ -8676,6 +8962,12 @@ async function handleApi(req, res, url) {
|
|
|
8676
8962
|
task.repository || task.meta?.repository || "",
|
|
8677
8963
|
).trim().toLowerCase();
|
|
8678
8964
|
if (workspaceFilter && taskWorkspace !== workspaceFilter) {
|
|
8965
|
+
// Backward compatibility: many legacy internal-store tasks predate
|
|
8966
|
+
// workspace stamping and should remain visible in the active
|
|
8967
|
+
// workspace board instead of being filtered out.
|
|
8968
|
+
if (!taskWorkspaceRaw) {
|
|
8969
|
+
return true;
|
|
8970
|
+
}
|
|
8679
8971
|
const taskWorkspacePath = normalizeCandidatePath(taskWorkspaceRaw);
|
|
8680
8972
|
const workspaceMatchByPath =
|
|
8681
8973
|
Boolean(taskWorkspacePath) &&
|
|
@@ -8715,9 +9007,10 @@ async function handleApi(req, res, url) {
|
|
|
8715
9007
|
const start = page * pageSize;
|
|
8716
9008
|
const slice = filtered.slice(start, start + pageSize);
|
|
8717
9009
|
const enriched = await applySharedStateToTasks(slice);
|
|
9010
|
+
const withRuntime = enriched.map((task) => withTaskRuntimeSnapshot(task));
|
|
8718
9011
|
jsonResponse(res, 200, {
|
|
8719
9012
|
ok: true,
|
|
8720
|
-
data:
|
|
9013
|
+
data: withRuntime,
|
|
8721
9014
|
page,
|
|
8722
9015
|
pageSize,
|
|
8723
9016
|
total,
|
|
@@ -8743,7 +9036,7 @@ async function handleApi(req, res, url) {
|
|
|
8743
9036
|
const adapter = getKanbanAdapter();
|
|
8744
9037
|
const task = await adapter.getTask(taskId);
|
|
8745
9038
|
const enriched = await applySharedStateToTasks(task ? [task] : []);
|
|
8746
|
-
|
|
9039
|
+
let detailTask = enriched[0] || null;
|
|
8747
9040
|
if (detailTask) {
|
|
8748
9041
|
const workflowRuns = await collectWorkflowRunsForTask(detailTask.id, url, 40);
|
|
8749
9042
|
const mergedWorkflowRuns = mergeTaskWorkflowRuns(detailTask.workflowRuns, workflowRuns, 80);
|
|
@@ -8764,6 +9057,7 @@ async function handleApi(req, res, url) {
|
|
|
8764
9057
|
};
|
|
8765
9058
|
if (sprintDag) detailTask.sprintDag = sprintDag.data;
|
|
8766
9059
|
if (globalDag) detailTask.dagOfDags = globalDag.data;
|
|
9060
|
+
detailTask = withTaskRuntimeSnapshot(detailTask);
|
|
8767
9061
|
}
|
|
8768
9062
|
jsonResponse(res, 200, { ok: true, data: detailTask });
|
|
8769
9063
|
} catch (err) {
|
|
@@ -9219,25 +9513,12 @@ async function handleApi(req, res, url) {
|
|
|
9219
9513
|
const freeSlots =
|
|
9220
9514
|
(status.maxParallel || 0) - (status.activeSlots || 0);
|
|
9221
9515
|
|
|
9222
|
-
try {
|
|
9223
|
-
if (typeof adapter.updateTaskStatus === "function") {
|
|
9224
|
-
await adapter.updateTaskStatus(taskId, "inprogress", { source: "api.tasks.start" });
|
|
9225
|
-
} else if (typeof adapter.updateTask === "function") {
|
|
9226
|
-
await adapter.updateTask(taskId, { status: "inprogress" });
|
|
9227
|
-
}
|
|
9228
|
-
applyInternalLifecycleTransition(taskId, "start", {
|
|
9229
|
-
source: "api.tasks.start",
|
|
9230
|
-
actor: "ui",
|
|
9231
|
-
force: forceStart || manualOverride,
|
|
9232
|
-
reason: "manual start",
|
|
9233
|
-
});
|
|
9234
|
-
} catch (err) {
|
|
9235
|
-
console.warn(
|
|
9236
|
-
`[telegram-ui] failed to mark task ${taskId} inprogress: ${err.message}`,
|
|
9237
|
-
);
|
|
9238
|
-
}
|
|
9239
|
-
|
|
9240
9516
|
if (freeSlots <= 0) {
|
|
9517
|
+
const queuedTask = await persistTaskExecutionMeta(adapter, taskId, {
|
|
9518
|
+
queued: true,
|
|
9519
|
+
queueState: "queued",
|
|
9520
|
+
requestedAt: new Date().toISOString(),
|
|
9521
|
+
});
|
|
9241
9522
|
jsonResponse(res, 202, {
|
|
9242
9523
|
ok: true,
|
|
9243
9524
|
taskId,
|
|
@@ -9245,6 +9526,7 @@ async function handleApi(req, res, url) {
|
|
|
9245
9526
|
started: false,
|
|
9246
9527
|
reason: "No free slots",
|
|
9247
9528
|
canStart,
|
|
9529
|
+
data: withTaskRuntimeSnapshot(queuedTask || task),
|
|
9248
9530
|
});
|
|
9249
9531
|
broadcastUiEvent(
|
|
9250
9532
|
["tasks", "overview", "executor", "agents"],
|
|
@@ -9256,8 +9538,27 @@ async function handleApi(req, res, url) {
|
|
|
9256
9538
|
);
|
|
9257
9539
|
return;
|
|
9258
9540
|
}
|
|
9541
|
+
let startedTask = task;
|
|
9542
|
+
try {
|
|
9543
|
+
startedTask = await persistTaskStatusForExecution(adapter, taskId, "inprogress", "api.tasks.start") || task;
|
|
9544
|
+
startedTask = await persistTaskExecutionMeta(adapter, taskId, {
|
|
9545
|
+
queued: false,
|
|
9546
|
+
queueState: null,
|
|
9547
|
+
}) || startedTask;
|
|
9548
|
+
applyInternalLifecycleTransition(taskId, "start", {
|
|
9549
|
+
source: "api.tasks.start",
|
|
9550
|
+
actor: "ui",
|
|
9551
|
+
force: forceStart || manualOverride,
|
|
9552
|
+
reason: "manual start",
|
|
9553
|
+
});
|
|
9554
|
+
} catch (err) {
|
|
9555
|
+
console.warn(
|
|
9556
|
+
`[telegram-ui] failed to mark task ${taskId} inprogress: ${err.message}`,
|
|
9557
|
+
);
|
|
9558
|
+
}
|
|
9559
|
+
|
|
9259
9560
|
const wasPaused = executor.isPaused?.();
|
|
9260
|
-
executor.executeTask(
|
|
9561
|
+
executor.executeTask(startedTask, {
|
|
9261
9562
|
...(sdk ? { sdk } : {}),
|
|
9262
9563
|
...(model ? { model } : {}),
|
|
9263
9564
|
force: forceStart || manualOverride,
|
|
@@ -9265,6 +9566,16 @@ async function handleApi(req, res, url) {
|
|
|
9265
9566
|
console.warn(
|
|
9266
9567
|
`[telegram-ui] failed to execute task ${taskId}: ${error.message}`,
|
|
9267
9568
|
);
|
|
9569
|
+
void persistTaskStatusForExecution(
|
|
9570
|
+
adapter,
|
|
9571
|
+
taskId,
|
|
9572
|
+
resolveFallbackStatusAfterFailedDispatch(task?.status, { started: false }),
|
|
9573
|
+
"api.tasks.start.failed",
|
|
9574
|
+
);
|
|
9575
|
+
broadcastUiEvent(["tasks", "overview", "executor", "agents"], "invalidate", {
|
|
9576
|
+
reason: "task-start-failed",
|
|
9577
|
+
taskId,
|
|
9578
|
+
});
|
|
9268
9579
|
});
|
|
9269
9580
|
jsonResponse(res, 200, {
|
|
9270
9581
|
ok: true,
|
|
@@ -9273,6 +9584,7 @@ async function handleApi(req, res, url) {
|
|
|
9273
9584
|
started: true,
|
|
9274
9585
|
wasPaused,
|
|
9275
9586
|
canStart,
|
|
9587
|
+
data: withTaskRuntimeSnapshot(startedTask),
|
|
9276
9588
|
});
|
|
9277
9589
|
broadcastUiEvent(
|
|
9278
9590
|
["tasks", "overview", "executor", "agents"],
|
|
@@ -9370,6 +9682,17 @@ async function handleApi(req, res, url) {
|
|
|
9370
9682
|
forceStart,
|
|
9371
9683
|
manualOverride,
|
|
9372
9684
|
});
|
|
9685
|
+
const reconciled = await reconcileTaskAfterDispatchAttempt({
|
|
9686
|
+
adapter,
|
|
9687
|
+
taskId,
|
|
9688
|
+
previousStatus: previousTask?.status || null,
|
|
9689
|
+
requestedStatus: nextStatus,
|
|
9690
|
+
lifecycleAction,
|
|
9691
|
+
startDispatch,
|
|
9692
|
+
source: "api.tasks.update",
|
|
9693
|
+
});
|
|
9694
|
+
const responseTask = withTaskRuntimeSnapshot(reconciled || updated);
|
|
9695
|
+
const responseStatus = responseTask?.status || nextStatus;
|
|
9373
9696
|
if (body?.pauseExecution === true && lifecycleAction === "pause" && executor && typeof executor.abortTask === "function") {
|
|
9374
9697
|
executor.abortTask(taskId, "task_lifecycle_pause");
|
|
9375
9698
|
}
|
|
@@ -9386,19 +9709,19 @@ async function handleApi(req, res, url) {
|
|
|
9386
9709
|
});
|
|
9387
9710
|
jsonResponse(res, 200, {
|
|
9388
9711
|
ok: true,
|
|
9389
|
-
data:
|
|
9712
|
+
data: responseTask,
|
|
9390
9713
|
restart,
|
|
9391
9714
|
lifecycle: {
|
|
9392
9715
|
action: lifecycleAction,
|
|
9393
9716
|
previousStatus: previousTask?.status || null,
|
|
9394
|
-
nextStatus,
|
|
9717
|
+
nextStatus: responseStatus,
|
|
9395
9718
|
startDispatch,
|
|
9396
9719
|
},
|
|
9397
9720
|
});
|
|
9398
9721
|
broadcastUiEvent(["tasks", "overview"], "invalidate", {
|
|
9399
9722
|
reason: "task-updated",
|
|
9400
9723
|
taskId,
|
|
9401
|
-
status:
|
|
9724
|
+
status: responseStatus,
|
|
9402
9725
|
});
|
|
9403
9726
|
if (restart?.started || startDispatch?.started) {
|
|
9404
9727
|
broadcastUiEvent(["tasks", "overview", "executor", "agents"], "invalidate", {
|
|
@@ -9494,6 +9817,17 @@ async function handleApi(req, res, url) {
|
|
|
9494
9817
|
forceStart,
|
|
9495
9818
|
manualOverride,
|
|
9496
9819
|
});
|
|
9820
|
+
const reconciled = await reconcileTaskAfterDispatchAttempt({
|
|
9821
|
+
adapter,
|
|
9822
|
+
taskId,
|
|
9823
|
+
previousStatus: previousTask?.status || null,
|
|
9824
|
+
requestedStatus: nextStatus,
|
|
9825
|
+
lifecycleAction,
|
|
9826
|
+
startDispatch,
|
|
9827
|
+
source: "api.tasks.edit",
|
|
9828
|
+
});
|
|
9829
|
+
const responseTask = withTaskRuntimeSnapshot(reconciled || updated);
|
|
9830
|
+
const responseStatus = responseTask?.status || nextStatus;
|
|
9497
9831
|
if (body?.pauseExecution === true && lifecycleAction === "pause" && executor && typeof executor.abortTask === "function") {
|
|
9498
9832
|
executor.abortTask(taskId, "task_lifecycle_pause");
|
|
9499
9833
|
}
|
|
@@ -9510,19 +9844,19 @@ async function handleApi(req, res, url) {
|
|
|
9510
9844
|
});
|
|
9511
9845
|
jsonResponse(res, 200, {
|
|
9512
9846
|
ok: true,
|
|
9513
|
-
data:
|
|
9847
|
+
data: responseTask,
|
|
9514
9848
|
restart,
|
|
9515
9849
|
lifecycle: {
|
|
9516
9850
|
action: lifecycleAction,
|
|
9517
9851
|
previousStatus: previousTask?.status || null,
|
|
9518
|
-
nextStatus,
|
|
9852
|
+
nextStatus: responseStatus,
|
|
9519
9853
|
startDispatch,
|
|
9520
9854
|
},
|
|
9521
9855
|
});
|
|
9522
9856
|
broadcastUiEvent(["tasks", "overview"], "invalidate", {
|
|
9523
9857
|
reason: "task-edited",
|
|
9524
9858
|
taskId,
|
|
9525
|
-
status:
|
|
9859
|
+
status: responseStatus,
|
|
9526
9860
|
});
|
|
9527
9861
|
if (restart?.started || startDispatch?.started) {
|
|
9528
9862
|
broadcastUiEvent(["tasks", "overview", "executor", "agents"], "invalidate", {
|
|
@@ -11151,8 +11485,20 @@ async function handleApi(req, res, url) {
|
|
|
11151
11485
|
jsonResponse(res, 200, { ok: true, data: null });
|
|
11152
11486
|
return;
|
|
11153
11487
|
}
|
|
11154
|
-
const tail = await tailFile(filePath, lines);
|
|
11155
|
-
|
|
11488
|
+
const tail = await tailFile(filePath, Math.max(lines * 4, 240));
|
|
11489
|
+
const filteredLines = filterRelevantLogLines(tail?.lines || [], query || fileName, lines);
|
|
11490
|
+
const contentLines = filteredLines.length ? filteredLines : (tail?.lines || []).slice(-lines);
|
|
11491
|
+
jsonResponse(res, 200, {
|
|
11492
|
+
ok: true,
|
|
11493
|
+
data: {
|
|
11494
|
+
file: fileName,
|
|
11495
|
+
content: contentLines.join("\n"),
|
|
11496
|
+
lines: contentLines,
|
|
11497
|
+
mode: filteredLines.length ? "focused" : "tail",
|
|
11498
|
+
totalLines: Array.isArray(tail?.lines) ? tail.lines.length : 0,
|
|
11499
|
+
truncated: tail?.truncated === true,
|
|
11500
|
+
},
|
|
11501
|
+
});
|
|
11156
11502
|
} catch (err) {
|
|
11157
11503
|
jsonResponse(res, 200, { ok: true, data: null });
|
|
11158
11504
|
}
|
|
@@ -13538,7 +13884,7 @@ async function handleApi(req, res, url) {
|
|
|
13538
13884
|
});
|
|
13539
13885
|
return;
|
|
13540
13886
|
}
|
|
13541
|
-
const worktreePath = session
|
|
13887
|
+
const worktreePath = await resolveSessionWorktreePath(session);
|
|
13542
13888
|
if (!worktreePath || !existsSync(worktreePath)) {
|
|
13543
13889
|
jsonResponse(res, 200, { ok: true, diff: { files: [], totalFiles: 0, totalAdditions: 0, totalDeletions: 0, formatted: "(no worktree)" }, summary: "(no worktree)", commits: [] });
|
|
13544
13890
|
return;
|