@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
package/dist/index.cjs CHANGED
@@ -455,6 +455,7 @@ __export(paths_exports, {
455
455
  sideboardReposDir: () => sideboardReposDir,
456
456
  sideboardWorkspacesDir: () => sideboardWorkspacesDir,
457
457
  threadFilePath: () => threadFilePath,
458
+ threadLivePath: () => threadLivePath,
458
459
  threadLockPath: () => threadLockPath,
459
460
  threadsDir: () => threadsDir,
460
461
  worktreesRoot: () => worktreesRoot
@@ -506,6 +507,9 @@ function worktreesRoot(repoPath) {
506
507
  function threadFilePath(id) {
507
508
  return (0, import_node_path5.join)(threadsDir(), `${id}.json`);
508
509
  }
510
+ function threadLivePath(id) {
511
+ return (0, import_node_path5.join)(threadsDir(), `${id}.live.json`);
512
+ }
509
513
  function threadLockPath(id) {
510
514
  return (0, import_node_path5.join)(locksDir(), `${id}.lock`);
511
515
  }
@@ -2039,8 +2043,11 @@ function writeThread(thread) {
2039
2043
  function idPath(id) {
2040
2044
  return id;
2041
2045
  }
2046
+ function isThreadRecordFile(name) {
2047
+ return name.endsWith(".json") && !name.endsWith(".live.json");
2048
+ }
2042
2049
  function listThreads(opts) {
2043
- const files = (0, import_node_fs9.readdirSync)(threadsDir()).filter((f) => f.endsWith(".json"));
2050
+ const files = (0, import_node_fs9.readdirSync)(threadsDir()).filter(isThreadRecordFile);
2044
2051
  const threads = files.map((f) => {
2045
2052
  try {
2046
2053
  return normalizeThread(
@@ -2049,7 +2056,9 @@ function listThreads(opts) {
2049
2056
  } catch {
2050
2057
  return null;
2051
2058
  }
2052
- }).filter((t) => t !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2059
+ }).filter(
2060
+ (t) => t !== null && typeof t.id === "string" && t.id.length > 0
2061
+ ).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2053
2062
  if (opts?.includeArchived) return threads;
2054
2063
  return threads.filter((t) => t.status !== "archived");
2055
2064
  }
@@ -2081,7 +2090,9 @@ function setStatus(id, status, lastError) {
2081
2090
  }
2082
2091
  function findThreadByRef(ref) {
2083
2092
  const all = listThreads({ includeArchived: true });
2084
- return all.find((t) => t.id === ref || t.id.startsWith(ref) || t.branchName === ref || t.title === ref) ?? null;
2093
+ return all.find(
2094
+ (t) => t.id === ref || t.id.startsWith(ref) || t.branchName === ref || t.title === ref
2095
+ ) ?? null;
2085
2096
  }
2086
2097
  var import_node_crypto3, import_node_fs9, import_proper_lockfile;
2087
2098
  var init_thread_store = __esm({
@@ -2938,7 +2949,7 @@ var init_teams = __esm({
2938
2949
 
2939
2950
  // src/git/worktree-labels.ts
2940
2951
  function normalizeWorktreePath(worktreePath) {
2941
- const trimmed = worktreePath.replace(/\/+$/, "") || "/";
2952
+ const trimmed = (worktreePath ?? "").replace(/\/+$/, "") || "/";
2942
2953
  const absolute = trimmed.startsWith("/");
2943
2954
  const parts = trimmed.split("/").filter((p) => p && p !== ".");
2944
2955
  const out = [];
@@ -3525,6 +3536,42 @@ var init_git_auth_mode = __esm({
3525
3536
  }
3526
3537
  });
3527
3538
 
3539
+ // src/util/enqueue-by-key.ts
3540
+ function enqueueByKey(key, fn) {
3541
+ const prev = tails.get(key) ?? Promise.resolve();
3542
+ const next = prev.then(fn, fn);
3543
+ tails.set(
3544
+ key,
3545
+ next.then(
3546
+ () => void 0,
3547
+ () => void 0
3548
+ )
3549
+ );
3550
+ return next;
3551
+ }
3552
+ var tails;
3553
+ var init_enqueue_by_key = __esm({
3554
+ "src/util/enqueue-by-key.ts"() {
3555
+ "use strict";
3556
+ tails = /* @__PURE__ */ new Map();
3557
+ }
3558
+ });
3559
+
3560
+ // src/git/repo-git-lock.ts
3561
+ function repoLockKey(repoPath) {
3562
+ const trimmed = repoPath.replace(/\/+$/, "");
3563
+ return `git-worktree:${trimmed || "/"}`;
3564
+ }
3565
+ function withRepoGitLock(repoPath, fn) {
3566
+ return enqueueByKey(repoLockKey(repoPath), fn);
3567
+ }
3568
+ var init_repo_git_lock = __esm({
3569
+ "src/git/repo-git-lock.ts"() {
3570
+ "use strict";
3571
+ init_enqueue_by_key();
3572
+ }
3573
+ });
3574
+
3528
3575
  // src/git/pr-gates.ts
3529
3576
  function prIsInMergeQueue(gate) {
3530
3577
  if (gate.isInMergeQueue) return true;
@@ -4706,12 +4753,13 @@ async function createThreadWorktree(opts) {
4706
4753
  `Invalid git reference: ${opts.sourceRef} (and fetch did not resolve it)`
4707
4754
  );
4708
4755
  }
4709
- const add = await git(
4710
- ["worktree", "add", "-b", branchName, worktreePath, startPoint],
4711
- opts.repoPath,
4712
- { reject: false, timeoutMs: 6e4 }
4713
- );
4714
- if (add.exitCode !== 0) {
4756
+ const added = await withRepoGitLock(opts.repoPath, async () => {
4757
+ const add = await git(
4758
+ ["worktree", "add", "-b", branchName, worktreePath, startPoint],
4759
+ opts.repoPath,
4760
+ { reject: false, timeoutMs: 6e4 }
4761
+ );
4762
+ if (add.exitCode === 0) return branchName;
4715
4763
  const err = `${add.stderr}
4716
4764
  ${add.stdout}`;
4717
4765
  if (/already used by worktree|already exists|checked out/i.test(err)) {
@@ -4722,17 +4770,14 @@ ${add.stdout}`;
4722
4770
  opts.repoPath,
4723
4771
  { reject: false, timeoutMs: 6e4 }
4724
4772
  );
4725
- if (retry.exitCode === 0) {
4726
- branchName = alt;
4727
- await ensureGhPreferOrigin(worktreePath);
4728
- return { branchName, worktreePath };
4729
- }
4773
+ if (retry.exitCode === 0) return alt;
4730
4774
  }
4731
4775
  }
4732
4776
  throw new Error(
4733
4777
  `Failed to create worktree: ${add.stderr.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
4734
4778
  );
4735
- }
4779
+ });
4780
+ branchName = added;
4736
4781
  await ensureGhPreferOrigin(worktreePath);
4737
4782
  return { branchName, worktreePath };
4738
4783
  }
@@ -4756,12 +4801,13 @@ async function createExistingBranchWorktree(opts) {
4756
4801
  );
4757
4802
  }
4758
4803
  const startPoint = await resolveWorktreeStartPoint(opts.repoPath, branchName);
4759
- const add = await git(
4760
- ["worktree", "add", worktreePath, startPoint],
4761
- opts.repoPath,
4762
- { reject: false }
4763
- );
4764
- if (add.exitCode !== 0) {
4804
+ await withRepoGitLock(opts.repoPath, async () => {
4805
+ const add = await git(
4806
+ ["worktree", "add", worktreePath, startPoint],
4807
+ opts.repoPath,
4808
+ { reject: false }
4809
+ );
4810
+ if (add.exitCode === 0) return;
4765
4811
  const retry = await git(
4766
4812
  ["worktree", "add", worktreePath, branchName],
4767
4813
  opts.repoPath,
@@ -4772,7 +4818,7 @@ async function createExistingBranchWorktree(opts) {
4772
4818
  `Failed to create worktree for ${branchName}: ${retry.stderr.trim() || add.stderr.trim() || retry.stdout.trim() || add.stdout.trim() || `exit ${add.exitCode}`}`
4773
4819
  );
4774
4820
  }
4775
- }
4821
+ });
4776
4822
  const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
4777
4823
  reject: false
4778
4824
  });
@@ -4783,12 +4829,14 @@ async function createExistingBranchWorktree(opts) {
4783
4829
  return { branchName, worktreePath };
4784
4830
  }
4785
4831
  async function removeWorktree(repoPath, worktreePath, opts) {
4786
- await git(["worktree", "remove", "--force", worktreePath], repoPath, {
4787
- reject: false
4832
+ await withRepoGitLock(repoPath, async () => {
4833
+ await git(["worktree", "remove", "--force", worktreePath], repoPath, {
4834
+ reject: false
4835
+ });
4836
+ if (opts?.deleteBranch) {
4837
+ await git(["branch", "-D", opts.deleteBranch], repoPath, { reject: false });
4838
+ }
4788
4839
  });
4789
- if (opts?.deleteBranch) {
4790
- await git(["branch", "-D", opts.deleteBranch], repoPath, { reject: false });
4791
- }
4792
4840
  }
4793
4841
  async function listWorktrees(repoPath) {
4794
4842
  const { stdout } = await git(
@@ -5080,6 +5128,7 @@ var init_worktree = __esm({
5080
5128
  init_gh_errors();
5081
5129
  init_git_auth_mode();
5082
5130
  init_run();
5131
+ init_repo_git_lock();
5083
5132
  init_app_settings();
5084
5133
  init_pr_gates();
5085
5134
  init_stack();
@@ -5124,7 +5173,7 @@ function coordinatorGreenfieldPlaybook(reposDir) {
5124
5173
  "- Examples:",
5125
5174
  ` - Clone: \`git clone <url> ${reposDir}/<name>\``,
5126
5175
  ` - New GitHub repo: \`gh repo create <owner>/<name> --private --clone -- ${reposDir}/<name>\` (or mkdir + git init + gh repo create + remote add + push)`,
5127
- "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn \u2192 ask_git create-draft (or send_to_thread `gh pr create --draft -R <origin-owner/name>`).",
5176
+ "- Then: add_workspace with that absolute path \u2192 create_thread (repoPath + parentThreadId) \u2192 send_to_thread (build) \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft (or send_to_thread `gh pr create --draft -R <origin-owner/name>`).",
5128
5177
  "- Always target the child worktree's **origin** (`github:` slug from list_workspaces / `git remote get-url origin` in that worktree). Never open PRs against `upstream`.",
5129
5178
  "- Do coding work in the child worktree thread, not by editing files in this home cwd."
5130
5179
  ].join("\n");
@@ -5147,8 +5196,8 @@ function coordinatorTurnReminder(opts) {
5147
5196
  goal ? `- Goal / title: ${goal}` : null,
5148
5197
  accountDefaultsPlaybookLine(),
5149
5198
  `- For "what's going on": call list_threads (and list_workspaces if needed). Summarize fleet status \u2014 do not ls/git-status this synthetic home.`,
5150
- "- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn. Commit/push/draft PR with ask_git on the child, then wait_for_turn \u2014 never git/gh from this cwd. Call ask_git merge only if the user explicitly asked to merge.",
5151
- "- New repo: Bash (clone or gh repo create under ~/sideboard/repos/<name>) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn \u2192 ask_git create-draft.",
5199
+ "- Existing repo: create_thread on a repoPath \u2192 send_to_thread \u2192 wait_for_turn. If stillRunning, the child is working (progress is tools/thinking) \u2014 wait_for_turn again; do not ping it. If status is error, lastError/text is the failure \u2014 adapt (switch agent, tell the user). Commit/push/draft PR with ask_git on the child, then wait_for_turn \u2014 never git/gh from this cwd. Call ask_git merge only if the user explicitly asked to merge.",
5200
+ "- New repo: Bash (clone or gh repo create under ~/sideboard/repos/<name>) \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
5152
5201
  "- When naming threads for the user, link them as `[Title](sideboard://thread/<id>)`.",
5153
5202
  "- If they will wait on Slack or leave the Mac, call set_caffeinate enabled=true. When they say they are done / wrapping up / going to sleep, call set_caffeinate enabled=false. Closing this chat also turns it off."
5154
5203
  ].filter(Boolean).join("\n");
@@ -5201,8 +5250,8 @@ function ensureGlobalCoordinatorCwd(opts) {
5201
5250
  orchId ? `Pass parentThreadId="${orchId}" (or omit it). Never invent another parentThreadId.` : "Pass `parentThreadId` for children (this chat's id from the turn reminder).",
5202
5251
  "Omit `agent` / `model` on `create_thread` unless you have a reason to override Account defaults.",
5203
5252
  "Never pass `agent=codex` when you yourself are Codex \u2014 nested Codex deadlocks on shared ~/.codex locks. Omit agent (Account default) or use cursor/claude.",
5204
- "Typical flow (existing): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn \u2192 ask_git create-draft \u2192 wait_for_turn. Merge only if the user explicitly asked (`ask_git` merge).",
5205
- "Typical flow (new app): Bash create/clone under repos dir \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread (implement) \u2192 wait_for_turn \u2192 ask_git create-draft.",
5253
+ "Typical flow (existing): list_workspaces \u2192 list_branches|list_prs|list_issues \u2192 create_thread \u2192 send_to_thread \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft \u2192 wait_for_turn. Merge only if the user explicitly asked (`ask_git` merge).",
5254
+ "Typical flow (new app): Bash create/clone under repos dir \u2192 add_workspace \u2192 create_thread \u2192 send_to_thread (implement) \u2192 wait_for_turn (loop while stillRunning) \u2192 ask_git create-draft.",
5206
5255
  "Always ask worktree agents to commit, push, and open draft PRs (`ask_git` / `send_to_thread`). Tell them to merge only when the user explicitly asked. The worktree agent runs git/gh; never merge from this orchestration cwd."
5207
5256
  ].join("\n");
5208
5257
  try {
@@ -5270,7 +5319,7 @@ var init_coordinator_prompt = __esm({
5270
5319
  "- fork_worktree \u2014 fork a worktree chat into a NEW git worktree + chat (transcript attached); optional agent; leave model unset (Auto) unless you have a reason. Not for orchestration chats.",
5271
5320
  "- fork_chat \u2014 fork a worktree chat (same worktree tab) OR a Global orchestration chat (new orchestration tab); optional agent; leave model unset (Auto) unless you have a reason. Remote coordinators: use this to continue another orchestration chat on a different agent after session limits.",
5272
5321
  "- send_to_thread \u2014 queue a prompt (start/continue a chat turn); pass force_stop: true to interrupt mid-turn / clear stale queued prompts before replacing with a new request",
5273
- "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply",
5322
+ "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply. wait_for_turn returns within ~45s even if the child is still working (MCP clients kill longer tool calls). If stillRunning is true, progress is a live snapshot of tools/thinking \u2014 call wait_for_turn again. Do not send \u201Care you stuck?\u201D or assume a hang while lastActivityAt is recent. On status error, lastError (and text) is the failure \u2014 switch agent, tell the user, or retry; do not treat empty text as success.",
5274
5323
  "- stop_thread \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
5275
5324
  "- archive_thread / restore_thread \u2014 archive (tears down worktree when last tab) or restore",
5276
5325
  "Setup / run:",
@@ -5278,8 +5327,8 @@ var init_coordinator_prompt = __esm({
5278
5327
  "- list_run_scripts / run_dev_script / stop_dev_script \u2014 start/stop named run scripts",
5279
5328
  "Inspect / review / PRs:",
5280
5329
  "- get_diff \u2014 compact diff summary",
5281
- '- request_review \u2014 open a Review chat tab on a worktree thread (attaches .sideboard/review.md when present, else local guidelines; sends "Review changes in this workspace."); then wait_for_turn / get_turn_result on the returned id',
5282
- "- ask_git \u2014 commit & push, open a draft PR, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it queues the worktree agent \u2014 then wait_for_turn. Prefer this over paraphrasing.",
5330
+ '- request_review \u2014 open a Review chat tab on a worktree thread (attaches .sideboard/review.md when present, else local guidelines; sends "Review changes in this workspace."); then wait_for_turn (loop while stillRunning) / get_turn_result on the returned id',
5331
+ "- ask_git \u2014 commit & push, open a draft PR, resolve conflicts, or merge. When the worktree is clean, Sideboard pushes / opens the PR itself. When dirty, it queues the worktree agent \u2014 then wait_for_turn (loop while stillRunning). Prefer this over paraphrasing.",
5283
5332
  '- Merge (`ask_git` action=merge / send_to_thread "Merge PR.") only when the user explicitly asked to merge that PR. Do not merge because the work looks done, CI is green, or a typical flow includes it.',
5284
5333
  '- Or send_to_thread with those exact phrases: "Commit and push.", "Commit, push, and open a draft PR.", "Merge the remote branch (main) into your branch and resolve conflicts. Then, commit and push your changes.", "Merge PR." (draft PRs: `gh pr create --draft -R <origin-owner/name>` using the workspace `github:` slug \u2014 never upstream). Never run git/gh from this orchestration cwd, and never merge the PR yourself.',
5285
5334
  "Process guides:",
@@ -6133,6 +6182,13 @@ function looksLikeMinifiedJsDump(line) {
6133
6182
  function looksLikeNestedElectronCrash(line) {
6134
6183
  return /HasCustomHostObject|ElectronInitializeICUandStartNode/i.test(line);
6135
6184
  }
6185
+ function looksLikeHomebrewLibuvCrash(line) {
6186
+ const uvRun = /uv_run/i.test(line);
6187
+ const spin = /SpinEventLoopInternal/i.test(line);
6188
+ const homebrewUv = /Cellar\/libuv|libuv\.\d\.dylib/i.test(line);
6189
+ const homebrewNode = /Cellar\/node\//i.test(line);
6190
+ return uvRun && (homebrewUv || spin || homebrewNode) || spin && (homebrewUv || homebrewNode);
6191
+ }
6136
6192
  function clipStderr(text3, maxChars) {
6137
6193
  const trimmed = text3.trim();
6138
6194
  if (trimmed.length <= maxChars) return trimmed;
@@ -6143,6 +6199,7 @@ function summarizeTurnStderr(tail, maxChars = 500) {
6143
6199
  const cursorStartup = [...tail].reverse().find((line) => /cursor startup failed:/i.test(line));
6144
6200
  if (cursorStartup) return clipStderr(cursorStartup, maxChars);
6145
6201
  if (tail.some(looksLikeNestedElectronCrash)) return NESTED_ELECTRON_SUMMARY;
6202
+ if (tail.some(looksLikeHomebrewLibuvCrash)) return HOMEBREW_LIBUV_SUMMARY;
6146
6203
  if (tail.some(
6147
6204
  (line) => /\[resource_exhausted\]|resource_exhausted/i.test(line) || /findFilesWithRipgrep/.test(line)
6148
6205
  )) {
@@ -6165,6 +6222,20 @@ function looksLikeInvalidAgentSession(text3) {
6165
6222
  if (!lower) return false;
6166
6223
  return /session not found/.test(lower) || /no conversation found/.test(lower) || /conversation .+ not found/.test(lower) || /thread .+ not found/.test(lower) || /unknown session/.test(lower) || /invalid session/.test(lower) || /session .+ (missing|expired|deleted|gone)/.test(lower) || /cannot resume/.test(lower) || /failed to (load|resume|open) session/.test(lower) || /corrupt local agent checkpoint/.test(lower) || /missing root blob/.test(lower) || /\bagent\b.{0,120}\bnot found\b/.test(lower);
6167
6224
  }
6225
+ function looksLikeRetryableRunnerCrash(text3) {
6226
+ if (looksLikeAgentFailureMessage(text3)) return false;
6227
+ if (looksLikeInvalidAgentSession(text3)) return false;
6228
+ const lower = text3.trim().toLowerCase();
6229
+ if (/cannot find (?:package|module)|err_module_not_found/.test(lower)) return false;
6230
+ if (!lower) return true;
6231
+ return /uv_run|spineventloopinternal|libuv|homebrew node \+ shared libuv|hascustomhostobject|electroninitializeicuandstartnode|nested chromium|truncated crash dump|sig(?:segv|abrt|ill)|segmentation fault|illegal instruction|fatal error/.test(
6232
+ lower
6233
+ );
6234
+ }
6235
+ function shouldRetryFailedAgentTurn(detail, opts) {
6236
+ if (looksLikeInvalidAgentSession(detail) && opts.hasSession) return true;
6237
+ return looksLikeRetryableRunnerCrash(detail);
6238
+ }
6168
6239
  function looksLikeAgentFailureMessage(text3) {
6169
6240
  const lower = text3.trim().toLowerCase();
6170
6241
  if (!lower) return false;
@@ -6209,11 +6280,22 @@ function humanizeAgentFailDetail(detail) {
6209
6280
  if (/hascustomhostobject|electroninitializeicuandstartnode|nested chromium/i.test(lower)) {
6210
6281
  return `${raw} \u2014 retry the turn; if it keeps failing, pick another agent.`;
6211
6282
  }
6283
+ if (/homebrew node \+ shared libuv|uv_run|spineventloopinternal/i.test(lower)) {
6284
+ return /brew install node@22/i.test(raw) ? raw : `${raw} \u2014 install Node 22 LTS (\`brew install node@22\`) and retry.`;
6285
+ }
6212
6286
  if (/corrupt local agent checkpoint|missing root blob|truncated crash dump/.test(lower)) {
6213
6287
  return `${raw} \u2014 retry the turn (Sideboard will start a fresh Cursor session).`;
6214
6288
  }
6215
6289
  return raw;
6216
6290
  }
6291
+ function turnFailChatText(opts) {
6292
+ const chat = opts.assistantText.trim();
6293
+ if (chat) return chat;
6294
+ if (opts.exitCode === 0) return "";
6295
+ const detail = opts.detail.trim();
6296
+ if (detail) return humanizeAgentFailDetail(detail);
6297
+ return formatTurnExitError(opts.exitCode ?? 1, "");
6298
+ }
6217
6299
  function formatTurnExitError(exitCode, stderrSummary) {
6218
6300
  const code = exitCode ?? 1;
6219
6301
  const raw = stderrSummary.trim();
@@ -6227,12 +6309,13 @@ function formatTurnExitError(exitCode, stderrSummary) {
6227
6309
  if (looksLikeAgentFailureMessage(raw)) return detail;
6228
6310
  return `exit ${code}: ${detail}`;
6229
6311
  }
6230
- var NODE_VERSION_FOOTER, NESTED_ELECTRON_SUMMARY, MINIFIED_DUMP_SUMMARY;
6312
+ var NODE_VERSION_FOOTER, NESTED_ELECTRON_SUMMARY, HOMEBREW_LIBUV_SUMMARY, MINIFIED_DUMP_SUMMARY;
6231
6313
  var init_error_detail = __esm({
6232
6314
  "src/agents/error-detail.ts"() {
6233
6315
  "use strict";
6234
6316
  NODE_VERSION_FOOTER = /^Node\.js v\d+/i;
6235
6317
  NESTED_ELECTRON_SUMMARY = "Cursor local agent crashed at Electron startup (nested Chromium / HasCustomHostObject)";
6318
+ HOMEBREW_LIBUV_SUMMARY = "Cursor runner crashed in Node (Homebrew Node + shared libuv). Install Node 22 LTS (`brew install node@22`) and retry.";
6236
6319
  MINIFIED_DUMP_SUMMARY = "Cursor local agent crashed during startup (truncated crash dump)";
6237
6320
  }
6238
6321
  });
@@ -6673,6 +6756,55 @@ var init_profile = __esm({
6673
6756
  }
6674
6757
  });
6675
6758
 
6759
+ // src/agents/packaged-runtime.ts
6760
+ function electronResourcesPath() {
6761
+ const resources = process.resourcesPath;
6762
+ if (typeof resources !== "string" || !resources) return null;
6763
+ return resources;
6764
+ }
6765
+ function packagedCursorRuntimeDir() {
6766
+ const resources = electronResourcesPath();
6767
+ if (!resources) return null;
6768
+ const dir = (0, import_node_path19.join)(resources, "cursor-runtime");
6769
+ if (!(0, import_node_fs19.existsSync)((0, import_node_path19.join)(dir, "core-dist", "agents", "cursor-runner.js"))) return null;
6770
+ return dir;
6771
+ }
6772
+ function packagedCursorRunnerPath() {
6773
+ const dir = packagedCursorRuntimeDir();
6774
+ return dir ? (0, import_node_path19.join)(dir, "core-dist", "agents", "cursor-runner.js") : null;
6775
+ }
6776
+ function packagedMcpDir() {
6777
+ const resources = electronResourcesPath();
6778
+ if (!resources) return null;
6779
+ const dir = (0, import_node_path19.join)(resources, "sideboard-mcp");
6780
+ if (!(0, import_node_fs19.existsSync)((0, import_node_path19.join)(dir, "core-dist", "mcp", "run-stdio.js"))) return null;
6781
+ return dir;
6782
+ }
6783
+ function packagedMcpStdioPath() {
6784
+ const dir = packagedMcpDir();
6785
+ return dir ? (0, import_node_path19.join)(dir, "core-dist", "mcp", "run-stdio.js") : null;
6786
+ }
6787
+ function packagedBundledNodePath() {
6788
+ const resources = electronResourcesPath();
6789
+ if (!resources) return null;
6790
+ const bin = (0, import_node_path19.join)(resources, "node", "bin", "node");
6791
+ if (!(0, import_node_fs19.existsSync)(bin)) return null;
6792
+ return bin;
6793
+ }
6794
+ function packagedCursorRipgrepCandidate(platformPkg, binName) {
6795
+ const dir = packagedCursorRuntimeDir();
6796
+ if (!dir) return null;
6797
+ return (0, import_node_path19.join)(dir, "node_modules", platformPkg, "bin", binName);
6798
+ }
6799
+ var import_node_fs19, import_node_path19;
6800
+ var init_packaged_runtime = __esm({
6801
+ "src/agents/packaged-runtime.ts"() {
6802
+ "use strict";
6803
+ import_node_fs19 = require("fs");
6804
+ import_node_path19 = require("path");
6805
+ }
6806
+ });
6807
+
6676
6808
  // src/agents/node-launch.ts
6677
6809
  function isAsarPath(filePath) {
6678
6810
  if (/\.asar\.unpacked([/\\]|$)/.test(filePath)) return false;
@@ -6682,24 +6814,129 @@ function unpackedAsarPath(filePath) {
6682
6814
  if (!isAsarPath(filePath)) return null;
6683
6815
  const unpacked = filePath.replace(/\.asar(?=[/\\])/, ".asar.unpacked");
6684
6816
  if (unpacked === filePath) return null;
6685
- return (0, import_node_fs19.existsSync)(unpacked) ? unpacked : null;
6817
+ return (0, import_node_fs20.existsSync)(unpacked) ? unpacked : null;
6686
6818
  }
6687
6819
  function nodeReadableScriptPath(scriptPath) {
6688
6820
  return unpackedAsarPath(scriptPath) ?? scriptPath;
6689
6821
  }
6690
- async function findSystemNode() {
6691
- const whichNode = await run("which", ["node"], { reject: false });
6692
- const fromWhich = whichNode.exitCode === 0 && whichNode.stdout.trim() ? whichNode.stdout.trim() : "";
6693
- if (fromWhich && !isElectronLikeCommand(fromWhich)) return fromWhich;
6694
- const fallbacks = [
6695
- ...WELL_KNOWN_NODE_BINS,
6696
- (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".local/share/fnm/aliases/default/bin/node"),
6697
- (0, import_node_path19.join)((0, import_node_os7.homedir)(), ".nvm/current/bin/node")
6822
+ function parseNodeMajor(version) {
6823
+ const match = /^v?(\d+)/.exec(version.trim());
6824
+ if (!match) return null;
6825
+ const major = Number(match[1]);
6826
+ return Number.isInteger(major) ? major : null;
6827
+ }
6828
+ function scoreNodeForAgentRuntime(candidate) {
6829
+ const major = parseNodeMajor(candidate.version);
6830
+ if (major == null) return Number.NEGATIVE_INFINITY;
6831
+ if (major < 20) return major - 100;
6832
+ const posix = candidate.path.replace(/\\/g, "/");
6833
+ let score = 0;
6834
+ if (major % 2 === 0) {
6835
+ score += 1e3 + major * 10;
6836
+ } else {
6837
+ score += major;
6838
+ }
6839
+ if (/\/Cellar\/node\/\d/.test(posix) && !/\/Cellar\/node@\d+/.test(posix)) {
6840
+ score -= 50;
6841
+ }
6842
+ return score;
6843
+ }
6844
+ function pickPreferredNode(candidates) {
6845
+ let best = null;
6846
+ let bestScore = Number.NEGATIVE_INFINITY;
6847
+ for (const candidate of candidates) {
6848
+ const score = scoreNodeForAgentRuntime(candidate);
6849
+ if (score > bestScore) {
6850
+ bestScore = score;
6851
+ best = candidate;
6852
+ }
6853
+ }
6854
+ return best;
6855
+ }
6856
+ function versionDirNodeBins(root, toBin) {
6857
+ if (!(0, import_node_fs20.existsSync)(root)) return [];
6858
+ try {
6859
+ return (0, import_node_fs20.readdirSync)(root).map(toBin);
6860
+ } catch {
6861
+ return [];
6862
+ }
6863
+ }
6864
+ function defaultNodeBinCandidates(home = (0, import_node_os7.homedir)()) {
6865
+ const kegs = ["/opt/homebrew", "/usr/local"].flatMap(
6866
+ (prefix) => PREFERRED_LTS_MAJORS.map((major) => (0, import_node_path20.join)(prefix, "opt", `node@${major}`, "bin", "node"))
6867
+ );
6868
+ return [
6869
+ ...kegs,
6870
+ "/opt/homebrew/bin/node",
6871
+ "/usr/local/bin/node",
6872
+ (0, import_node_path20.join)(home, ".local/share/fnm/aliases/default/bin/node"),
6873
+ (0, import_node_path20.join)(home, ".nvm/current/bin/node"),
6874
+ (0, import_node_path20.join)(home, ".volta/bin/node"),
6875
+ (0, import_node_path20.join)(home, ".asdf/shims/node"),
6876
+ (0, import_node_path20.join)(home, ".local/share/mise/shims/node"),
6877
+ ...versionDirNodeBins(
6878
+ (0, import_node_path20.join)(home, ".nvm", "versions", "node"),
6879
+ (name) => (0, import_node_path20.join)(home, ".nvm", "versions", "node", name, "bin", "node")
6880
+ ),
6881
+ ...versionDirNodeBins(
6882
+ (0, import_node_path20.join)(home, ".local/share/fnm", "node-versions"),
6883
+ (name) => (0, import_node_path20.join)(home, ".local/share/fnm", "node-versions", name, "installation", "bin", "node")
6884
+ ),
6885
+ ...versionDirNodeBins(
6886
+ (0, import_node_path20.join)(home, ".volta", "tools", "image", "node"),
6887
+ (name) => (0, import_node_path20.join)(home, ".volta", "tools", "image", "node", name, "bin", "node")
6888
+ )
6698
6889
  ];
6699
- for (const bin of fallbacks) {
6700
- if ((0, import_node_fs19.existsSync)(bin) && !isElectronLikeCommand(bin)) return bin;
6890
+ }
6891
+ function uniqueExistingNodeBins(paths) {
6892
+ const seen = /* @__PURE__ */ new Set();
6893
+ const out = [];
6894
+ for (const raw of paths) {
6895
+ const p = raw.trim();
6896
+ if (!p || !(0, import_node_fs20.existsSync)(p) || isElectronLikeCommand(p)) continue;
6897
+ let key = p;
6898
+ try {
6899
+ key = (0, import_node_fs20.realpathSync)(p);
6900
+ } catch {
6901
+ continue;
6902
+ }
6903
+ if (seen.has(key)) continue;
6904
+ seen.add(key);
6905
+ out.push(p);
6701
6906
  }
6702
- return null;
6907
+ return out;
6908
+ }
6909
+ async function whichAllNode() {
6910
+ if (process.platform === "win32") {
6911
+ const located = await run("where", ["node"], { reject: false, timeoutMs: 5e3 });
6912
+ if (located.exitCode !== 0) return [];
6913
+ return located.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
6914
+ }
6915
+ const all = await run("which", ["-a", "node"], { reject: false, timeoutMs: 5e3 });
6916
+ if (all.exitCode === 0 && all.stdout.trim()) {
6917
+ return all.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
6918
+ }
6919
+ const single = await run("which", ["node"], { reject: false, timeoutMs: 5e3 });
6920
+ if (single.exitCode !== 0 || !single.stdout.trim()) return [];
6921
+ return [single.stdout.trim()];
6922
+ }
6923
+ async function probeNodeVersion(bin) {
6924
+ const probed = await run(bin, ["-v"], { reject: false, timeoutMs: 4e3 });
6925
+ if (probed.exitCode !== 0) return null;
6926
+ const version = probed.stdout.trim().split(/\r?\n/).find(Boolean);
6927
+ return version || null;
6928
+ }
6929
+ async function findSystemNode() {
6930
+ const fromPath = await whichAllNode();
6931
+ const bins = uniqueExistingNodeBins([...fromPath, ...defaultNodeBinCandidates()]);
6932
+ if (bins.length === 0) return null;
6933
+ const probed = await Promise.all(
6934
+ bins.map(async (path2) => {
6935
+ const version = await probeNodeVersion(path2);
6936
+ return version ? { path: path2, version } : null;
6937
+ })
6938
+ );
6939
+ return pickPreferredNode(probed.filter((c) => c != null));
6703
6940
  }
6704
6941
  function applyNodeLaunch(launch, args) {
6705
6942
  const readableArgs = args.map(nodeReadableScriptPath);
@@ -6717,9 +6954,13 @@ function applyNodeLaunch(launch, args) {
6717
6954
  async function resolveNodeLaunch(scriptPath) {
6718
6955
  const script = nodeReadableScriptPath(scriptPath);
6719
6956
  if (!isAsarPath(script)) {
6720
- const nodeBin = await findSystemNode();
6721
- if (nodeBin) {
6722
- return { file: nodeBin, env: {} };
6957
+ const bundled = packagedBundledNodePath();
6958
+ if (bundled && !isElectronLikeCommand(bundled)) {
6959
+ return { file: bundled, env: {} };
6960
+ }
6961
+ const node = await findSystemNode();
6962
+ if (node) {
6963
+ return { file: node.path, env: {}, nodeVersion: node.version };
6723
6964
  }
6724
6965
  }
6725
6966
  return {
@@ -6727,61 +6968,17 @@ async function resolveNodeLaunch(scriptPath) {
6727
6968
  env: { ELECTRON_RUN_AS_NODE: "1" }
6728
6969
  };
6729
6970
  }
6730
- var import_node_fs19, import_node_os7, import_node_path19, WELL_KNOWN_NODE_BINS;
6971
+ var import_node_fs20, import_node_os7, import_node_path20, PREFERRED_LTS_MAJORS;
6731
6972
  var init_node_launch = __esm({
6732
6973
  "src/agents/node-launch.ts"() {
6733
6974
  "use strict";
6734
- import_node_fs19 = require("fs");
6975
+ import_node_fs20 = require("fs");
6735
6976
  import_node_os7 = require("os");
6736
- import_node_path19 = require("path");
6977
+ import_node_path20 = require("path");
6737
6978
  init_nested_electron_env();
6738
6979
  init_run();
6739
- WELL_KNOWN_NODE_BINS = [
6740
- "/opt/homebrew/bin/node",
6741
- "/usr/local/bin/node"
6742
- ];
6743
- }
6744
- });
6745
-
6746
- // src/agents/packaged-runtime.ts
6747
- function electronResourcesPath() {
6748
- const resources = process.resourcesPath;
6749
- if (typeof resources !== "string" || !resources) return null;
6750
- return resources;
6751
- }
6752
- function packagedCursorRuntimeDir() {
6753
- const resources = electronResourcesPath();
6754
- if (!resources) return null;
6755
- const dir = (0, import_node_path20.join)(resources, "cursor-runtime");
6756
- if (!(0, import_node_fs20.existsSync)((0, import_node_path20.join)(dir, "core-dist", "agents", "cursor-runner.js"))) return null;
6757
- return dir;
6758
- }
6759
- function packagedCursorRunnerPath() {
6760
- const dir = packagedCursorRuntimeDir();
6761
- return dir ? (0, import_node_path20.join)(dir, "core-dist", "agents", "cursor-runner.js") : null;
6762
- }
6763
- function packagedMcpDir() {
6764
- const resources = electronResourcesPath();
6765
- if (!resources) return null;
6766
- const dir = (0, import_node_path20.join)(resources, "sideboard-mcp");
6767
- if (!(0, import_node_fs20.existsSync)((0, import_node_path20.join)(dir, "core-dist", "mcp", "run-stdio.js"))) return null;
6768
- return dir;
6769
- }
6770
- function packagedMcpStdioPath() {
6771
- const dir = packagedMcpDir();
6772
- return dir ? (0, import_node_path20.join)(dir, "core-dist", "mcp", "run-stdio.js") : null;
6773
- }
6774
- function packagedCursorRipgrepCandidate(platformPkg, binName) {
6775
- const dir = packagedCursorRuntimeDir();
6776
- if (!dir) return null;
6777
- return (0, import_node_path20.join)(dir, "node_modules", platformPkg, "bin", binName);
6778
- }
6779
- var import_node_fs20, import_node_path20;
6780
- var init_packaged_runtime = __esm({
6781
- "src/agents/packaged-runtime.ts"() {
6782
- "use strict";
6783
- import_node_fs20 = require("fs");
6784
- import_node_path20 = require("path");
6980
+ init_packaged_runtime();
6981
+ PREFERRED_LTS_MAJORS = [24, 22, 20];
6785
6982
  }
6786
6983
  });
6787
6984
 
@@ -9160,17 +9357,17 @@ __export(plan_file_exports, {
9160
9357
  });
9161
9358
  function ensureAttachmentsGitignore2(worktreePath) {
9162
9359
  const gitignoreAbs = (0, import_node_path37.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
9163
- if ((0, import_node_fs40.existsSync)(gitignoreAbs)) return;
9164
- (0, import_node_fs40.mkdirSync)((0, import_node_path37.dirname)(gitignoreAbs), { recursive: true });
9165
- (0, import_node_fs40.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
9360
+ if ((0, import_node_fs41.existsSync)(gitignoreAbs)) return;
9361
+ (0, import_node_fs41.mkdirSync)((0, import_node_path37.dirname)(gitignoreAbs), { recursive: true });
9362
+ (0, import_node_fs41.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
9166
9363
  }
9167
9364
  function planFileAbs(worktreePath) {
9168
9365
  return (0, import_node_path37.join)(worktreePath, PLAN_FILE_REL);
9169
9366
  }
9170
9367
  function readTextIfPresent2(abs) {
9171
- if (!(0, import_node_fs40.existsSync)(abs)) return null;
9368
+ if (!(0, import_node_fs41.existsSync)(abs)) return null;
9172
9369
  try {
9173
- const content = (0, import_node_fs40.readFileSync)(abs, "utf8");
9370
+ const content = (0, import_node_fs41.readFileSync)(abs, "utf8");
9174
9371
  return content.trim() ? content : null;
9175
9372
  } catch {
9176
9373
  return null;
@@ -9182,16 +9379,16 @@ function readPlanFile(worktreePath) {
9182
9379
  function writePlanFile(worktreePath, content) {
9183
9380
  ensureAttachmentsGitignore2(worktreePath);
9184
9381
  const abs = planFileAbs(worktreePath);
9185
- (0, import_node_fs40.mkdirSync)((0, import_node_path37.dirname)(abs), { recursive: true });
9382
+ (0, import_node_fs41.mkdirSync)((0, import_node_path37.dirname)(abs), { recursive: true });
9186
9383
  const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
9187
- (0, import_node_fs40.writeFileSync)(abs, body, "utf8");
9384
+ (0, import_node_fs41.writeFileSync)(abs, body, "utf8");
9188
9385
  return PLAN_FILE_REL;
9189
9386
  }
9190
- var import_node_fs40, import_node_path37;
9387
+ var import_node_fs41, import_node_path37;
9191
9388
  var init_plan_file = __esm({
9192
9389
  "src/plan/plan-file.ts"() {
9193
9390
  "use strict";
9194
- import_node_fs40 = require("fs");
9391
+ import_node_fs41 = require("fs");
9195
9392
  import_node_path37 = require("path");
9196
9393
  init_workspace_scratch();
9197
9394
  init_plan_present();
@@ -9208,9 +9405,9 @@ function recoverFinishedCursorRun(opts) {
9208
9405
  const agentId = opts.agentId.trim();
9209
9406
  if (!agentId) return null;
9210
9407
  const runsPath = (0, import_node_path38.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
9211
- if (!(0, import_node_fs41.existsSync)(runsPath)) return null;
9408
+ if (!(0, import_node_fs42.existsSync)(runsPath)) return null;
9212
9409
  try {
9213
- const lines = (0, import_node_fs41.readFileSync)(runsPath, "utf8").split("\n");
9410
+ const lines = (0, import_node_fs42.readFileSync)(runsPath, "utf8").split("\n");
9214
9411
  let best = null;
9215
9412
  for (const line of lines) {
9216
9413
  const trimmed = line.trim();
@@ -9236,11 +9433,11 @@ function recoverFinishedCursorRun(opts) {
9236
9433
  return null;
9237
9434
  }
9238
9435
  }
9239
- var import_node_fs41, import_node_path38;
9436
+ var import_node_fs42, import_node_path38;
9240
9437
  var init_cursor_recover = __esm({
9241
9438
  "src/agents/cursor-recover.ts"() {
9242
9439
  "use strict";
9243
- import_node_fs41 = require("fs");
9440
+ import_node_fs42 = require("fs");
9244
9441
  import_node_path38 = require("path");
9245
9442
  init_paths();
9246
9443
  }
@@ -9329,6 +9526,8 @@ __export(index_exports, {
9329
9526
  SLACK_LISTEN_TIMEOUT_REPLY: () => SLACK_LISTEN_TIMEOUT_REPLY,
9330
9527
  SLACK_OAUTH_CANCELLED: () => SLACK_OAUTH_CANCELLED,
9331
9528
  SLACK_OAUTH_REDIRECT: () => SLACK_OAUTH_REDIRECT,
9529
+ SLACK_PROGRESS_DELAY_MS: () => SLACK_PROGRESS_DELAY_MS,
9530
+ SLACK_PROGRESS_EDIT_MS: () => SLACK_PROGRESS_EDIT_MS,
9332
9531
  SLACK_REPLY_FORMATTING: () => SLACK_REPLY_FORMATTING,
9333
9532
  SLACK_SEEN_REACTION: () => SLACK_SEEN_REACTION,
9334
9533
  SlackOAuthCancelledError: () => SlackOAuthCancelledError,
@@ -9486,6 +9685,7 @@ __export(index_exports, {
9486
9685
  formatSlackInboundPrompt: () => formatSlackInboundPrompt,
9487
9686
  formatSlackRepliesForTurn: () => formatSlackRepliesForTurn,
9488
9687
  formatSlackSignedReply: () => formatSlackSignedReply,
9688
+ formatSlackWorkingText: () => formatSlackWorkingText,
9489
9689
  formatTranscriptMarkdown: () => formatTranscriptMarkdown,
9490
9690
  formatUiReminder: () => formatUiReminder,
9491
9691
  formatWorkspaceInventory: () => formatWorkspaceInventory,
@@ -9781,6 +9981,7 @@ __export(index_exports, {
9781
9981
  thisProcessShouldDrainAgentQueues: () => thisProcessShouldDrainAgentQueues,
9782
9982
  threadDisplayLabel: () => threadDisplayLabel,
9783
9983
  threadFilePath: () => threadFilePath,
9984
+ threadLivePath: () => threadLivePath,
9784
9985
  threadLockPath: () => threadLockPath,
9785
9986
  threadRequestsBrightsyMcp: () => threadRequestsBrightsyMcp,
9786
9987
  threadsDir: () => threadsDir,
@@ -9804,6 +10005,7 @@ __export(index_exports, {
9804
10005
  userClaudeMcpConfigPath: () => userClaudeMcpConfigPath,
9805
10006
  userCursorMcpConfigPath: () => userCursorMcpConfigPath,
9806
10007
  validateLinearApiKey: () => validateLinearApiKey,
10008
+ waitForPidExit: () => waitForPidExit,
9807
10009
  warmGithubAgentAuth: () => warmGithubAgentAuth,
9808
10010
  withAgentInstructions: () => withAgentInstructions,
9809
10011
  withExportedPath: () => withExportedPath,
@@ -10874,7 +11076,7 @@ function createAgentStreamCoalescer(emit, opts) {
10874
11076
  const intervalMs = opts?.intervalMs ?? 32;
10875
11077
  let pending = null;
10876
11078
  let timer = null;
10877
- const flush = () => {
11079
+ const flush2 = () => {
10878
11080
  if (timer) {
10879
11081
  clearTimeout(timer);
10880
11082
  timer = null;
@@ -10886,13 +11088,13 @@ function createAgentStreamCoalescer(emit, opts) {
10886
11088
  };
10887
11089
  const schedule = () => {
10888
11090
  if (timer) return;
10889
- timer = setTimeout(flush, intervalMs);
11091
+ timer = setTimeout(flush2, intervalMs);
10890
11092
  timer.unref?.();
10891
11093
  };
10892
11094
  return {
10893
11095
  push(event) {
10894
11096
  if (!isTextEvent(event)) {
10895
- flush();
11097
+ flush2();
10896
11098
  emit(event);
10897
11099
  return;
10898
11100
  }
@@ -10900,12 +11102,12 @@ function createAgentStreamCoalescer(emit, opts) {
10900
11102
  if (pending && pending.type === event.type) {
10901
11103
  pending.data += event.data;
10902
11104
  } else {
10903
- flush();
11105
+ flush2();
10904
11106
  pending = { type: event.type, data: event.data };
10905
11107
  }
10906
11108
  schedule();
10907
11109
  },
10908
- flush
11110
+ flush: flush2
10909
11111
  };
10910
11112
  }
10911
11113
 
@@ -12818,11 +13020,11 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
12818
13020
  }
12819
13021
  return out;
12820
13022
  }
12821
- function stageBuffersAsAttachments(worktreePath, buffers) {
12822
- if (buffers.length === 0) return [];
13023
+ function stageBuffersAsAttachments(worktreePath, buffers2) {
13024
+ if (buffers2.length === 0) return [];
12823
13025
  const dir = ensureAttachmentsDir(worktreePath);
12824
13026
  const out = [];
12825
- for (const item of buffers) {
13027
+ for (const item of buffers2) {
12826
13028
  const originalName = (item.name || "file").replace(/[/\\]/g, "_") || "file";
12827
13029
  try {
12828
13030
  const buf = Buffer.from(item.dataBase64, "base64");
@@ -12842,8 +13044,8 @@ function stageBuffersAsAttachments(worktreePath, buffers) {
12842
13044
  }
12843
13045
  return out;
12844
13046
  }
12845
- function attachmentsFromBuffers(buffers) {
12846
- return buffers.map((item) => {
13047
+ function attachmentsFromBuffers(buffers2) {
13048
+ return buffers2.map((item) => {
12847
13049
  const name = (item.name || "file").replace(/[/\\]/g, "_") || "file";
12848
13050
  try {
12849
13051
  const buf = Buffer.from(item.dataBase64, "base64");
@@ -13500,8 +13702,11 @@ function worktreeBindingFrom(from) {
13500
13702
  };
13501
13703
  }
13502
13704
  function threadsSharingWorktree(worktreePath) {
13705
+ if (!worktreePath) return [];
13503
13706
  const key = normalizeWorktreePath(worktreePath);
13504
- return listThreads({ includeArchived: true }).filter((t) => sameWorktreePath(t.worktreePath, key) && t.status !== "archived").sort((a, b) => a.createdAt.localeCompare(b.createdAt));
13707
+ return listThreads({ includeArchived: true }).filter(
13708
+ (t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
13709
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
13505
13710
  }
13506
13711
  function formatTranscriptMarkdown(title, messages) {
13507
13712
  const body = formatMessagesAsTranscript(messages, { tools: "full" });
@@ -14780,7 +14985,7 @@ async function refreshSlackReplyBadges(opts) {
14780
14985
  }
14781
14986
 
14782
14987
  // src/orchestrator/orchestrator.ts
14783
- var import_node_fs42 = require("fs");
14988
+ var import_node_fs43 = require("fs");
14784
14989
  init_error_detail();
14785
14990
  init_agents();
14786
14991
  init_worktree();
@@ -15040,10 +15245,131 @@ async function cloneRepoIntoSideboard(opts) {
15040
15245
  // src/orchestrator/orchestrator.ts
15041
15246
  init_thread_store();
15042
15247
  init_orchestrator_capable();
15248
+ init_enqueue_by_key();
15249
+ init_run();
15250
+ init_repo_git_lock();
15251
+
15252
+ // src/store/turn-live.ts
15253
+ var import_node_fs39 = require("fs");
15254
+ init_paths();
15255
+ var buffers = /* @__PURE__ */ new Map();
15256
+ var FLUSH_MS = 800;
15257
+ var MAX_PARTS = 80;
15258
+ function trimParts(parts) {
15259
+ if (parts.length <= MAX_PARTS) return parts;
15260
+ return parts.slice(-Math.floor(MAX_PARTS / 2));
15261
+ }
15262
+ function lastText(parts, type) {
15263
+ for (let i = parts.length - 1; i >= 0; i--) {
15264
+ const p = parts[i];
15265
+ if (p.type === type && p.text.trim()) return p.text.trim();
15266
+ }
15267
+ return "";
15268
+ }
15269
+ function summarizeTurnLive(parts) {
15270
+ const tools = parts.filter(
15271
+ (p) => p.type === "tool"
15272
+ );
15273
+ const lastTool = tools[tools.length - 1];
15274
+ const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
15275
+ const running = lastTool?.status === "running";
15276
+ const thinking = lastText(parts, "thinking");
15277
+ const text3 = lastText(parts, "text");
15278
+ const excerptRaw = thinking || text3;
15279
+ const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
15280
+ let summary = "Working\u2026";
15281
+ if (lastToolLabel) {
15282
+ const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
15283
+ summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
15284
+ } else if (thinking) {
15285
+ summary = "Thinking\u2026";
15286
+ } else if (text3) {
15287
+ summary = "Writing reply\u2026";
15288
+ }
15289
+ return {
15290
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
15291
+ summary,
15292
+ lastTool: lastToolLabel,
15293
+ toolCount: tools.length,
15294
+ excerpt
15295
+ };
15296
+ }
15297
+ function flush(threadId) {
15298
+ const buf = buffers.get(threadId);
15299
+ if (!buf) return;
15300
+ if (buf.timer) {
15301
+ clearTimeout(buf.timer);
15302
+ buf.timer = null;
15303
+ }
15304
+ writeTurnLive(threadId, summarizeTurnLive(buf.parts));
15305
+ }
15306
+ function scheduleFlush(threadId) {
15307
+ const buf = buffers.get(threadId);
15308
+ if (!buf) return;
15309
+ if (buf.timer) return;
15310
+ buf.timer = setTimeout(() => {
15311
+ const live = buffers.get(threadId);
15312
+ if (live) live.timer = null;
15313
+ flush(threadId);
15314
+ }, FLUSH_MS);
15315
+ }
15316
+ function noteTurnLiveEvent(threadId, event) {
15317
+ if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
15318
+ return;
15319
+ }
15320
+ const prev = buffers.get(threadId) ?? { parts: [], timer: null };
15321
+ prev.parts = trimParts(applyAgentEvent(prev.parts, event));
15322
+ buffers.set(threadId, prev);
15323
+ if (event.type === "exit") {
15324
+ flush(threadId);
15325
+ const cur = buffers.get(threadId);
15326
+ if (cur?.timer) clearTimeout(cur.timer);
15327
+ buffers.delete(threadId);
15328
+ return;
15329
+ }
15330
+ const immediate = event.type === "tool_use" || event.type === "tool_result";
15331
+ if (immediate) flush(threadId);
15332
+ else scheduleFlush(threadId);
15333
+ }
15334
+ function writeTurnLive(threadId, progress) {
15335
+ const path2 = threadLivePath(threadId);
15336
+ const tmp = `${path2}.${process.pid}.tmp`;
15337
+ try {
15338
+ (0, import_node_fs39.writeFileSync)(tmp, JSON.stringify(progress), "utf8");
15339
+ (0, import_node_fs39.renameSync)(tmp, path2);
15340
+ } catch {
15341
+ try {
15342
+ (0, import_node_fs39.unlinkSync)(tmp);
15343
+ } catch {
15344
+ }
15345
+ }
15346
+ }
15347
+ function readTurnLive(threadId) {
15348
+ const path2 = threadLivePath(threadId);
15349
+ if (!(0, import_node_fs39.existsSync)(path2)) return null;
15350
+ try {
15351
+ const raw = JSON.parse((0, import_node_fs39.readFileSync)(path2, "utf8"));
15352
+ if (!raw || typeof raw.summary !== "string") return null;
15353
+ return raw;
15354
+ } catch {
15355
+ return null;
15356
+ }
15357
+ }
15358
+ function clearTurnLive(threadId) {
15359
+ const buf = buffers.get(threadId);
15360
+ if (buf?.timer) clearTimeout(buf.timer);
15361
+ buffers.delete(threadId);
15362
+ const path2 = threadLivePath(threadId);
15363
+ if (!(0, import_node_fs39.existsSync)(path2)) return;
15364
+ try {
15365
+ (0, import_node_fs39.unlinkSync)(path2);
15366
+ } catch {
15367
+ }
15368
+ }
15043
15369
 
15044
15370
  // src/review/request-review.ts
15045
15371
  var import_node_crypto8 = require("crypto");
15046
- var import_node_fs39 = require("fs");
15372
+ var import_node_fs40 = require("fs");
15047
15373
  var import_node_path36 = require("path");
15048
15374
  init_global_workspace();
15049
15375
  init_thread_store();
@@ -15192,9 +15518,9 @@ function shouldRefreshReviewRequestTemplate(content) {
15192
15518
  return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
15193
15519
  }
15194
15520
  function readTextIfPresent(abs) {
15195
- if (!(0, import_node_fs39.existsSync)(abs)) return null;
15521
+ if (!(0, import_node_fs40.existsSync)(abs)) return null;
15196
15522
  try {
15197
- const content = (0, import_node_fs39.readFileSync)(abs, "utf8");
15523
+ const content = (0, import_node_fs40.readFileSync)(abs, "utf8");
15198
15524
  return content.trim() ? content : null;
15199
15525
  } catch {
15200
15526
  return null;
@@ -15202,9 +15528,9 @@ function readTextIfPresent(abs) {
15202
15528
  }
15203
15529
  function ensureAttachmentsGitignore(worktreePath) {
15204
15530
  const gitignoreAbs = (0, import_node_path36.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
15205
- if ((0, import_node_fs39.existsSync)(gitignoreAbs)) return;
15206
- (0, import_node_fs39.mkdirSync)((0, import_node_path36.dirname)(gitignoreAbs), { recursive: true });
15207
- (0, import_node_fs39.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
15531
+ if ((0, import_node_fs40.existsSync)(gitignoreAbs)) return;
15532
+ (0, import_node_fs40.mkdirSync)((0, import_node_path36.dirname)(gitignoreAbs), { recursive: true });
15533
+ (0, import_node_fs40.writeFileSync)(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
15208
15534
  }
15209
15535
  function resolveReviewGuidelines(worktreePath) {
15210
15536
  const repoAbs = (0, import_node_path36.join)(worktreePath, REPO_REVIEW_PATH);
@@ -15238,8 +15564,8 @@ function resolveReviewGuidelines(worktreePath) {
15238
15564
  };
15239
15565
  }
15240
15566
  ensureAttachmentsGitignore(worktreePath);
15241
- (0, import_node_fs39.mkdirSync)((0, import_node_path36.dirname)(localAbs), { recursive: true });
15242
- (0, import_node_fs39.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
15567
+ (0, import_node_fs40.mkdirSync)((0, import_node_path36.dirname)(localAbs), { recursive: true });
15568
+ (0, import_node_fs40.writeFileSync)(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
15243
15569
  return {
15244
15570
  path: REVIEW_REQUEST_PATH,
15245
15571
  name: REVIEW_REQUEST_NAME,
@@ -15261,7 +15587,7 @@ function ensureReviewRequestFile(worktreePath) {
15261
15587
  const localAbs = (0, import_node_path36.join)(worktreePath, REVIEW_REQUEST_PATH);
15262
15588
  const localContent = readTextIfPresent(localAbs) ?? readTextIfPresent((0, import_node_path36.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
15263
15589
  if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
15264
- const path2 = (0, import_node_fs39.existsSync)(localAbs) ? REVIEW_REQUEST_PATH : LEGACY_REVIEW_REQUEST_PATH;
15590
+ const path2 = (0, import_node_fs40.existsSync)(localAbs) ? REVIEW_REQUEST_PATH : LEGACY_REVIEW_REQUEST_PATH;
15265
15591
  return {
15266
15592
  path: path2,
15267
15593
  name: REVIEW_REQUEST_NAME,
@@ -15269,8 +15595,8 @@ function ensureReviewRequestFile(worktreePath) {
15269
15595
  source: "local"
15270
15596
  };
15271
15597
  }
15272
- (0, import_node_fs39.mkdirSync)((0, import_node_path36.dirname)(repoAbs), { recursive: true });
15273
- (0, import_node_fs39.writeFileSync)(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
15598
+ (0, import_node_fs40.mkdirSync)((0, import_node_path36.dirname)(repoAbs), { recursive: true });
15599
+ (0, import_node_fs40.writeFileSync)(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
15274
15600
  return {
15275
15601
  path: REPO_REVIEW_PATH,
15276
15602
  name: REPO_REVIEW_NAME,
@@ -15479,6 +15805,15 @@ function isPidAlive(pid) {
15479
15805
  return false;
15480
15806
  }
15481
15807
  }
15808
+ async function waitForPidExit(pid, timeoutMs) {
15809
+ const start = Date.now();
15810
+ while (isPidAlive(pid)) {
15811
+ if (Date.now() - start >= timeoutMs) return false;
15812
+ await new Promise((r) => setTimeout(r, 80));
15813
+ }
15814
+ return true;
15815
+ }
15816
+ var STALE_AGENT_PID_WAIT_MS = 2500;
15482
15817
  function writeLiveStatus(threadId, status, lastError) {
15483
15818
  const latest = readThread(threadId);
15484
15819
  if (!latest || latest.status === "archived") return latest;
@@ -15556,7 +15891,7 @@ var Orchestrator = class {
15556
15891
  }
15557
15892
  continue;
15558
15893
  }
15559
- if (!(0, import_node_fs42.existsSync)(thread.worktreePath)) {
15894
+ if (!(0, import_node_fs43.existsSync)(thread.worktreePath)) {
15560
15895
  setStatus(thread.id, "broken", "Worktree missing on disk");
15561
15896
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
15562
15897
  continue;
@@ -15877,14 +16212,10 @@ var Orchestrator = class {
15877
16212
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
15878
16213
  const livePid = current.agentPid;
15879
16214
  const foreignLive = !inFlight && typeof livePid === "number" && livePid > 0 && isPidAlive(livePid);
15880
- if (inFlight) {
16215
+ if (inFlight || foreignLive) {
15881
16216
  this.stop(thread.id, { clearQueue: false, continueQueue: true });
15882
- } else {
15883
- if (foreignLive) {
15884
- this.stop(thread.id, { clearQueue: false, continueQueue: true });
15885
- }
15886
- void this.drainQueue(thread.id);
15887
16217
  }
16218
+ void this.drainQueue(thread.id);
15888
16219
  return this.requireThread(thread.id);
15889
16220
  }
15890
16221
  async drainQueue(threadId) {
@@ -15913,7 +16244,18 @@ var Orchestrator = class {
15913
16244
  }
15914
16245
  const livePid = thread.agentPid;
15915
16246
  if (typeof livePid === "number" && livePid > 0 && isPidAlive(livePid)) {
15916
- await new Promise((r) => setTimeout(r, 200));
16247
+ const exited = await waitForPidExit(livePid, STALE_AGENT_PID_WAIT_MS);
16248
+ if (!exited && isPidAlive(livePid)) {
16249
+ try {
16250
+ process.kill(livePid, "SIGKILL");
16251
+ } catch {
16252
+ }
16253
+ await waitForPidExit(livePid, 400);
16254
+ try {
16255
+ updateThread(threadId, { agentPid: null });
16256
+ } catch {
16257
+ }
16258
+ }
15917
16259
  continue;
15918
16260
  }
15919
16261
  const prompt = thread.queue[0];
@@ -16070,6 +16412,7 @@ var Orchestrator = class {
16070
16412
  { cachedPrefix, prompt: agentPrompt },
16071
16413
  (event) => {
16072
16414
  this.emit({ type: "turn_output", threadId, event });
16415
+ noteTurnLiveEvent(threadId, event);
16073
16416
  if (event.type === "session_id") {
16074
16417
  updateThread(threadId, { sessionId: event.data });
16075
16418
  }
@@ -16133,19 +16476,16 @@ var Orchestrator = class {
16133
16476
  }
16134
16477
  let lastStderr = summarizeTurnStderr(stderrTail);
16135
16478
  let detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
16136
- if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && looksLikeInvalidAgentSession(detail) && this.requireThread(threadId).sessionId && this.requireThread(threadId).agent !== "brightsy") {
16479
+ if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && this.requireThread(threadId).agent !== "brightsy" && shouldRetryFailedAgentTurn(detail, {
16480
+ hasSession: Boolean(this.requireThread(threadId).sessionId)
16481
+ })) {
16137
16482
  updateThread(threadId, { sessionId: null });
16138
- pushTurnStderr(
16139
- stderrTail,
16140
- "Agent session missing \u2014 starting a fresh session"
16141
- );
16483
+ const retryNote = looksLikeInvalidAgentSession(detail) ? "Agent session missing \u2014 starting a fresh session" : "Agent runner crashed \u2014 restarting Node once";
16484
+ pushTurnStderr(stderrTail, retryNote);
16142
16485
  this.emit({
16143
16486
  type: "turn_output",
16144
16487
  threadId,
16145
- event: {
16146
- type: "stderr",
16147
- data: "Agent session missing \u2014 starting a fresh session"
16148
- }
16488
+ event: { type: "stderr", data: retryNote }
16149
16489
  });
16150
16490
  const retryThread = this.requireThread(threadId);
16151
16491
  const prior = retryThread.messages.slice(0, -1);
@@ -16194,10 +16534,7 @@ var Orchestrator = class {
16194
16534
  lastStderr = summarizeTurnStderr(stderrTail);
16195
16535
  detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
16196
16536
  }
16197
- let chatText = assistantText;
16198
- if (exitCode !== 0 && !chatText && looksLikeAgentFailureMessage(detail)) {
16199
- chatText = humanizeAgentFailDetail(detail);
16200
- }
16537
+ let chatText = this.stoppedTurns.has(threadId) ? assistantText : turnFailChatText({ exitCode, assistantText, detail });
16201
16538
  if (chatText || parts.length > 0) {
16202
16539
  appendMessage(threadId, {
16203
16540
  role: "agent",
@@ -16280,6 +16617,7 @@ var Orchestrator = class {
16280
16617
  this.processes.delete(`${threadId}:agent`);
16281
16618
  this.stoppedTurns.delete(threadId);
16282
16619
  this.runningCount = Math.max(0, this.runningCount - 1);
16620
+ clearTurnLive(threadId);
16283
16621
  try {
16284
16622
  updateThread(threadId, { agentPid: null });
16285
16623
  } catch {
@@ -16553,51 +16891,56 @@ var Orchestrator = class {
16553
16891
  }
16554
16892
  return created;
16555
16893
  }
16556
- async waitForTurn(threadRef, timeoutMs = 6e5) {
16894
+ async waitForTurn(threadRef, timeoutMs = 6e5, opts) {
16557
16895
  const thread = this.requireThread(threadRef);
16896
+ if (!["running", "queued"].includes(thread.status)) {
16897
+ return thread;
16898
+ }
16558
16899
  const start = Date.now();
16559
16900
  return new Promise((resolve, reject) => {
16560
- if (!["running", "queued"].includes(thread.status)) {
16561
- resolve(thread);
16562
- return;
16563
- }
16901
+ let settled = false;
16902
+ let timer;
16903
+ const finish = (fn) => {
16904
+ if (settled) return;
16905
+ settled = true;
16906
+ if (timer) clearInterval(timer);
16907
+ off();
16908
+ fn();
16909
+ };
16564
16910
  const off = this.on((event) => {
16565
16911
  if (event.type === "turn_finished" && event.threadId === thread.id) {
16566
- off();
16567
16912
  const latest = readThread(thread.id);
16568
16913
  if (!latest) {
16569
- reject(new Error(`Thread not found: ${thread.id}`));
16914
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
16570
16915
  return;
16571
16916
  }
16572
- resolve(latest);
16917
+ finish(() => resolve(latest));
16573
16918
  }
16574
16919
  if (event.type === "error" && event.threadId === thread.id) {
16575
- off();
16576
16920
  const latest = readThread(thread.id);
16577
16921
  if (!latest) {
16578
- reject(new Error(`Thread not found: ${thread.id}`));
16922
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
16579
16923
  return;
16580
16924
  }
16581
- resolve(latest);
16925
+ finish(() => resolve(latest));
16582
16926
  }
16583
16927
  });
16584
- const timer = setInterval(() => {
16585
- if (Date.now() - start > timeoutMs) {
16586
- clearInterval(timer);
16587
- off();
16588
- reject(new Error("wait_for_turn timed out"));
16589
- }
16928
+ timer = setInterval(() => {
16590
16929
  const current = readThread(thread.id);
16591
16930
  if (!current) {
16592
- clearInterval(timer);
16593
- off();
16594
- reject(new Error(`Thread not found: ${thread.id}`));
16931
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
16595
16932
  return;
16596
16933
  }
16597
16934
  if (!["running", "queued"].includes(current.status)) {
16598
- clearInterval(timer);
16599
- off();
16600
- resolve(current);
16935
+ finish(() => resolve(current));
16936
+ return;
16937
+ }
16938
+ if (Date.now() - start > timeoutMs) {
16939
+ if (opts?.resolveIfStillRunning) {
16940
+ finish(() => resolve(current));
16941
+ } else {
16942
+ finish(() => reject(new Error("wait_for_turn timed out")));
16943
+ }
16601
16944
  }
16602
16945
  }, 200);
16603
16946
  });
@@ -16605,10 +16948,18 @@ var Orchestrator = class {
16605
16948
  getTurnResult(threadRef) {
16606
16949
  const thread = this.requireThread(threadRef);
16607
16950
  const lastAgent = [...thread.messages].reverse().find((m) => m.role === "agent");
16951
+ const lastError = thread.lastError ?? null;
16952
+ const text3 = (lastAgent?.text ?? "").trim() || (thread.status === "error" ? lastError ?? "" : "");
16953
+ const stillRunning = thread.status === "running" || thread.status === "queued";
16954
+ const live = stillRunning ? readTurnLive(thread.id) : null;
16608
16955
  return {
16609
- text: lastAgent?.text ?? "",
16956
+ text: text3,
16610
16957
  status: thread.status,
16611
- sessionId: thread.sessionId
16958
+ sessionId: thread.sessionId,
16959
+ lastError,
16960
+ stillRunning,
16961
+ progress: live?.summary ?? null,
16962
+ lastActivityAt: live?.updatedAt ?? null
16612
16963
  };
16613
16964
  }
16614
16965
  assertNotGlobal(thread, action) {
@@ -17034,7 +17385,19 @@ var Orchestrator = class {
17034
17385
  const thread = this.requireThread(threadRef);
17035
17386
  return threadsSharingWorktree(thread.worktreePath);
17036
17387
  }
17388
+ /**
17389
+ * Last-tab teardown must see siblings already archived. Serialize per
17390
+ * worktree so parallel archive/purge of sibling tabs still removes once.
17391
+ */
17392
+ enqueueWorktreeTeardown(thread, fn) {
17393
+ const key = isGlobalThread(thread) ? `archive:global:${thread.id}` : `archive:wt:${normalizeWorktreePath(thread.worktreePath)}`;
17394
+ return enqueueByKey(key, fn);
17395
+ }
17037
17396
  async archive(threadRef) {
17397
+ const thread = this.requireThread(threadRef);
17398
+ return this.enqueueWorktreeTeardown(thread, () => this.archiveUnlocked(threadRef));
17399
+ }
17400
+ async archiveUnlocked(threadRef) {
17038
17401
  const thread = this.requireThread(threadRef);
17039
17402
  this.stop(thread.id);
17040
17403
  this.releaseOrchestratorCaffeinate(thread);
@@ -17070,6 +17433,10 @@ var Orchestrator = class {
17070
17433
  return archived;
17071
17434
  }
17072
17435
  async purge(threadRef, opts) {
17436
+ const thread = this.requireThread(threadRef);
17437
+ await this.enqueueWorktreeTeardown(thread, () => this.purgeUnlocked(threadRef, opts));
17438
+ }
17439
+ async purgeUnlocked(threadRef, opts) {
17073
17440
  const thread = this.requireThread(threadRef);
17074
17441
  this.stop(thread.id);
17075
17442
  this.releaseOrchestratorCaffeinate(thread);
@@ -17104,13 +17471,12 @@ var Orchestrator = class {
17104
17471
  this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
17105
17472
  return restored2;
17106
17473
  }
17107
- if (!(0, import_node_fs42.existsSync)(thread.worktreePath)) {
17474
+ if (!(0, import_node_fs43.existsSync)(thread.worktreePath)) {
17108
17475
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
17109
- const { execa: execa7 } = await import("execa");
17110
17476
  const slug = thread.worktreePath.split("/").pop();
17111
17477
  const dest = thread.worktreePath;
17112
- await execa7("git", ["worktree", "add", dest, thread.branchName], {
17113
- cwd: thread.repoPath
17478
+ await withRepoGitLock(thread.repoPath, async () => {
17479
+ await git(["worktree", "add", dest, thread.branchName], thread.repoPath);
17114
17480
  });
17115
17481
  void createThreadWorktree2;
17116
17482
  void slug;
@@ -17362,6 +17728,15 @@ function mcpArchiveBlockedReason(thread) {
17362
17728
  // src/mcp/server.ts
17363
17729
  init_profile();
17364
17730
 
17731
+ // src/mcp/wait-for-turn.ts
17732
+ var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
17733
+ function mcpWaitForTurnTimeoutMs(requested) {
17734
+ const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
17735
+ if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
17736
+ return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
17737
+ }
17738
+ 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.";
17739
+
17365
17740
  // src/mcp/slack-tools.ts
17366
17741
  var import_zod = require("zod");
17367
17742
 
@@ -18033,7 +18408,9 @@ async function startMcpServer() {
18033
18408
  const threads = orch.getThreads(true);
18034
18409
  const lines = threads.map((t) => {
18035
18410
  const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path39.basename)(t.repoPath) || t.repoPath;
18036
- 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}` : ""}`;
18411
+ const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
18412
+ const progress = live?.summary ? ` ${live.summary}` : "";
18413
+ 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}`;
18037
18414
  });
18038
18415
  return {
18039
18416
  content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
@@ -18042,13 +18419,14 @@ async function startMcpServer() {
18042
18419
  );
18043
18420
  server.tool(
18044
18421
  "get_thread",
18045
- "Get a compact thread summary by id/ref",
18422
+ "Get a compact thread summary by id/ref. While running, includes progress (last tool/thinking) and lastActivityAt.",
18046
18423
  { ref: import_zod3.z.string() },
18047
18424
  async ({ ref }) => {
18048
18425
  const t = orch.getThread(ref);
18049
18426
  if (!t) {
18050
18427
  return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
18051
18428
  }
18429
+ const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
18052
18430
  const summary = {
18053
18431
  id: t.id,
18054
18432
  title: t.title,
@@ -18063,7 +18441,10 @@ async function startMcpServer() {
18063
18441
  messageCount: t.messages.length,
18064
18442
  devPort: t.devPort,
18065
18443
  prUrl: t.prUrl,
18066
- lastError: t.lastError ?? null
18444
+ lastError: t.lastError ?? null,
18445
+ stillRunning: t.status === "running" || t.status === "queued",
18446
+ progress: live?.summary ?? null,
18447
+ lastActivityAt: live?.updatedAt ?? null
18067
18448
  };
18068
18449
  return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
18069
18450
  }
@@ -18389,13 +18770,15 @@ async function startMcpServer() {
18389
18770
  );
18390
18771
  server.tool(
18391
18772
  "wait_for_turn",
18392
- "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread or ask_git to read the agent reply.",
18773
+ "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.",
18393
18774
  {
18394
18775
  ref: import_zod3.z.string(),
18395
18776
  timeoutMs: import_zod3.z.number().optional()
18396
18777
  },
18397
18778
  async ({ ref, timeoutMs }) => {
18398
- const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
18779
+ const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
18780
+ resolveIfStillRunning: true
18781
+ });
18399
18782
  const result = orch.getTurnResult(thread.id);
18400
18783
  return {
18401
18784
  content: [
@@ -18404,7 +18787,12 @@ async function startMcpServer() {
18404
18787
  text: JSON.stringify({
18405
18788
  id: thread.id,
18406
18789
  status: result.status,
18407
- text: result.text
18790
+ text: result.text,
18791
+ lastError: result.lastError,
18792
+ stillRunning: result.stillRunning,
18793
+ progress: result.progress,
18794
+ lastActivityAt: result.lastActivityAt,
18795
+ hint: result.stillRunning ? MCP_WAIT_STILL_RUNNING_HINT : void 0
18408
18796
  })
18409
18797
  }
18410
18798
  ]
@@ -18413,11 +18801,21 @@ async function startMcpServer() {
18413
18801
  );
18414
18802
  server.tool(
18415
18803
  "get_turn_result",
18416
- "Final assistant message only (not full transcript)",
18804
+ "Assistant message when the turn finished, or live progress while stillRunning. Not the full transcript.",
18417
18805
  { ref: import_zod3.z.string() },
18418
18806
  async ({ ref }) => {
18419
18807
  const result = orch.getTurnResult(ref);
18420
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
18808
+ return {
18809
+ content: [
18810
+ {
18811
+ type: "text",
18812
+ text: JSON.stringify({
18813
+ ...result,
18814
+ hint: result.stillRunning ? MCP_WAIT_STILL_RUNNING_HINT : void 0
18815
+ })
18816
+ }
18817
+ ]
18818
+ };
18421
18819
  }
18422
18820
  );
18423
18821
  server.tool(
@@ -18534,7 +18932,7 @@ async function startMcpServer() {
18534
18932
  );
18535
18933
  server.tool(
18536
18934
  "request_review",
18537
- '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.',
18935
+ '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.',
18538
18936
  { ref: import_zod3.z.string().describe("Worktree thread id/ref to review") },
18539
18937
  async ({ ref }) => {
18540
18938
  try {
@@ -18562,7 +18960,7 @@ async function startMcpServer() {
18562
18960
  );
18563
18961
  server.tool(
18564
18962
  "ask_git",
18565
- "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.",
18963
+ "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.",
18566
18964
  {
18567
18965
  ref: import_zod3.z.string().describe("Worktree thread id/ref"),
18568
18966
  action: import_zod3.z.enum(AGENT_GIT_ACTIONS).describe(
@@ -18613,7 +19011,7 @@ async function startMcpServer() {
18613
19011
  );
18614
19012
  server.tool(
18615
19013
  "fork_worktree",
18616
- "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.",
19014
+ "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.",
18617
19015
  {
18618
19016
  ref: import_zod3.z.string().describe("Worktree thread id/ref to fork"),
18619
19017
  through_index: import_zod3.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -18658,7 +19056,7 @@ async function startMcpServer() {
18658
19056
  );
18659
19057
  server.tool(
18660
19058
  "fork_chat",
18661
- "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.",
19059
+ "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.",
18662
19060
  {
18663
19061
  ref: import_zod3.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
18664
19062
  through_index: import_zod3.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -19341,7 +19739,7 @@ init_connected_teams();
19341
19739
  init_injected_mcp();
19342
19740
 
19343
19741
  // src/agents/user-mcp-config.ts
19344
- var import_node_fs43 = require("fs");
19742
+ var import_node_fs44 = require("fs");
19345
19743
  var import_node_os12 = require("os");
19346
19744
  var import_node_path40 = require("path");
19347
19745
  init_paths();
@@ -19377,16 +19775,16 @@ function mergeSideboardIntoMcpServersJson(existing, sideboard) {
19377
19775
  }
19378
19776
  function writeMergedMcpServersJson(configPath, sideboard) {
19379
19777
  let existing = {};
19380
- if ((0, import_node_fs43.existsSync)(configPath)) {
19778
+ if ((0, import_node_fs44.existsSync)(configPath)) {
19381
19779
  try {
19382
- existing = JSON.parse((0, import_node_fs43.readFileSync)(configPath, "utf8"));
19780
+ existing = JSON.parse((0, import_node_fs44.readFileSync)(configPath, "utf8"));
19383
19781
  } catch {
19384
19782
  existing = {};
19385
19783
  }
19386
19784
  }
19387
19785
  const next = mergeSideboardIntoMcpServersJson(existing, sideboard);
19388
- (0, import_node_fs43.mkdirSync)((0, import_node_path40.dirname)(configPath), { recursive: true });
19389
- (0, import_node_fs43.writeFileSync)(configPath, `${JSON.stringify(next, null, 2)}
19786
+ (0, import_node_fs44.mkdirSync)((0, import_node_path40.dirname)(configPath), { recursive: true });
19787
+ (0, import_node_fs44.writeFileSync)(configPath, `${JSON.stringify(next, null, 2)}
19390
19788
  `);
19391
19789
  }
19392
19790
  function launchFromResolved(server) {
@@ -19404,7 +19802,7 @@ async function registerPackagedUserMcpClients() {
19404
19802
  const cursor = userCursorMcpConfigPath();
19405
19803
  writeMergedMcpServersJson(cursor, launch);
19406
19804
  const claude = userClaudeMcpConfigPath();
19407
- if ((0, import_node_fs43.existsSync)(claude)) {
19805
+ if ((0, import_node_fs44.existsSync)(claude)) {
19408
19806
  writeMergedMcpServersJson(claude, launch);
19409
19807
  return { cursor, claude };
19410
19808
  }
@@ -19910,6 +20308,8 @@ function connectSession(ws, opts, log) {
19910
20308
 
19911
20309
  // src/slack/relay-protocol.ts
19912
20310
  var import_node_crypto11 = require("crypto");
20311
+ var SLACK_RELAY_PING_INTERVAL_MS = 2e4;
20312
+ var SLACK_RELAY_PONG_TIMEOUT_MS = 15e3;
19913
20313
  function slackRelaySessionKey(teamId, userId, deviceId) {
19914
20314
  return `${teamId.trim()}:${userId.trim()}:${deviceId.trim()}`;
19915
20315
  }
@@ -20095,10 +20495,21 @@ function connectSession2(ws, opts, log) {
20095
20495
  const allowedUsers = new Set(
20096
20496
  opts.workspaces.map((w) => `${w.teamId.trim()}:${w.userId.trim()}`)
20097
20497
  );
20498
+ const pingMs = opts.pingIntervalMs ?? SLACK_RELAY_PING_INTERVAL_MS;
20499
+ const pongMs = opts.pongTimeoutMs ?? SLACK_RELAY_PONG_TIMEOUT_MS;
20500
+ let pingTimer = null;
20501
+ let pongTimer = null;
20502
+ const stopKeepalive = () => {
20503
+ if (pingTimer) clearInterval(pingTimer);
20504
+ if (pongTimer) clearTimeout(pongTimer);
20505
+ pingTimer = null;
20506
+ pongTimer = null;
20507
+ };
20098
20508
  const finish = (err) => {
20099
20509
  if (settled) return;
20100
20510
  settled = true;
20101
20511
  pending.clear();
20512
+ stopKeepalive();
20102
20513
  opts.signal?.removeEventListener("abort", onAbort);
20103
20514
  if (err) reject(err);
20104
20515
  else resolve();
@@ -20111,6 +20522,25 @@ function connectSession2(ws, opts, log) {
20111
20522
  finish();
20112
20523
  };
20113
20524
  opts.signal?.addEventListener("abort", onAbort, { once: true });
20525
+ const armPongTimeout = () => {
20526
+ if (pongTimer) clearTimeout(pongTimer);
20527
+ pongTimer = setTimeout(() => {
20528
+ log("relay error: ping timeout");
20529
+ try {
20530
+ ws.close();
20531
+ } catch {
20532
+ }
20533
+ finish(new Error("relay ping timeout"));
20534
+ }, pongMs);
20535
+ };
20536
+ const pingOnce = () => {
20537
+ try {
20538
+ send(ws, { type: "ping" });
20539
+ armPongTimeout();
20540
+ } catch (err) {
20541
+ finish(err instanceof Error ? err : new Error("relay ping failed"));
20542
+ }
20543
+ };
20114
20544
  ws.addEventListener("open", () => {
20115
20545
  const label = opts.deviceLabel?.trim() || opts.deviceId.trim();
20116
20546
  log(`Relay connected \xB7 ${label}`);
@@ -20125,6 +20555,8 @@ function connectSession2(ws, opts, log) {
20125
20555
  userToken: wsInfo.userToken
20126
20556
  });
20127
20557
  }
20558
+ pingOnce();
20559
+ pingTimer = setInterval(pingOnce, pingMs);
20128
20560
  });
20129
20561
  ws.addEventListener("error", () => {
20130
20562
  finish(new Error("WebSocket error"));
@@ -20136,7 +20568,13 @@ function connectSession2(ws, opts, log) {
20136
20568
  const raw = typeof ev.data === "string" ? ev.data : String(ev.data ?? "");
20137
20569
  const msg = parseSlackRelayServerMessage(raw);
20138
20570
  if (!msg) return;
20139
- if (msg.type === "pong") return;
20571
+ if (msg.type === "pong") {
20572
+ if (pongTimer) {
20573
+ clearTimeout(pongTimer);
20574
+ pongTimer = null;
20575
+ }
20576
+ return;
20577
+ }
20140
20578
  if (msg.type === "error") {
20141
20579
  log(`relay error: ${msg.message}`);
20142
20580
  return;
@@ -20259,6 +20697,13 @@ function slackReplyThreadTs(msg) {
20259
20697
  if (msg.kind === "mention") return msg.ts;
20260
20698
  return void 0;
20261
20699
  }
20700
+ var SLACK_PROGRESS_DELAY_MS = 2e4;
20701
+ var SLACK_PROGRESS_EDIT_MS = 15e3;
20702
+ function formatSlackWorkingText(summary) {
20703
+ const line = (summary ?? "").trim();
20704
+ if (!line || /^working/i.test(line)) return "Working\u2026";
20705
+ return `Working\u2026 ${line}`;
20706
+ }
20262
20707
  var SLACK_SEEN_REACTION = "eyes";
20263
20708
  function writeTokenForTeam(teamId) {
20264
20709
  const workspaces = listSlackWorkspacesRaw();
@@ -20295,8 +20740,8 @@ async function ackSlackInboundSeen(msg, opts) {
20295
20740
  log(`react error: ${errMsg}${hint}`);
20296
20741
  }
20297
20742
  }
20298
- async function postSlackText(target, text3, opts) {
20299
- const stub = {
20743
+ function replyStub(target) {
20744
+ return {
20300
20745
  teamId: target.teamId,
20301
20746
  channelId: target.channelId,
20302
20747
  ts: target.threadTs ?? "",
@@ -20304,12 +20749,15 @@ async function postSlackText(target, text3, opts) {
20304
20749
  text: "",
20305
20750
  kind: "dm"
20306
20751
  };
20752
+ }
20753
+ async function postSlackText(target, text3, opts) {
20307
20754
  if (opts.postReply) {
20308
- await opts.postReply(stub, text3);
20309
- return;
20755
+ const result = await opts.postReply(replyStub(target), text3);
20756
+ const ts2 = result && typeof result === "object" && typeof result.ts === "string" ? result.ts.trim() : "";
20757
+ return ts2 ? { ts: ts2 } : null;
20310
20758
  }
20311
20759
  const token = writeTokenForTeam(target.teamId);
20312
- await slackApi(
20760
+ const json = await slackApi(
20313
20761
  token,
20314
20762
  "chat.postMessage",
20315
20763
  {
@@ -20319,18 +20767,163 @@ async function postSlackText(target, text3, opts) {
20319
20767
  },
20320
20768
  opts.fetchImpl
20321
20769
  );
20770
+ const ts = json.ts?.trim();
20771
+ return ts ? { ts } : null;
20322
20772
  }
20323
- async function postSlackReply(msg, text3, opts) {
20324
- await postSlackText(
20773
+ async function updateSlackText(target, ts, text3, opts) {
20774
+ if (opts.updateReply) {
20775
+ await opts.updateReply(replyStub(target), ts, text3);
20776
+ return;
20777
+ }
20778
+ const token = writeTokenForTeam(target.teamId);
20779
+ await slackApi(
20780
+ token,
20781
+ "chat.update",
20325
20782
  {
20326
- teamId: msg.teamId,
20327
- channelId: msg.channelId,
20328
- threadTs: slackReplyThreadTs(msg)
20783
+ channel: target.channelId,
20784
+ ts,
20785
+ text: text3
20329
20786
  },
20330
- signForThisMac(text3),
20331
- opts
20787
+ opts.fetchImpl
20788
+ );
20789
+ }
20790
+ async function deleteSlackText(target, ts, opts) {
20791
+ if (opts.deleteReply) {
20792
+ await opts.deleteReply(replyStub(target), ts);
20793
+ return;
20794
+ }
20795
+ const token = writeTokenForTeam(target.teamId);
20796
+ await slackApi(
20797
+ token,
20798
+ "chat.delete",
20799
+ {
20800
+ channel: target.channelId,
20801
+ ts
20802
+ },
20803
+ opts.fetchImpl
20332
20804
  );
20333
20805
  }
20806
+ function replyTargetOf(msg) {
20807
+ return {
20808
+ teamId: msg.teamId,
20809
+ channelId: msg.channelId,
20810
+ threadTs: slackReplyThreadTs(msg)
20811
+ };
20812
+ }
20813
+ async function postSlackReply(msg, text3, opts) {
20814
+ await postSlackText(replyTargetOf(msg), signForThisMac(text3), opts);
20815
+ }
20816
+ function startSlackTurnProgress(msg, threadId, opts) {
20817
+ const log = opts.onLog ?? (() => void 0);
20818
+ const target = replyTargetOf(msg);
20819
+ const delayMs = opts.progressDelayMs ?? SLACK_PROGRESS_DELAY_MS;
20820
+ const editMs = opts.progressEditMs ?? SLACK_PROGRESS_EDIT_MS;
20821
+ let stopped = false;
20822
+ let postedTs = null;
20823
+ let lastBody = null;
20824
+ let chain = Promise.resolve();
20825
+ let editTimer = null;
20826
+ const run2 = (fn) => {
20827
+ const next = chain.then(fn, fn);
20828
+ chain = next.then(
20829
+ () => void 0,
20830
+ () => void 0
20831
+ );
20832
+ return next;
20833
+ };
20834
+ const stillLive = () => {
20835
+ if (stopped || slackInboundSuperseded(opts)) return false;
20836
+ const thread = readThread(threadId());
20837
+ return Boolean(thread && (thread.status === "running" || thread.status === "queued"));
20838
+ };
20839
+ const postWorking = async () => {
20840
+ if (!stillLive()) return;
20841
+ const summary = readTurnLive(threadId())?.summary ?? "Working\u2026";
20842
+ const body = formatSlackWorkingText(summary);
20843
+ try {
20844
+ const posted = await postSlackText(target, signForThisMac(body), opts);
20845
+ postedTs = posted?.ts ?? postedTs;
20846
+ lastBody = body;
20847
+ if (postedTs && !stopped && !editTimer) {
20848
+ editTimer = setInterval(() => {
20849
+ void run2(editWorking);
20850
+ }, editMs);
20851
+ }
20852
+ } catch (err) {
20853
+ const errMsg = err instanceof Error ? err.message : String(err);
20854
+ log(`progress post error: ${errMsg}`);
20855
+ }
20856
+ };
20857
+ const editWorking = async () => {
20858
+ if (!postedTs || !stillLive()) return;
20859
+ const summary = readTurnLive(threadId())?.summary ?? "Working\u2026";
20860
+ const body = formatSlackWorkingText(summary);
20861
+ if (body === lastBody) return;
20862
+ lastBody = body;
20863
+ try {
20864
+ await updateSlackText(target, postedTs, signForThisMac(body), opts);
20865
+ } catch (err) {
20866
+ const errMsg = err instanceof Error ? err.message : String(err);
20867
+ log(`progress update error: ${errMsg}`);
20868
+ }
20869
+ };
20870
+ const delayTimer = setTimeout(() => {
20871
+ void run2(postWorking);
20872
+ }, delayMs);
20873
+ const stop = () => {
20874
+ stopped = true;
20875
+ clearTimeout(delayTimer);
20876
+ if (editTimer) {
20877
+ clearInterval(editTimer);
20878
+ editTimer = null;
20879
+ }
20880
+ };
20881
+ return {
20882
+ stop,
20883
+ discard: () => {
20884
+ stop();
20885
+ return run2(async () => {
20886
+ if (!postedTs) return;
20887
+ const ts = postedTs;
20888
+ postedTs = null;
20889
+ try {
20890
+ await deleteSlackText(target, ts, opts);
20891
+ } catch (err) {
20892
+ const errMsg = err instanceof Error ? err.message : String(err);
20893
+ log(`progress delete error: ${errMsg}`);
20894
+ }
20895
+ });
20896
+ },
20897
+ finishWith: (text3) => {
20898
+ stop();
20899
+ return run2(async () => {
20900
+ const signed = signForThisMac(text3.trim());
20901
+ if (!signed) {
20902
+ if (!postedTs) return;
20903
+ const ts = postedTs;
20904
+ postedTs = null;
20905
+ try {
20906
+ await deleteSlackText(target, ts, opts);
20907
+ } catch (err) {
20908
+ const errMsg = err instanceof Error ? err.message : String(err);
20909
+ log(`progress delete error: ${errMsg}`);
20910
+ }
20911
+ return;
20912
+ }
20913
+ if (postedTs) {
20914
+ try {
20915
+ await updateSlackText(target, postedTs, signed, opts);
20916
+ return;
20917
+ } catch (err) {
20918
+ const errMsg = err instanceof Error ? err.message : String(err);
20919
+ log(`progress update error: ${errMsg}`);
20920
+ }
20921
+ }
20922
+ await postSlackText(target, signed, opts);
20923
+ });
20924
+ }
20925
+ };
20926
+ }
20334
20927
  var lastRelayed = /* @__PURE__ */ new Map();
20335
20928
  function markRelayed(threadId, text3) {
20336
20929
  lastRelayed.set(threadId, `${threadId}:${text3}`);
@@ -20422,10 +21015,15 @@ async function handleSlackInbound(msg, opts) {
20422
21015
  });
20423
21016
  };
20424
21017
  bindReplyTarget(fresh.id);
21018
+ const progress = startSlackTurnProgress(msg, () => fresh.id, opts);
20425
21019
  const runTurn = async (threadId) => {
20426
21020
  await orch.send(threadId, prompt);
20427
21021
  await orch.waitForTurn(threadId, 14 * 60 * 1e3);
20428
21022
  };
21023
+ const dropProgress = async (reason) => {
21024
+ log(reason);
21025
+ await progress.discard();
21026
+ };
20429
21027
  let reply;
20430
21028
  try {
20431
21029
  try {
@@ -20438,12 +21036,12 @@ async function handleSlackInbound(msg, opts) {
20438
21036
  await runTurn(fresh.id);
20439
21037
  }
20440
21038
  if (slackInboundSuperseded(opts)) {
20441
- log(`turn finished ${msg.ts} (superseded)`);
21039
+ await dropProgress(`turn finished ${msg.ts} (superseded)`);
20442
21040
  return;
20443
21041
  }
20444
21042
  let after = readThread(fresh.id);
20445
21043
  if (after?.status === "stopped") {
20446
- log(`turn finished ${msg.ts} (interrupted, skip post)`);
21044
+ await dropProgress(`turn finished ${msg.ts} (interrupted, skip post)`);
20447
21045
  return;
20448
21046
  }
20449
21047
  if (!after || after.status === "archived") {
@@ -20452,46 +21050,48 @@ async function handleSlackInbound(msg, opts) {
20452
21050
  bindReplyTarget(fresh.id);
20453
21051
  await runTurn(fresh.id);
20454
21052
  if (slackInboundSuperseded(opts)) {
20455
- log(`turn finished ${msg.ts} (superseded)`);
21053
+ await dropProgress(`turn finished ${msg.ts} (superseded)`);
20456
21054
  return;
20457
21055
  }
20458
21056
  after = readThread(fresh.id);
20459
21057
  if (!after || after.status === "stopped" || after.status === "archived") {
20460
- log(`turn finished ${msg.ts} (interrupted, skip post)`);
21058
+ await dropProgress(`turn finished ${msg.ts} (interrupted, skip post)`);
20461
21059
  return;
20462
21060
  }
20463
21061
  }
20464
21062
  reply = orch.getTurnResult(fresh.id).text.trim();
20465
21063
  } catch (err) {
20466
21064
  if (slackInboundSuperseded(opts)) {
20467
- log(`turn error ${msg.ts} (superseded)`);
21065
+ await dropProgress(`turn error ${msg.ts} (superseded)`);
20468
21066
  return;
20469
21067
  }
20470
21068
  const errMsg = err instanceof Error ? err.message : String(err);
20471
21069
  if (slackCoordinatorGone(err)) {
20472
- log(`turn finished ${msg.ts} (thread gone, skip post)`);
21070
+ await dropProgress(`turn finished ${msg.ts} (thread gone, skip post)`);
20473
21071
  return;
20474
21072
  }
20475
21073
  const timedOut = /timed out|timeout/i.test(errMsg);
20476
21074
  const errorReply = timedOut ? SLACK_LISTEN_TIMEOUT_REPLY : `Sideboard coordinator error: ${errMsg}`;
20477
- await postSlackReply(msg, errorReply, opts).catch((postErr) => {
21075
+ try {
21076
+ await progress.finishWith(errorReply);
21077
+ log(`replied error ${msg.ts}: ${errMsg}`);
21078
+ } catch (postErr) {
20478
21079
  const postMsg = postErr instanceof Error ? postErr.message : String(postErr);
20479
21080
  log(`post error: ${postMsg}`);
20480
- });
20481
- log(`replied error ${msg.ts}: ${errMsg}`);
21081
+ }
20482
21082
  if (!timedOut) throw err;
20483
21083
  return;
20484
21084
  }
20485
21085
  if (!reply) {
20486
- log(`post error: no agent text to send for ${msg.ts}`);
21086
+ await dropProgress(`post error: no agent text to send for ${msg.ts}`);
20487
21087
  return;
20488
21088
  }
20489
21089
  if (alreadyRelayed(fresh.id, reply)) {
20490
- log(`turn finished ${msg.ts} (already posted)`);
21090
+ await dropProgress(`turn finished ${msg.ts} (already posted)`);
20491
21091
  return;
20492
21092
  }
20493
21093
  try {
20494
- await postSlackReply(msg, reply, opts);
21094
+ await progress.finishWith(reply);
20495
21095
  markRelayed(fresh.id, reply);
20496
21096
  log(`replied ${msg.ts} (${reply.length} chars)`);
20497
21097
  } catch (err) {
@@ -20818,7 +21418,7 @@ var import_node_http3 = require("http");
20818
21418
  var import_ws3 = require("ws");
20819
21419
 
20820
21420
  // src/slack/relay-static.ts
20821
- var import_node_fs44 = require("fs");
21421
+ var import_node_fs45 = require("fs");
20822
21422
  var import_promises = require("fs/promises");
20823
21423
  var import_node_path41 = __toESM(require("path"), 1);
20824
21424
  var TYPES = {
@@ -20877,7 +21477,7 @@ function sendFile(req, res, file, size) {
20877
21477
  res.end();
20878
21478
  return true;
20879
21479
  }
20880
- (0, import_node_fs44.createReadStream)(file).pipe(res);
21480
+ (0, import_node_fs45.createReadStream)(file).pipe(res);
20881
21481
  return true;
20882
21482
  }
20883
21483
  async function tryServeStatic(req, res, root) {
@@ -21042,6 +21642,20 @@ async function startSlackRelayServer(opts) {
21042
21642
  }
21043
21643
  }
21044
21644
  };
21645
+ const ping = setInterval(() => {
21646
+ if (ws.readyState !== ws.OPEN) {
21647
+ clearInterval(ping);
21648
+ return;
21649
+ }
21650
+ try {
21651
+ ws.ping();
21652
+ } catch {
21653
+ clearInterval(ping);
21654
+ }
21655
+ }, SLACK_RELAY_PING_INTERVAL_MS);
21656
+ const stopPing = () => clearInterval(ping);
21657
+ ws.on("close", stopPing);
21658
+ ws.on("error", stopPing);
21045
21659
  log("desktop connected");
21046
21660
  ws.on("message", (data) => {
21047
21661
  const raw = typeof data === "string" ? data : data.toString("utf8");
@@ -21153,6 +21767,8 @@ async function startSlackRelayServer(opts) {
21153
21767
  SLACK_LISTEN_TIMEOUT_REPLY,
21154
21768
  SLACK_OAUTH_CANCELLED,
21155
21769
  SLACK_OAUTH_REDIRECT,
21770
+ SLACK_PROGRESS_DELAY_MS,
21771
+ SLACK_PROGRESS_EDIT_MS,
21156
21772
  SLACK_REPLY_FORMATTING,
21157
21773
  SLACK_SEEN_REACTION,
21158
21774
  SlackOAuthCancelledError,
@@ -21310,6 +21926,7 @@ async function startSlackRelayServer(opts) {
21310
21926
  formatSlackInboundPrompt,
21311
21927
  formatSlackRepliesForTurn,
21312
21928
  formatSlackSignedReply,
21929
+ formatSlackWorkingText,
21313
21930
  formatTranscriptMarkdown,
21314
21931
  formatUiReminder,
21315
21932
  formatWorkspaceInventory,
@@ -21605,6 +22222,7 @@ async function startSlackRelayServer(opts) {
21605
22222
  thisProcessShouldDrainAgentQueues,
21606
22223
  threadDisplayLabel,
21607
22224
  threadFilePath,
22225
+ threadLivePath,
21608
22226
  threadLockPath,
21609
22227
  threadRequestsBrightsyMcp,
21610
22228
  threadsDir,
@@ -21628,6 +22246,7 @@ async function startSlackRelayServer(opts) {
21628
22246
  userClaudeMcpConfigPath,
21629
22247
  userCursorMcpConfigPath,
21630
22248
  validateLinearApiKey,
22249
+ waitForPidExit,
21631
22250
  warmGithubAgentAuth,
21632
22251
  withAgentInstructions,
21633
22252
  withExportedPath,