@sideboard-ai/core 0.1.99 → 0.1.103

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.
Files changed (52) hide show
  1. package/dist/agents/cursor-runner.cjs +22 -22
  2. package/dist/agents/cursor-runner.js +2 -2
  3. package/dist/{agents-ESJKIQQA.js → agents-UT5GCJH3.js} +8 -8
  4. package/dist/{agents-3MWWWSMF.js → agents-XZBAKBO2.js} +9 -9
  5. package/dist/{app-settings-BRF7VKTQ.js → app-settings-EKJM2344.js} +2 -2
  6. package/dist/{app-settings-ZXPAB3QN.js → app-settings-VDJK2BZ2.js} +2 -2
  7. package/dist/{caffeinate-hold-U3QJBV67.js → caffeinate-hold-NBRUWYXO.js} +2 -2
  8. package/dist/{caffeinate-hold-I7SG6UQR.js → caffeinate-hold-X3JNPR42.js} +2 -2
  9. package/dist/{chunk-UYQYK2RY.js → chunk-27MCESES.js} +3 -3
  10. package/dist/{chunk-QSLE4VEM.js → chunk-2GO3YKBA.js} +4 -4
  11. package/dist/{chunk-DKHGWYWR.js → chunk-7YKXHBIW.js} +1 -1
  12. package/dist/{chunk-JL4SJ6V7.js → chunk-BD2ICC4D.js} +4 -0
  13. package/dist/{chunk-NZPGMNFC.js → chunk-DCOZABNT.js} +1 -1
  14. package/dist/{chunk-CIRXAYWS.js → chunk-DHAGMJIK.js} +55 -28
  15. package/dist/{chunk-XUWDLRAE.js → chunk-ETMRBJKA.js} +11 -11
  16. package/dist/{chunk-MBP3XG57.js → chunk-H6LP57EF.js} +7 -7
  17. package/dist/{chunk-FO67IJTY.js → chunk-HHLZWO7U.js} +5 -5
  18. package/dist/{chunk-GXSYI7FH.js → chunk-HUKCGRAT.js} +209 -59
  19. package/dist/{chunk-3WA37BRI.js → chunk-KGSVLZV6.js} +11 -4
  20. package/dist/{chunk-7KDBDMNC.js → chunk-KLBOWJ74.js} +1 -1
  21. package/dist/{chunk-FYS2BULQ.js → chunk-LANDJIAI.js} +213 -63
  22. package/dist/{chunk-OANJQTVG.js → chunk-ND6AO7C5.js} +3 -3
  23. package/dist/{chunk-HI2OTFFR.js → chunk-OW34MVDN.js} +5 -5
  24. package/dist/{chunk-JOF3XIEM.js → chunk-P3BQ5DOL.js} +11 -4
  25. package/dist/{chunk-B4R2VB2A.js → chunk-QZQEXME2.js} +4 -0
  26. package/dist/{chunk-R7BQBSDT.js → chunk-WA3DVVWG.js} +55 -28
  27. package/dist/{chunk-6LFV4VFI.js → chunk-WSYCND2U.js} +1 -1
  28. package/dist/{chunk-FT2SQOL4.js → chunk-XHZJJWLI.js} +1 -1
  29. package/dist/{chunk-XH2GS2LO.js → chunk-YJADJH5U.js} +11 -11
  30. package/dist/{connected-teams-SWHJWK2A.js → connected-teams-J4I5AKMR.js} +2 -2
  31. package/dist/{connected-teams-TLVKZPJP.js → connected-teams-M5XNXRI5.js} +2 -2
  32. package/dist/{coordinator-prompt-OQOOD5ET.js → coordinator-prompt-3DFZRMXE.js} +5 -5
  33. package/dist/{coordinator-prompt-AKEY4WSO.js → coordinator-prompt-UWHSD65V.js} +5 -5
  34. package/dist/{cursor-recover-S5ORPW34.js → cursor-recover-JBTIDARH.js} +1 -1
  35. package/dist/{cursor-recover-G34BYKL2.js → cursor-recover-ZUJG6XRQ.js} +1 -1
  36. package/dist/{global-workspace-WMF3BJP5.js → global-workspace-KJQS5S2C.js} +6 -6
  37. package/dist/{global-workspace-RSQXRLT7.js → global-workspace-UR4E3CCG.js} +6 -6
  38. package/dist/index.cjs +851 -232
  39. package/dist/index.d.cts +39 -6
  40. package/dist/index.d.ts +39 -6
  41. package/dist/index.js +561 -138
  42. package/dist/mcp/run-stdio.cjs +651 -256
  43. package/dist/mcp/run-stdio.js +345 -143
  44. package/dist/{paths-TZX4L5X7.js → paths-QCI3MTO5.js} +3 -1
  45. package/dist/{paths-JQM2XRAD.js → paths-UMEDHOS5.js} +3 -1
  46. package/dist/{thread-store-XAKWCV2E.js → thread-store-4OUEQ2DB.js} +2 -2
  47. package/dist/{thread-store-V7NOOXKE.js → thread-store-DQJGDKIJ.js} +2 -2
  48. package/dist/{workspaces-MZVQHRSJ.js → workspaces-NTIVI6DI.js} +7 -7
  49. package/dist/{workspaces-4ZY4QPWQ.js → workspaces-TKJXPNV4.js} +7 -7
  50. package/dist/{worktree-EO5QAGJU.js → worktree-NKWN5ZTS.js} +4 -4
  51. package/dist/{worktree-DA4BOV7G.js → worktree-PEUPFAKL.js} +4 -4
  52. package/package.json +2 -2
@@ -6,7 +6,6 @@ import {
6
6
  fallbackTurnFailDetail,
7
7
  formatTurnExitError,
8
8
  getAdapter,
9
- humanizeAgentFailDetail,
10
9
  isSessionQuotaLimit,
11
10
  listModelsForAgent,
12
11
  looksLikeAgentFailureMessage,
@@ -15,16 +14,18 @@ import {
15
14
  parseSessionQuotaResetAt,
16
15
  pushTurnStderr,
17
16
  resolveQuotaFallbackAgent,
17
+ shouldRetryFailedAgentTurn,
18
18
  sideboardMcpProfile,
19
- summarizeTurnStderr
20
- } from "../chunk-FYS2BULQ.js";
21
- import "../chunk-DKHGWYWR.js";
19
+ summarizeTurnStderr,
20
+ turnFailChatText
21
+ } from "../chunk-LANDJIAI.js";
22
+ import "../chunk-7YKXHBIW.js";
22
23
  import {
23
24
  addWorkspace,
24
25
  ensureWorkspace,
25
26
  removeWorkspace,
26
27
  syncWorkspacesFromThreads
27
- } from "../chunk-OANJQTVG.js";
28
+ } from "../chunk-ND6AO7C5.js";
28
29
  import {
29
30
  extractPresentedPlan,
30
31
  readPlanFile,
@@ -32,7 +33,7 @@ import {
32
33
  } from "../chunk-N5PM7HGQ.js";
33
34
  import {
34
35
  releaseCaffeinateHoldForThread
35
- } from "../chunk-7KDBDMNC.js";
36
+ } from "../chunk-KLBOWJ74.js";
36
37
  import {
37
38
  GLOBAL_WORKSPACE_ID,
38
39
  assertOrchestratorCapableAgent,
@@ -43,14 +44,14 @@ import {
43
44
  isOrchestratorThread,
44
45
  isSlackCoordinatorThread,
45
46
  orchestratorSessionPoisonedByBuiltins
46
- } from "../chunk-HI2OTFFR.js";
47
+ } from "../chunk-OW34MVDN.js";
47
48
  import {
48
49
  SLACK_REPLY_FORMATTING,
49
50
  coordinatorSystemPrompt,
50
51
  coordinatorTurnReminder,
51
52
  enrichWorkspacesWithGithub,
52
53
  ensureGlobalCoordinatorCwd
53
- } from "../chunk-XH2GS2LO.js";
54
+ } from "../chunk-YJADJH5U.js";
54
55
  import {
55
56
  addPrStackLayer,
56
57
  allocateTeamName,
@@ -61,6 +62,7 @@ import {
61
62
  createThreadWorktree,
62
63
  currentBranch,
63
64
  detectGhStack,
65
+ enqueueByKey,
64
66
  fetchPrHead,
65
67
  formatGhLandError,
66
68
  formatGitAuthModeDirective,
@@ -93,8 +95,9 @@ import {
93
95
  takenSlugsFromThread,
94
96
  threadDisplayLabel,
95
97
  warmGithubAgentAuth,
98
+ withRepoGitLock,
96
99
  worktreeNameFromPath
97
- } from "../chunk-R7BQBSDT.js";
100
+ } from "../chunk-WA3DVVWG.js";
98
101
  import {
99
102
  ATTACHMENTS_DIR,
100
103
  LEGACY_ATTACHMENTS_DIR,
@@ -111,7 +114,7 @@ import {
111
114
  updateThread,
112
115
  withThreadLock,
113
116
  writeThread
114
- } from "../chunk-JOF3XIEM.js";
117
+ } from "../chunk-P3BQ5DOL.js";
115
118
  import {
116
119
  childEnvWithAppSettings,
117
120
  disconnectLinearConnection,
@@ -129,7 +132,7 @@ import {
129
132
  stripNestedElectronEnv,
130
133
  updateAdvancedSettings,
131
134
  writeSecureJson
132
- } from "../chunk-6LFV4VFI.js";
135
+ } from "../chunk-WSYCND2U.js";
133
136
  import {
134
137
  writePrivateFile
135
138
  } from "../chunk-NSTQ6QKD.js";
@@ -142,9 +145,10 @@ import {
142
145
  runCursorWorktreeSetup,
143
146
  sideboardReposDir,
144
147
  sideboardWorkspacesDir,
148
+ threadLivePath,
145
149
  workspaceSettingsSourceLabel,
146
150
  worktreesRoot
147
- } from "../chunk-JL4SJ6V7.js";
151
+ } from "../chunk-BD2ICC4D.js";
148
152
  import {
149
153
  enrichPathWithNpmGlobalBin,
150
154
  ensureAgentPath,
@@ -659,7 +663,7 @@ async function refreshSlackReplyBadges(opts) {
659
663
  }
660
664
 
661
665
  // src/orchestrator/orchestrator.ts
662
- import { existsSync as existsSync14 } from "fs";
666
+ import { existsSync as existsSync15 } from "fs";
663
667
 
664
668
  // src/agents/spawn.ts
665
669
  import { createInterface } from "readline";
@@ -901,7 +905,7 @@ function createAgentStreamCoalescer(emit, opts) {
901
905
  const intervalMs = opts?.intervalMs ?? 32;
902
906
  let pending = null;
903
907
  let timer = null;
904
- const flush = () => {
908
+ const flush2 = () => {
905
909
  if (timer) {
906
910
  clearTimeout(timer);
907
911
  timer = null;
@@ -913,13 +917,13 @@ function createAgentStreamCoalescer(emit, opts) {
913
917
  };
914
918
  const schedule = () => {
915
919
  if (timer) return;
916
- timer = setTimeout(flush, intervalMs);
920
+ timer = setTimeout(flush2, intervalMs);
917
921
  timer.unref?.();
918
922
  };
919
923
  return {
920
924
  push(event) {
921
925
  if (!isTextEvent(event)) {
922
- flush();
926
+ flush2();
923
927
  emit(event);
924
928
  return;
925
929
  }
@@ -927,12 +931,12 @@ function createAgentStreamCoalescer(emit, opts) {
927
931
  if (pending && pending.type === event.type) {
928
932
  pending.data += event.data;
929
933
  } else {
930
- flush();
934
+ flush2();
931
935
  pending = { type: event.type, data: event.data };
932
936
  }
933
937
  schedule();
934
938
  },
935
- flush
939
+ flush: flush2
936
940
  };
937
941
  }
938
942
 
@@ -944,9 +948,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
944
948
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
945
949
  );
946
950
  }
947
- const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-WMF3BJP5.js");
951
+ const { isGlobalThread: isGlobalThread2 } = await import("../global-workspace-KJQS5S2C.js");
948
952
  if (isGlobalThread2(thread)) {
949
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-OQOOD5ET.js");
953
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("../coordinator-prompt-3DFZRMXE.js");
950
954
  ensureGlobalCoordinatorCwd2(
951
955
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
952
956
  );
@@ -1887,7 +1891,7 @@ async function createThread(input, _onSetupLine) {
1887
1891
  return readThread(thread.id) ?? thread;
1888
1892
  }
1889
1893
  async function listLinearIssues(agent, repoPath) {
1890
- const { getAdapter: getAdapter2 } = await import("../agents-ESJKIQQA.js");
1894
+ const { getAdapter: getAdapter2 } = await import("../agents-UT5GCJH3.js");
1891
1895
  await requireAgent(agent, { requireLinear: true });
1892
1896
  const adapter = getAdapter2(agent);
1893
1897
  if (!adapter.listLinearIssues) {
@@ -2212,8 +2216,11 @@ function worktreeBindingFrom(from) {
2212
2216
  };
2213
2217
  }
2214
2218
  function threadsSharingWorktree(worktreePath) {
2219
+ if (!worktreePath) return [];
2215
2220
  const key = normalizeWorktreePath(worktreePath);
2216
- return listThreads({ includeArchived: true }).filter((t) => sameWorktreePath(t.worktreePath, key) && t.status !== "archived").sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2221
+ return listThreads({ includeArchived: true }).filter(
2222
+ (t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
2223
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2217
2224
  }
2218
2225
  function formatTranscriptMarkdown(title, messages) {
2219
2226
  const body = formatMessagesAsTranscript(messages, { tools: "full" });
@@ -2284,9 +2291,132 @@ function forkChatTab(input) {
2284
2291
  return tab;
2285
2292
  }
2286
2293
 
2294
+ // src/store/turn-live.ts
2295
+ import {
2296
+ existsSync as existsSync7,
2297
+ renameSync,
2298
+ unlinkSync as unlinkSync2,
2299
+ writeFileSync as writeFileSync2,
2300
+ readFileSync as readFileSync5
2301
+ } from "fs";
2302
+ var buffers = /* @__PURE__ */ new Map();
2303
+ var FLUSH_MS = 800;
2304
+ var MAX_PARTS = 80;
2305
+ function trimParts(parts) {
2306
+ if (parts.length <= MAX_PARTS) return parts;
2307
+ return parts.slice(-Math.floor(MAX_PARTS / 2));
2308
+ }
2309
+ function lastText(parts, type) {
2310
+ for (let i = parts.length - 1; i >= 0; i--) {
2311
+ const p = parts[i];
2312
+ if (p.type === type && p.text.trim()) return p.text.trim();
2313
+ }
2314
+ return "";
2315
+ }
2316
+ function summarizeTurnLive(parts) {
2317
+ const tools = parts.filter(
2318
+ (p) => p.type === "tool"
2319
+ );
2320
+ const lastTool = tools[tools.length - 1];
2321
+ const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
2322
+ const running = lastTool?.status === "running";
2323
+ const thinking = lastText(parts, "thinking");
2324
+ const text3 = lastText(parts, "text");
2325
+ const excerptRaw = thinking || text3;
2326
+ const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
2327
+ let summary = "Working\u2026";
2328
+ if (lastToolLabel) {
2329
+ const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
2330
+ summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
2331
+ } else if (thinking) {
2332
+ summary = "Thinking\u2026";
2333
+ } else if (text3) {
2334
+ summary = "Writing reply\u2026";
2335
+ }
2336
+ return {
2337
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2338
+ summary,
2339
+ lastTool: lastToolLabel,
2340
+ toolCount: tools.length,
2341
+ excerpt
2342
+ };
2343
+ }
2344
+ function flush(threadId) {
2345
+ const buf = buffers.get(threadId);
2346
+ if (!buf) return;
2347
+ if (buf.timer) {
2348
+ clearTimeout(buf.timer);
2349
+ buf.timer = null;
2350
+ }
2351
+ writeTurnLive(threadId, summarizeTurnLive(buf.parts));
2352
+ }
2353
+ function scheduleFlush(threadId) {
2354
+ const buf = buffers.get(threadId);
2355
+ if (!buf) return;
2356
+ if (buf.timer) return;
2357
+ buf.timer = setTimeout(() => {
2358
+ const live = buffers.get(threadId);
2359
+ if (live) live.timer = null;
2360
+ flush(threadId);
2361
+ }, FLUSH_MS);
2362
+ }
2363
+ function noteTurnLiveEvent(threadId, event) {
2364
+ if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
2365
+ return;
2366
+ }
2367
+ const prev = buffers.get(threadId) ?? { parts: [], timer: null };
2368
+ prev.parts = trimParts(applyAgentEvent(prev.parts, event));
2369
+ buffers.set(threadId, prev);
2370
+ if (event.type === "exit") {
2371
+ flush(threadId);
2372
+ const cur = buffers.get(threadId);
2373
+ if (cur?.timer) clearTimeout(cur.timer);
2374
+ buffers.delete(threadId);
2375
+ return;
2376
+ }
2377
+ const immediate = event.type === "tool_use" || event.type === "tool_result";
2378
+ if (immediate) flush(threadId);
2379
+ else scheduleFlush(threadId);
2380
+ }
2381
+ function writeTurnLive(threadId, progress) {
2382
+ const path = threadLivePath(threadId);
2383
+ const tmp = `${path}.${process.pid}.tmp`;
2384
+ try {
2385
+ writeFileSync2(tmp, JSON.stringify(progress), "utf8");
2386
+ renameSync(tmp, path);
2387
+ } catch {
2388
+ try {
2389
+ unlinkSync2(tmp);
2390
+ } catch {
2391
+ }
2392
+ }
2393
+ }
2394
+ function readTurnLive(threadId) {
2395
+ const path = threadLivePath(threadId);
2396
+ if (!existsSync7(path)) return null;
2397
+ try {
2398
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
2399
+ if (!raw || typeof raw.summary !== "string") return null;
2400
+ return raw;
2401
+ } catch {
2402
+ return null;
2403
+ }
2404
+ }
2405
+ function clearTurnLive(threadId) {
2406
+ const buf = buffers.get(threadId);
2407
+ if (buf?.timer) clearTimeout(buf.timer);
2408
+ buffers.delete(threadId);
2409
+ const path = threadLivePath(threadId);
2410
+ if (!existsSync7(path)) return;
2411
+ try {
2412
+ unlinkSync2(path);
2413
+ } catch {
2414
+ }
2415
+ }
2416
+
2287
2417
  // src/review/request-review.ts
2288
2418
  import { randomUUID as randomUUID2 } from "crypto";
2289
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
2419
+ import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2290
2420
  import { dirname as dirname2, join as join8 } from "path";
2291
2421
 
2292
2422
  // src/review/review-request-template.ts
@@ -2432,9 +2562,9 @@ function shouldRefreshReviewRequestTemplate(content) {
2432
2562
  return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
2433
2563
  }
2434
2564
  function readTextIfPresent(abs) {
2435
- if (!existsSync7(abs)) return null;
2565
+ if (!existsSync8(abs)) return null;
2436
2566
  try {
2437
- const content = readFileSync5(abs, "utf8");
2567
+ const content = readFileSync6(abs, "utf8");
2438
2568
  return content.trim() ? content : null;
2439
2569
  } catch {
2440
2570
  return null;
@@ -2442,9 +2572,9 @@ function readTextIfPresent(abs) {
2442
2572
  }
2443
2573
  function ensureAttachmentsGitignore(worktreePath) {
2444
2574
  const gitignoreAbs = join8(worktreePath, ATTACHMENTS_DIR, ".gitignore");
2445
- if (existsSync7(gitignoreAbs)) return;
2575
+ if (existsSync8(gitignoreAbs)) return;
2446
2576
  mkdirSync2(dirname2(gitignoreAbs), { recursive: true });
2447
- writeFileSync2(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
2577
+ writeFileSync3(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
2448
2578
  }
2449
2579
  function resolveReviewGuidelines(worktreePath) {
2450
2580
  const repoAbs = join8(worktreePath, REPO_REVIEW_PATH);
@@ -2479,7 +2609,7 @@ function resolveReviewGuidelines(worktreePath) {
2479
2609
  }
2480
2610
  ensureAttachmentsGitignore(worktreePath);
2481
2611
  mkdirSync2(dirname2(localAbs), { recursive: true });
2482
- writeFileSync2(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
2612
+ writeFileSync3(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
2483
2613
  return {
2484
2614
  path: REVIEW_REQUEST_PATH,
2485
2615
  name: REVIEW_REQUEST_NAME,
@@ -2675,10 +2805,10 @@ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
2675
2805
  import { execFileSync } from "child_process";
2676
2806
  import {
2677
2807
  copyFileSync as copyFileSync2,
2678
- existsSync as existsSync8,
2808
+ existsSync as existsSync9,
2679
2809
  mkdtempSync,
2680
2810
  readdirSync as readdirSync3,
2681
- readFileSync as readFileSync6,
2811
+ readFileSync as readFileSync7,
2682
2812
  rmSync
2683
2813
  } from "fs";
2684
2814
  import { tmpdir } from "os";
@@ -2708,7 +2838,7 @@ function mapAgentType(raw) {
2708
2838
  return null;
2709
2839
  }
2710
2840
  function resolveConductorCursorAgentId(workspacePath) {
2711
- if (!workspacePath || !existsSync8(CURSOR_SDK_STORE)) return null;
2841
+ if (!workspacePath || !existsSync9(CURSOR_SDK_STORE)) return null;
2712
2842
  const normalized = workspacePath.replace(/\/$/, "");
2713
2843
  let best = null;
2714
2844
  let hashes;
@@ -2719,10 +2849,10 @@ function resolveConductorCursorAgentId(workspacePath) {
2719
2849
  }
2720
2850
  for (const hash of hashes) {
2721
2851
  const agentsFile = join9(CURSOR_SDK_STORE, hash, "agents.ndjson");
2722
- if (!existsSync8(agentsFile)) continue;
2852
+ if (!existsSync9(agentsFile)) continue;
2723
2853
  let text3;
2724
2854
  try {
2725
- text3 = readFileSync6(agentsFile, "utf8");
2855
+ text3 = readFileSync7(agentsFile, "utf8");
2726
2856
  } catch {
2727
2857
  continue;
2728
2858
  }
@@ -2746,7 +2876,7 @@ function resolveConductorCursorAgentId(workspacePath) {
2746
2876
  return best?.agentId ?? null;
2747
2877
  }
2748
2878
  async function adoptThread(input) {
2749
- if (!existsSync8(input.worktreePath)) {
2879
+ if (!existsSync9(input.worktreePath)) {
2750
2880
  throw new Error(`Worktree not found: ${input.worktreePath}`);
2751
2881
  }
2752
2882
  const repoPath = await resolveRepoRoot(input.worktreePath);
@@ -2765,12 +2895,12 @@ async function adoptThread(input) {
2765
2895
  messages: input.messages ?? []
2766
2896
  });
2767
2897
  writeThread(thread);
2768
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-MZVQHRSJ.js");
2898
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-NTIVI6DI.js");
2769
2899
  await ensureWorkspace2(repoPath);
2770
2900
  return thread;
2771
2901
  }
2772
2902
  function listConductorWorkspaces() {
2773
- if (!existsSync8(CONDUCTOR_DB)) {
2903
+ if (!existsSync9(CONDUCTOR_DB)) {
2774
2904
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
2775
2905
  }
2776
2906
  const tmp = mkdtempSync(join9(tmpdir(), "sideboard-conductor-"));
@@ -2779,7 +2909,7 @@ function listConductorWorkspaces() {
2779
2909
  copyFileSync2(CONDUCTOR_DB, snapshot);
2780
2910
  for (const suffix of ["-wal", "-shm"]) {
2781
2911
  const src = `${CONDUCTOR_DB}${suffix}`;
2782
- if (existsSync8(src)) {
2912
+ if (existsSync9(src)) {
2783
2913
  try {
2784
2914
  copyFileSync2(src, `${snapshot}${suffix}`);
2785
2915
  } catch {
@@ -2861,7 +2991,7 @@ function listConductorWorkspaces() {
2861
2991
  }
2862
2992
  }
2863
2993
  function importConductorWorkspace(workspaceId) {
2864
- if (!existsSync8(CONDUCTOR_DB)) {
2994
+ if (!existsSync9(CONDUCTOR_DB)) {
2865
2995
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
2866
2996
  }
2867
2997
  const tmp = mkdtempSync(join9(tmpdir(), "sideboard-conductor-"));
@@ -2870,7 +3000,7 @@ function importConductorWorkspace(workspaceId) {
2870
3000
  copyFileSync2(CONDUCTOR_DB, snapshot);
2871
3001
  for (const suffix of ["-wal", "-shm"]) {
2872
3002
  const src = `${CONDUCTOR_DB}${suffix}`;
2873
- if (existsSync8(src)) {
3003
+ if (existsSync9(src)) {
2874
3004
  try {
2875
3005
  copyFileSync2(src, `${snapshot}${suffix}`);
2876
3006
  } catch {
@@ -2890,7 +3020,7 @@ function importConductorWorkspace(workspaceId) {
2890
3020
  ).get(workspaceId);
2891
3021
  if (!row) throw new Error(`Conductor workspace not found: ${workspaceId}`);
2892
3022
  const worktreePath = String(row.workspacePath);
2893
- if (!existsSync8(worktreePath)) {
3023
+ if (!existsSync9(worktreePath)) {
2894
3024
  throw new Error(`Conductor worktree missing on disk: ${worktreePath}`);
2895
3025
  }
2896
3026
  let sessionId = null;
@@ -2961,7 +3091,7 @@ async function importConductorWorkspaceAsync(workspaceId) {
2961
3091
  }
2962
3092
 
2963
3093
  // src/threads/stack-layers.ts
2964
- import { existsSync as existsSync9 } from "fs";
3094
+ import { existsSync as existsSync10 } from "fs";
2965
3095
  function stackIdFrom(stack) {
2966
3096
  if (stack.stackNumber != null) return `gh-stack-${stack.stackNumber}`;
2967
3097
  const key = stack.layers.map((l) => l.branchName).join("|");
@@ -3024,7 +3154,7 @@ async function openStackLayer(input, _onSetupLine) {
3024
3154
  let createdWorktree = false;
3025
3155
  const trees = await listWorktrees(repoPath);
3026
3156
  const checkedOut = trees.find((w) => w.branch === branchName);
3027
- if (checkedOut?.path && existsSync9(checkedOut.path)) {
3157
+ if (checkedOut?.path && existsSync10(checkedOut.path)) {
3028
3158
  if (input.reuseExistingWorktree !== false) {
3029
3159
  worktreePath = checkedOut.path;
3030
3160
  } else {
@@ -3166,7 +3296,7 @@ async function initStackFromThread(input, onSetupLine) {
3166
3296
  async function createPrStack(input, onSetupLine) {
3167
3297
  await requireAgent(input.agent);
3168
3298
  const repoPath = await resolveRepoRoot(input.repoPath);
3169
- if (!existsSync9(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
3299
+ if (!existsSync10(repoPath)) throw new Error(`Repo not found: ${repoPath}`);
3170
3300
  if (!input.branches.length) throw new Error("At least one branch name required");
3171
3301
  const status = await detectGhStack(repoPath);
3172
3302
  if (!status.available) throw new Error(status.reason);
@@ -3233,7 +3363,7 @@ async function createPrStack(input, onSetupLine) {
3233
3363
  }
3234
3364
  }
3235
3365
  const claimed = new Set(threads.map((t) => t.worktreePath));
3236
- if (!claimed.has(bootstrap.worktreePath) && existsSync9(bootstrap.worktreePath)) {
3366
+ if (!claimed.has(bootstrap.worktreePath) && existsSync10(bootstrap.worktreePath)) {
3237
3367
  try {
3238
3368
  await removeWorktree(repoPath, bootstrap.worktreePath, {
3239
3369
  deleteBranch: bootstrap.branchName
@@ -3245,10 +3375,10 @@ async function createPrStack(input, onSetupLine) {
3245
3375
  }
3246
3376
 
3247
3377
  // src/diff/diff.ts
3248
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync7, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
3378
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync8, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
3249
3379
  import { dirname as dirname3, join as join10 } from "path";
3250
3380
  async function inspectGitWorktree(worktreePath) {
3251
- if (!worktreePath || !existsSync10(worktreePath)) return "missing_worktree";
3381
+ if (!worktreePath || !existsSync11(worktreePath)) return "missing_worktree";
3252
3382
  const check = await git(["rev-parse", "--is-inside-work-tree"], worktreePath, {
3253
3383
  reject: false
3254
3384
  });
@@ -3256,7 +3386,7 @@ async function inspectGitWorktree(worktreePath) {
3256
3386
  return "ok";
3257
3387
  }
3258
3388
  async function initializeGitRepository(worktreePath) {
3259
- if (!worktreePath || !existsSync10(worktreePath)) {
3389
+ if (!worktreePath || !existsSync11(worktreePath)) {
3260
3390
  throw new Error("Worktree not found");
3261
3391
  }
3262
3392
  const status = await inspectGitWorktree(worktreePath);
@@ -3396,7 +3526,7 @@ async function untrackedPatch(worktreePath, path, maxHunk) {
3396
3526
  try {
3397
3527
  const st = statSync2(abs);
3398
3528
  if (st.isFile() && st.size > maxHunk) {
3399
- const buf = readFileSync7(abs).subarray(0, maxHunk);
3529
+ const buf = readFileSync8(abs).subarray(0, maxHunk);
3400
3530
  return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
3401
3531
  }
3402
3532
  } catch {
@@ -3907,7 +4037,7 @@ function readWorktreeFileForUpload(worktreePath, relativePath, opts) {
3907
4037
  `File too large to upload (${st.size} bytes; max ${maxBytes})`
3908
4038
  );
3909
4039
  }
3910
- const buf = readFileSync7(abs);
4040
+ const buf = readFileSync8(abs);
3911
4041
  return {
3912
4042
  path: relativePath,
3913
4043
  contentBase64: buf.toString("base64"),
@@ -3922,7 +4052,7 @@ function readWorktreeFile(worktreePath, relativePath, opts) {
3922
4052
  if (!st.isFile()) {
3923
4053
  throw new Error(`Not a file: ${relativePath}`);
3924
4054
  }
3925
- const buf = readFileSync7(abs);
4055
+ const buf = readFileSync8(abs);
3926
4056
  if (isImageRelativePath(relativePath)) {
3927
4057
  const maxImageBytes = Math.max(maxBytes, 15e6);
3928
4058
  const truncated2 = buf.length > maxImageBytes;
@@ -3967,7 +4097,7 @@ function writeWorktreeFile(worktreePath, relativePath, content) {
3967
4097
  assertSafeRelativePath(relativePath);
3968
4098
  const abs = join10(worktreePath, relativePath);
3969
4099
  mkdirSync3(dirname3(abs), { recursive: true });
3970
- writeFileSync3(abs, content, "utf8");
4100
+ writeFileSync4(abs, content, "utf8");
3971
4101
  return { path: relativePath };
3972
4102
  }
3973
4103
  async function getDiffSummary(worktreePath, repoPath, opts) {
@@ -4070,7 +4200,7 @@ async function confirmLand(thread, opts) {
4070
4200
  }
4071
4201
 
4072
4202
  // src/skills/discover.ts
4073
- import { existsSync as existsSync11, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
4203
+ import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
4074
4204
  import { homedir } from "os";
4075
4205
  import { join as join11 } from "path";
4076
4206
  function toCommand(name) {
@@ -4104,7 +4234,7 @@ function parseFrontmatter(content) {
4104
4234
  }
4105
4235
  function readSkill(skillMd, source) {
4106
4236
  try {
4107
- const content = readFileSync8(skillMd, "utf8");
4237
+ const content = readFileSync9(skillMd, "utf8");
4108
4238
  const { name: fmName, description } = parseFrontmatter(content);
4109
4239
  const dirName = skillMd.split("/").slice(-2, -1)[0] || "skill";
4110
4240
  const name = fmName || dirName;
@@ -4123,7 +4253,7 @@ function readSkill(skillMd, source) {
4123
4253
  }
4124
4254
  }
4125
4255
  function scanSkillsDir(dir, source, out) {
4126
- if (!existsSync11(dir)) return;
4256
+ if (!existsSync12(dir)) return;
4127
4257
  let entries;
4128
4258
  try {
4129
4259
  entries = readdirSync4(dir);
@@ -4133,7 +4263,7 @@ function scanSkillsDir(dir, source, out) {
4133
4263
  for (const entry of entries) {
4134
4264
  if (entry.startsWith(".")) continue;
4135
4265
  const skillMd = join11(dir, entry, "SKILL.md");
4136
- if (!existsSync11(skillMd)) continue;
4266
+ if (!existsSync12(skillMd)) continue;
4137
4267
  try {
4138
4268
  if (!statSync3(skillMd).isFile()) continue;
4139
4269
  } catch {
@@ -4144,7 +4274,7 @@ function scanSkillsDir(dir, source, out) {
4144
4274
  }
4145
4275
  }
4146
4276
  function scanClaudePluginSkills(pluginsRoot, out) {
4147
- if (!existsSync11(pluginsRoot)) return;
4277
+ if (!existsSync12(pluginsRoot)) return;
4148
4278
  const walk = (dir, depth, lookingForSkillsDir) => {
4149
4279
  if (depth > 7) return;
4150
4280
  let entries;
@@ -4201,7 +4331,7 @@ function discoverSkills(worktreePath) {
4201
4331
  return [...byCommand.values()].sort((a, b) => a.command.localeCompare(b.command));
4202
4332
  }
4203
4333
  function readSkillBody(skillPath, maxChars = 12e3) {
4204
- const raw = readFileSync8(skillPath, "utf8");
4334
+ const raw = readFileSync9(skillPath, "utf8");
4205
4335
  if (raw.startsWith("---")) {
4206
4336
  const end = raw.indexOf("\n---", 3);
4207
4337
  if (end >= 0) {
@@ -4294,7 +4424,7 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
4294
4424
  }
4295
4425
 
4296
4426
  // src/composer/stage-files.ts
4297
- import { copyFileSync as copyFileSync3, existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync9, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
4427
+ import { copyFileSync as copyFileSync3, existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync10, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
4298
4428
  import { basename as basename3, extname, join as join12 } from "path";
4299
4429
  import { randomUUID as randomUUID4 } from "crypto";
4300
4430
  var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
@@ -4333,19 +4463,19 @@ function ensureAttachmentsDir(worktreePath) {
4333
4463
  const dir = join12(worktreePath, ATTACHMENTS_DIR);
4334
4464
  mkdirSync4(dir, { recursive: true });
4335
4465
  const gi = join12(dir, ".gitignore");
4336
- if (!existsSync12(gi)) {
4337
- writeFileSync4(gi, attachmentsGitignoreBody(), "utf8");
4466
+ if (!existsSync13(gi)) {
4467
+ writeFileSync5(gi, attachmentsGitignoreBody(), "utf8");
4338
4468
  }
4339
4469
  return dir;
4340
4470
  }
4341
4471
  function uniqueAttachmentName(dir, originalName) {
4342
4472
  const safe = originalName.replace(/[/\\]/g, "_") || "file";
4343
- if (!existsSync12(join12(dir, safe))) return safe;
4473
+ if (!existsSync13(join12(dir, safe))) return safe;
4344
4474
  const ext = extname(safe);
4345
4475
  const stem = ext ? safe.slice(0, -ext.length) : safe;
4346
4476
  for (let i = 1; i < 1e4; i++) {
4347
4477
  const candidate = `${stem}-${i}${ext}`;
4348
- if (!existsSync12(join12(dir, candidate))) return candidate;
4478
+ if (!existsSync13(join12(dir, candidate))) return candidate;
4349
4479
  }
4350
4480
  return `${stem}-${randomUUID4()}${ext}`;
4351
4481
  }
@@ -4409,7 +4539,7 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
4409
4539
  const destAbs = join12(dir, name);
4410
4540
  copyFileSync3(abs, destAbs);
4411
4541
  const rel = `${ATTACHMENTS_DIR}/${name}`;
4412
- const buf = readFileSync9(destAbs);
4542
+ const buf = readFileSync10(destAbs);
4413
4543
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
4414
4544
  } catch (err) {
4415
4545
  out.push({
@@ -4422,17 +4552,17 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
4422
4552
  }
4423
4553
  return out;
4424
4554
  }
4425
- function stageBuffersAsAttachments(worktreePath, buffers) {
4426
- if (buffers.length === 0) return [];
4555
+ function stageBuffersAsAttachments(worktreePath, buffers2) {
4556
+ if (buffers2.length === 0) return [];
4427
4557
  const dir = ensureAttachmentsDir(worktreePath);
4428
4558
  const out = [];
4429
- for (const item of buffers) {
4559
+ for (const item of buffers2) {
4430
4560
  const originalName = (item.name || "file").replace(/[/\\]/g, "_") || "file";
4431
4561
  try {
4432
4562
  const buf = Buffer.from(item.dataBase64, "base64");
4433
4563
  const name = uniqueAttachmentName(dir, originalName);
4434
4564
  const destAbs = join12(dir, name);
4435
- writeFileSync4(destAbs, buf);
4565
+ writeFileSync5(destAbs, buf);
4436
4566
  const rel = `${ATTACHMENTS_DIR}/${name}`;
4437
4567
  out.push(attachmentFromBuffer(name, buf, { path: rel }));
4438
4568
  } catch (err) {
@@ -4463,7 +4593,7 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
4463
4593
  const abs = join12(worktreePath, rel);
4464
4594
  const st = statSync4(abs);
4465
4595
  if (!st.isFile()) continue;
4466
- const buf = readFileSync9(abs);
4596
+ const buf = readFileSync10(abs);
4467
4597
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
4468
4598
  } catch (err) {
4469
4599
  out.push({
@@ -4478,7 +4608,7 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
4478
4608
  }
4479
4609
 
4480
4610
  // src/agents/instructions.ts
4481
- import { existsSync as existsSync13, readFileSync as readFileSync10, statSync as statSync5 } from "fs";
4611
+ import { existsSync as existsSync14, readFileSync as readFileSync11, statSync as statSync5 } from "fs";
4482
4612
  import { join as join13 } from "path";
4483
4613
  function normPath(p) {
4484
4614
  return p.replace(/\/+$/, "");
@@ -4668,6 +4798,15 @@ function isPidAlive(pid) {
4668
4798
  return false;
4669
4799
  }
4670
4800
  }
4801
+ async function waitForPidExit(pid, timeoutMs) {
4802
+ const start = Date.now();
4803
+ while (isPidAlive(pid)) {
4804
+ if (Date.now() - start >= timeoutMs) return false;
4805
+ await new Promise((r) => setTimeout(r, 80));
4806
+ }
4807
+ return true;
4808
+ }
4809
+ var STALE_AGENT_PID_WAIT_MS = 2500;
4671
4810
  function writeLiveStatus(threadId, status, lastError) {
4672
4811
  const latest = readThread(threadId);
4673
4812
  if (!latest || latest.status === "archived") return latest;
@@ -4745,7 +4884,7 @@ var Orchestrator = class {
4745
4884
  }
4746
4885
  continue;
4747
4886
  }
4748
- if (!existsSync14(thread.worktreePath)) {
4887
+ if (!existsSync15(thread.worktreePath)) {
4749
4888
  setStatus(thread.id, "broken", "Worktree missing on disk");
4750
4889
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
4751
4890
  continue;
@@ -4764,7 +4903,7 @@ var Orchestrator = class {
4764
4903
  orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
4765
4904
  });
4766
4905
  }
4767
- const { autoCleanupOrphansEnabled } = await import("../app-settings-ZXPAB3QN.js");
4906
+ const { autoCleanupOrphansEnabled } = await import("../app-settings-VDJK2BZ2.js");
4768
4907
  if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
4769
4908
  await cleanupOrphanWorktrees({ repoPaths });
4770
4909
  }
@@ -4932,7 +5071,7 @@ var Orchestrator = class {
4932
5071
  }
4933
5072
  }
4934
5073
  await setup;
4935
- const { autoRunAfterSetupEnabled } = await import("../app-settings-ZXPAB3QN.js");
5074
+ const { autoRunAfterSetupEnabled } = await import("../app-settings-VDJK2BZ2.js");
4936
5075
  if (autoRunAfterSetupEnabled()) {
4937
5076
  try {
4938
5077
  await this.startDev(threadId);
@@ -5066,14 +5205,10 @@ var Orchestrator = class {
5066
5205
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
5067
5206
  const livePid = current.agentPid;
5068
5207
  const foreignLive = !inFlight && typeof livePid === "number" && livePid > 0 && isPidAlive(livePid);
5069
- if (inFlight) {
5208
+ if (inFlight || foreignLive) {
5070
5209
  this.stop(thread.id, { clearQueue: false, continueQueue: true });
5071
- } else {
5072
- if (foreignLive) {
5073
- this.stop(thread.id, { clearQueue: false, continueQueue: true });
5074
- }
5075
- void this.drainQueue(thread.id);
5076
5210
  }
5211
+ void this.drainQueue(thread.id);
5077
5212
  return this.requireThread(thread.id);
5078
5213
  }
5079
5214
  async drainQueue(threadId) {
@@ -5102,7 +5237,18 @@ var Orchestrator = class {
5102
5237
  }
5103
5238
  const livePid = thread.agentPid;
5104
5239
  if (typeof livePid === "number" && livePid > 0 && isPidAlive(livePid)) {
5105
- await new Promise((r) => setTimeout(r, 200));
5240
+ const exited = await waitForPidExit(livePid, STALE_AGENT_PID_WAIT_MS);
5241
+ if (!exited && isPidAlive(livePid)) {
5242
+ try {
5243
+ process.kill(livePid, "SIGKILL");
5244
+ } catch {
5245
+ }
5246
+ await waitForPidExit(livePid, 400);
5247
+ try {
5248
+ updateThread(threadId, { agentPid: null });
5249
+ } catch {
5250
+ }
5251
+ }
5106
5252
  continue;
5107
5253
  }
5108
5254
  const prompt = thread.queue[0];
@@ -5210,7 +5356,7 @@ var Orchestrator = class {
5210
5356
  }
5211
5357
  const isBrightsy = fresh.agent === "brightsy";
5212
5358
  const isOrchestration = isOrchestratorThread(fresh);
5213
- const { autoRenameBranchEnabled, getGithubGitAuthMode } = await import("../app-settings-ZXPAB3QN.js");
5359
+ const { autoRenameBranchEnabled, getGithubGitAuthMode } = await import("../app-settings-VDJK2BZ2.js");
5214
5360
  const gitAuthMode = getGithubGitAuthMode();
5215
5361
  const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
5216
5362
  githubSlug: await resolveGithubRepoSlug(fresh.worktreePath).catch(
@@ -5259,6 +5405,7 @@ var Orchestrator = class {
5259
5405
  { cachedPrefix, prompt: agentPrompt },
5260
5406
  (event) => {
5261
5407
  this.emit({ type: "turn_output", threadId, event });
5408
+ noteTurnLiveEvent(threadId, event);
5262
5409
  if (event.type === "session_id") {
5263
5410
  updateThread(threadId, { sessionId: event.data });
5264
5411
  }
@@ -5305,7 +5452,7 @@ var Orchestrator = class {
5305
5452
  if (this.requireThread(threadId).agent === "cursor" && exitCode !== 0 && !assistantText && parts.length === 0) {
5306
5453
  const sessionId = result.sessionId || this.requireThread(threadId).sessionId || "";
5307
5454
  if (sessionId) {
5308
- const { recoverFinishedCursorRun } = await import("../cursor-recover-S5ORPW34.js");
5455
+ const { recoverFinishedCursorRun } = await import("../cursor-recover-JBTIDARH.js");
5309
5456
  for (let i = 0; i < 8; i++) {
5310
5457
  const recovered = recoverFinishedCursorRun({
5311
5458
  agentId: sessionId,
@@ -5322,19 +5469,16 @@ var Orchestrator = class {
5322
5469
  }
5323
5470
  let lastStderr = summarizeTurnStderr(stderrTail);
5324
5471
  let detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
5325
- if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && looksLikeInvalidAgentSession(detail) && this.requireThread(threadId).sessionId && this.requireThread(threadId).agent !== "brightsy") {
5472
+ if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && this.requireThread(threadId).agent !== "brightsy" && shouldRetryFailedAgentTurn(detail, {
5473
+ hasSession: Boolean(this.requireThread(threadId).sessionId)
5474
+ })) {
5326
5475
  updateThread(threadId, { sessionId: null });
5327
- pushTurnStderr(
5328
- stderrTail,
5329
- "Agent session missing \u2014 starting a fresh session"
5330
- );
5476
+ const retryNote = looksLikeInvalidAgentSession(detail) ? "Agent session missing \u2014 starting a fresh session" : "Agent runner crashed \u2014 restarting Node once";
5477
+ pushTurnStderr(stderrTail, retryNote);
5331
5478
  this.emit({
5332
5479
  type: "turn_output",
5333
5480
  threadId,
5334
- event: {
5335
- type: "stderr",
5336
- data: "Agent session missing \u2014 starting a fresh session"
5337
- }
5481
+ event: { type: "stderr", data: retryNote }
5338
5482
  });
5339
5483
  const retryThread = this.requireThread(threadId);
5340
5484
  const prior = retryThread.messages.slice(0, -1);
@@ -5383,10 +5527,7 @@ var Orchestrator = class {
5383
5527
  lastStderr = summarizeTurnStderr(stderrTail);
5384
5528
  detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
5385
5529
  }
5386
- let chatText = assistantText;
5387
- if (exitCode !== 0 && !chatText && looksLikeAgentFailureMessage(detail)) {
5388
- chatText = humanizeAgentFailDetail(detail);
5389
- }
5530
+ let chatText = this.stoppedTurns.has(threadId) ? assistantText : turnFailChatText({ exitCode, assistantText, detail });
5390
5531
  if (chatText || parts.length > 0) {
5391
5532
  appendMessage(threadId, {
5392
5533
  role: "agent",
@@ -5469,6 +5610,7 @@ var Orchestrator = class {
5469
5610
  this.processes.delete(`${threadId}:agent`);
5470
5611
  this.stoppedTurns.delete(threadId);
5471
5612
  this.runningCount = Math.max(0, this.runningCount - 1);
5613
+ clearTurnLive(threadId);
5472
5614
  try {
5473
5615
  updateThread(threadId, { agentPid: null });
5474
5616
  } catch {
@@ -5742,51 +5884,56 @@ var Orchestrator = class {
5742
5884
  }
5743
5885
  return created;
5744
5886
  }
5745
- async waitForTurn(threadRef, timeoutMs = 6e5) {
5887
+ async waitForTurn(threadRef, timeoutMs = 6e5, opts) {
5746
5888
  const thread = this.requireThread(threadRef);
5889
+ if (!["running", "queued"].includes(thread.status)) {
5890
+ return thread;
5891
+ }
5747
5892
  const start = Date.now();
5748
5893
  return new Promise((resolve, reject) => {
5749
- if (!["running", "queued"].includes(thread.status)) {
5750
- resolve(thread);
5751
- return;
5752
- }
5894
+ let settled = false;
5895
+ let timer;
5896
+ const finish = (fn) => {
5897
+ if (settled) return;
5898
+ settled = true;
5899
+ if (timer) clearInterval(timer);
5900
+ off();
5901
+ fn();
5902
+ };
5753
5903
  const off = this.on((event) => {
5754
5904
  if (event.type === "turn_finished" && event.threadId === thread.id) {
5755
- off();
5756
5905
  const latest = readThread(thread.id);
5757
5906
  if (!latest) {
5758
- reject(new Error(`Thread not found: ${thread.id}`));
5907
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
5759
5908
  return;
5760
5909
  }
5761
- resolve(latest);
5910
+ finish(() => resolve(latest));
5762
5911
  }
5763
5912
  if (event.type === "error" && event.threadId === thread.id) {
5764
- off();
5765
5913
  const latest = readThread(thread.id);
5766
5914
  if (!latest) {
5767
- reject(new Error(`Thread not found: ${thread.id}`));
5915
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
5768
5916
  return;
5769
5917
  }
5770
- resolve(latest);
5918
+ finish(() => resolve(latest));
5771
5919
  }
5772
5920
  });
5773
- const timer = setInterval(() => {
5774
- if (Date.now() - start > timeoutMs) {
5775
- clearInterval(timer);
5776
- off();
5777
- reject(new Error("wait_for_turn timed out"));
5778
- }
5921
+ timer = setInterval(() => {
5779
5922
  const current = readThread(thread.id);
5780
5923
  if (!current) {
5781
- clearInterval(timer);
5782
- off();
5783
- reject(new Error(`Thread not found: ${thread.id}`));
5924
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
5784
5925
  return;
5785
5926
  }
5786
5927
  if (!["running", "queued"].includes(current.status)) {
5787
- clearInterval(timer);
5788
- off();
5789
- resolve(current);
5928
+ finish(() => resolve(current));
5929
+ return;
5930
+ }
5931
+ if (Date.now() - start > timeoutMs) {
5932
+ if (opts?.resolveIfStillRunning) {
5933
+ finish(() => resolve(current));
5934
+ } else {
5935
+ finish(() => reject(new Error("wait_for_turn timed out")));
5936
+ }
5790
5937
  }
5791
5938
  }, 200);
5792
5939
  });
@@ -5794,10 +5941,18 @@ var Orchestrator = class {
5794
5941
  getTurnResult(threadRef) {
5795
5942
  const thread = this.requireThread(threadRef);
5796
5943
  const lastAgent = [...thread.messages].reverse().find((m) => m.role === "agent");
5944
+ const lastError = thread.lastError ?? null;
5945
+ const text3 = (lastAgent?.text ?? "").trim() || (thread.status === "error" ? lastError ?? "" : "");
5946
+ const stillRunning = thread.status === "running" || thread.status === "queued";
5947
+ const live = stillRunning ? readTurnLive(thread.id) : null;
5797
5948
  return {
5798
- text: lastAgent?.text ?? "",
5949
+ text: text3,
5799
5950
  status: thread.status,
5800
- sessionId: thread.sessionId
5951
+ sessionId: thread.sessionId,
5952
+ lastError,
5953
+ stillRunning,
5954
+ progress: live?.summary ?? null,
5955
+ lastActivityAt: live?.updatedAt ?? null
5801
5956
  };
5802
5957
  }
5803
5958
  assertNotGlobal(thread, action) {
@@ -5960,7 +6115,7 @@ var Orchestrator = class {
5960
6115
  updateThread(thread.id, { title: meta.title });
5961
6116
  }
5962
6117
  }
5963
- const { autoArchiveOnMergeEnabled } = await import("../app-settings-ZXPAB3QN.js");
6118
+ const { autoArchiveOnMergeEnabled } = await import("../app-settings-VDJK2BZ2.js");
5964
6119
  const latest = this.requireThread(thread.id);
5965
6120
  if (!shouldAutoArchiveOnPrMerge({
5966
6121
  previousPrState: prevState || null,
@@ -6223,7 +6378,19 @@ var Orchestrator = class {
6223
6378
  const thread = this.requireThread(threadRef);
6224
6379
  return threadsSharingWorktree(thread.worktreePath);
6225
6380
  }
6381
+ /**
6382
+ * Last-tab teardown must see siblings already archived. Serialize per
6383
+ * worktree so parallel archive/purge of sibling tabs still removes once.
6384
+ */
6385
+ enqueueWorktreeTeardown(thread, fn) {
6386
+ const key = isGlobalThread(thread) ? `archive:global:${thread.id}` : `archive:wt:${normalizeWorktreePath(thread.worktreePath)}`;
6387
+ return enqueueByKey(key, fn);
6388
+ }
6226
6389
  async archive(threadRef) {
6390
+ const thread = this.requireThread(threadRef);
6391
+ return this.enqueueWorktreeTeardown(thread, () => this.archiveUnlocked(threadRef));
6392
+ }
6393
+ async archiveUnlocked(threadRef) {
6227
6394
  const thread = this.requireThread(threadRef);
6228
6395
  this.stop(thread.id);
6229
6396
  this.releaseOrchestratorCaffeinate(thread);
@@ -6251,7 +6418,7 @@ var Orchestrator = class {
6251
6418
  this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
6252
6419
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
6253
6420
  try {
6254
- const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-MZVQHRSJ.js");
6421
+ const { ensureWorkspace: ensureWorkspace2 } = await import("../workspaces-NTIVI6DI.js");
6255
6422
  await ensureWorkspace2(thread.repoPath);
6256
6423
  } catch {
6257
6424
  }
@@ -6259,6 +6426,10 @@ var Orchestrator = class {
6259
6426
  return archived;
6260
6427
  }
6261
6428
  async purge(threadRef, opts) {
6429
+ const thread = this.requireThread(threadRef);
6430
+ await this.enqueueWorktreeTeardown(thread, () => this.purgeUnlocked(threadRef, opts));
6431
+ }
6432
+ async purgeUnlocked(threadRef, opts) {
6262
6433
  const thread = this.requireThread(threadRef);
6263
6434
  this.stop(thread.id);
6264
6435
  this.releaseOrchestratorCaffeinate(thread);
@@ -6273,7 +6444,7 @@ var Orchestrator = class {
6273
6444
  await runArchiveScript(thread.repoPath, thread.worktreePath);
6274
6445
  } catch {
6275
6446
  }
6276
- const { deleteBranchOnPurgeEnabled } = await import("../app-settings-ZXPAB3QN.js");
6447
+ const { deleteBranchOnPurgeEnabled } = await import("../app-settings-VDJK2BZ2.js");
6277
6448
  const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
6278
6449
  await removeWorktree(thread.repoPath, thread.worktreePath, {
6279
6450
  deleteBranch: deleteBranch ? thread.branchName : void 0
@@ -6287,19 +6458,18 @@ var Orchestrator = class {
6287
6458
  throw new Error("Thread is not archived");
6288
6459
  }
6289
6460
  if (isGlobalThread(thread)) {
6290
- const { globalAgentCwd } = await import("../paths-TZX4L5X7.js");
6461
+ const { globalAgentCwd } = await import("../paths-QCI3MTO5.js");
6291
6462
  updateThread(thread.id, { worktreePath: globalAgentCwd() });
6292
6463
  const restored2 = setStatus(thread.id, "idle");
6293
6464
  this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
6294
6465
  return restored2;
6295
6466
  }
6296
- if (!existsSync14(thread.worktreePath)) {
6297
- const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-EO5QAGJU.js");
6298
- const { execa: execa5 } = await import("execa");
6467
+ if (!existsSync15(thread.worktreePath)) {
6468
+ const { createThreadWorktree: createThreadWorktree2 } = await import("../worktree-NKWN5ZTS.js");
6299
6469
  const slug = thread.worktreePath.split("/").pop();
6300
6470
  const dest = thread.worktreePath;
6301
- await execa5("git", ["worktree", "add", dest, thread.branchName], {
6302
- cwd: thread.repoPath
6471
+ await withRepoGitLock(thread.repoPath, async () => {
6472
+ await git(["worktree", "add", dest, thread.branchName], thread.repoPath);
6303
6473
  });
6304
6474
  void createThreadWorktree2;
6305
6475
  void slug;
@@ -6846,6 +7016,15 @@ function mcpArchiveBlockedReason(thread) {
6846
7016
  return null;
6847
7017
  }
6848
7018
 
7019
+ // src/mcp/wait-for-turn.ts
7020
+ var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
7021
+ function mcpWaitForTurnTimeoutMs(requested) {
7022
+ const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
7023
+ if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
7024
+ return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
7025
+ }
7026
+ var MCP_WAIT_STILL_RUNNING_HINT = "Child is still working. Call wait_for_turn again. Do not send a check-in prompt or assume a hang while progress is updating.";
7027
+
6849
7028
  // src/mcp/slack-tools.ts
6850
7029
  import { z } from "zod";
6851
7030
 
@@ -7473,7 +7652,7 @@ async function startMcpServer() {
7473
7652
  );
7474
7653
  }
7475
7654
  try {
7476
- const { maxConcurrentAgents } = await import("../app-settings-ZXPAB3QN.js");
7655
+ const { maxConcurrentAgents } = await import("../app-settings-VDJK2BZ2.js");
7477
7656
  orch.setMaxConcurrent(maxConcurrentAgents());
7478
7657
  } catch {
7479
7658
  }
@@ -7516,7 +7695,9 @@ async function startMcpServer() {
7516
7695
  const threads = orch.getThreads(true);
7517
7696
  const lines = threads.map((t) => {
7518
7697
  const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : basename4(t.repoPath) || t.repoPath;
7519
- return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}`;
7698
+ const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
7699
+ const progress = live?.summary ? ` ${live.summary}` : "";
7700
+ return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}${progress}`;
7520
7701
  });
7521
7702
  return {
7522
7703
  content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
@@ -7525,13 +7706,14 @@ async function startMcpServer() {
7525
7706
  );
7526
7707
  server.tool(
7527
7708
  "get_thread",
7528
- "Get a compact thread summary by id/ref",
7709
+ "Get a compact thread summary by id/ref. While running, includes progress (last tool/thinking) and lastActivityAt.",
7529
7710
  { ref: z3.string() },
7530
7711
  async ({ ref }) => {
7531
7712
  const t = orch.getThread(ref);
7532
7713
  if (!t) {
7533
7714
  return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
7534
7715
  }
7716
+ const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
7535
7717
  const summary = {
7536
7718
  id: t.id,
7537
7719
  title: t.title,
@@ -7546,7 +7728,10 @@ async function startMcpServer() {
7546
7728
  messageCount: t.messages.length,
7547
7729
  devPort: t.devPort,
7548
7730
  prUrl: t.prUrl,
7549
- lastError: t.lastError ?? null
7731
+ lastError: t.lastError ?? null,
7732
+ stillRunning: t.status === "running" || t.status === "queued",
7733
+ progress: live?.summary ?? null,
7734
+ lastActivityAt: live?.updatedAt ?? null
7550
7735
  };
7551
7736
  return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
7552
7737
  }
@@ -7682,8 +7867,8 @@ async function startMcpServer() {
7682
7867
  registerSlackTools(server);
7683
7868
  registerLinearTools(server);
7684
7869
  if (sideboardMcpProfile() !== "worktree") {
7685
- const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-U3QJBV67.js");
7686
- const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-ZXPAB3QN.js");
7870
+ const { getCaffeinateHold, setCaffeinateHold } = await import("../caffeinate-hold-NBRUWYXO.js");
7871
+ const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults } = await import("../app-settings-VDJK2BZ2.js");
7687
7872
  const accountDefaults = resolveThreadDefaults();
7688
7873
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
7689
7874
  server.tool(
@@ -7872,13 +8057,15 @@ async function startMcpServer() {
7872
8057
  );
7873
8058
  server.tool(
7874
8059
  "wait_for_turn",
7875
- "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread or ask_git to read the agent reply.",
8060
+ "Wait until the thread finishes its current/queued turn, or return early with a live progress snapshot. MCP clients often kill tools around 60s, so this returns within 45s even while the child is still working. If stillRunning is true, progress is tools/thinking \u2014 call wait_for_turn again. Do not send a check-in prompt or assume a hang. On status error, lastError/text is the failure.",
7876
8061
  {
7877
8062
  ref: z3.string(),
7878
8063
  timeoutMs: z3.number().optional()
7879
8064
  },
7880
8065
  async ({ ref, timeoutMs }) => {
7881
- const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
8066
+ const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
8067
+ resolveIfStillRunning: true
8068
+ });
7882
8069
  const result = orch.getTurnResult(thread.id);
7883
8070
  return {
7884
8071
  content: [
@@ -7887,7 +8074,12 @@ async function startMcpServer() {
7887
8074
  text: JSON.stringify({
7888
8075
  id: thread.id,
7889
8076
  status: result.status,
7890
- text: result.text
8077
+ text: result.text,
8078
+ lastError: result.lastError,
8079
+ stillRunning: result.stillRunning,
8080
+ progress: result.progress,
8081
+ lastActivityAt: result.lastActivityAt,
8082
+ hint: result.stillRunning ? MCP_WAIT_STILL_RUNNING_HINT : void 0
7891
8083
  })
7892
8084
  }
7893
8085
  ]
@@ -7896,11 +8088,21 @@ async function startMcpServer() {
7896
8088
  );
7897
8089
  server.tool(
7898
8090
  "get_turn_result",
7899
- "Final assistant message only (not full transcript)",
8091
+ "Assistant message when the turn finished, or live progress while stillRunning. Not the full transcript.",
7900
8092
  { ref: z3.string() },
7901
8093
  async ({ ref }) => {
7902
8094
  const result = orch.getTurnResult(ref);
7903
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
8095
+ return {
8096
+ content: [
8097
+ {
8098
+ type: "text",
8099
+ text: JSON.stringify({
8100
+ ...result,
8101
+ hint: result.stillRunning ? MCP_WAIT_STILL_RUNNING_HINT : void 0
8102
+ })
8103
+ }
8104
+ ]
8105
+ };
7904
8106
  }
7905
8107
  );
7906
8108
  server.tool(
@@ -8017,7 +8219,7 @@ async function startMcpServer() {
8017
8219
  );
8018
8220
  server.tool(
8019
8221
  "request_review",
8020
- 'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .sideboard/review.md when present (else local Review request.md / stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn / get_turn_result on the returned review tab id.',
8222
+ 'Start a merge-readiness Review on a worktree agent thread (same as the desktop Review button). Opens a new Review chat tab, attaches .sideboard/review.md when present (else local Review request.md / stock template), and sends "Review changes in this workspace." Expect Approve / Approve with nits / Request changes / Needs more information. Pass a worktree thread ref \u2014 not the orchestrator. Then wait_for_turn (loop while stillRunning) / get_turn_result on the returned review tab id.',
8021
8223
  { ref: z3.string().describe("Worktree thread id/ref to review") },
8022
8224
  async ({ ref }) => {
8023
8225
  try {
@@ -8045,7 +8247,7 @@ async function startMcpServer() {
8045
8247
  );
8046
8248
  server.tool(
8047
8249
  "ask_git",
8048
- "Commit & push, open a draft PR, resolve conflicts, or merge \u2014 same actions as the desktop git buttons. When the worktree is clean, Sideboard pushes / opens the PR itself (HTTPS via `gh` if SSH is missing). When dirty, queues the worktree agent to commit; then wait_for_turn. Pass a worktree thread ref (not the orchestrator). action=merge only when the user explicitly asked to merge that PR. Do not run git or gh from the orchestration cwd.",
8250
+ "Commit & push, open a draft PR, resolve conflicts, or merge \u2014 same actions as the desktop git buttons. When the worktree is clean, Sideboard pushes / opens the PR itself (HTTPS via `gh` if SSH is missing). When dirty, queues the worktree agent to commit; then wait_for_turn (loop while stillRunning). Pass a worktree thread ref (not the orchestrator). action=merge only when the user explicitly asked to merge that PR. Do not run git or gh from the orchestration cwd.",
8049
8251
  {
8050
8252
  ref: z3.string().describe("Worktree thread id/ref"),
8051
8253
  action: z3.enum(AGENT_GIT_ACTIONS).describe(
@@ -8096,7 +8298,7 @@ async function startMcpServer() {
8096
8298
  );
8097
8299
  server.tool(
8098
8300
  "fork_worktree",
8099
- "Fork a worktree agent chat into a NEW git worktree + chat (desktop \u201CFork to new workspace\u201D). Seeds a transcript (through through_index, default all). Optional agent override. Leave model unset for Auto (default) \u2014 only pass model when you have a reason. Not for the orchestrator. Then send_to_thread / wait_for_turn on the returned id.",
8301
+ "Fork a worktree agent chat into a NEW git worktree + chat (desktop \u201CFork to new workspace\u201D). Seeds a transcript (through through_index, default all). Optional agent override. Leave model unset for Auto (default) \u2014 only pass model when you have a reason. Not for the orchestrator. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id.",
8100
8302
  {
8101
8303
  ref: z3.string().describe("Worktree thread id/ref to fork"),
8102
8304
  through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -8141,7 +8343,7 @@ async function startMcpServer() {
8141
8343
  );
8142
8344
  server.tool(
8143
8345
  "fork_chat",
8144
- "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
8346
+ "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Orchestration forks require an MCP-capable agent (claude, cursor, codex, opencode \u2014 not brightsy). Slack / Global orchestrators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn (loop while stillRunning) on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
8145
8347
  {
8146
8348
  ref: z3.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
8147
8349
  through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),