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.
@@ -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
- let candidates = files.filter((f) => f.endsWith(".log")).sort().reverse();
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 info = await stat(resolve(agentLogsDir, name));
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: enriched,
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
- const detailTask = enriched[0] || null;
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(task, {
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: updated,
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: nextStatus,
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: updated,
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: nextStatus,
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
- jsonResponse(res, 200, { ok: true, data: { file: fileName, content: tail } });
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.metadata?.worktreePath;
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;