@sideboard-ai/core 0.1.100 → 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 (50) hide show
  1. package/dist/agents/cursor-runner.js +1 -1
  2. package/dist/{agents-CLP5BG4O.js → agents-UT5GCJH3.js} +8 -8
  3. package/dist/{agents-OXZDMPSE.js → agents-XZBAKBO2.js} +8 -8
  4. package/dist/{app-settings-BRF7VKTQ.js → app-settings-EKJM2344.js} +2 -2
  5. package/dist/{app-settings-ZXPAB3QN.js → app-settings-VDJK2BZ2.js} +2 -2
  6. package/dist/{caffeinate-hold-U3QJBV67.js → caffeinate-hold-NBRUWYXO.js} +2 -2
  7. package/dist/{caffeinate-hold-I7SG6UQR.js → caffeinate-hold-X3JNPR42.js} +2 -2
  8. package/dist/{chunk-MHJV4WS3.js → chunk-27MCESES.js} +3 -3
  9. package/dist/{chunk-QSLE4VEM.js → chunk-2GO3YKBA.js} +4 -4
  10. package/dist/{chunk-DKHGWYWR.js → chunk-7YKXHBIW.js} +1 -1
  11. package/dist/{chunk-JL4SJ6V7.js → chunk-BD2ICC4D.js} +4 -0
  12. package/dist/{chunk-NZPGMNFC.js → chunk-DCOZABNT.js} +1 -1
  13. package/dist/{chunk-CIRXAYWS.js → chunk-DHAGMJIK.js} +55 -28
  14. package/dist/{chunk-HYKZEP5A.js → chunk-ETMRBJKA.js} +11 -11
  15. package/dist/{chunk-OXC3MEFI.js → chunk-H6LP57EF.js} +6 -6
  16. package/dist/{chunk-GR4JKWR4.js → chunk-HHLZWO7U.js} +5 -5
  17. package/dist/{chunk-3WA37BRI.js → chunk-KGSVLZV6.js} +11 -4
  18. package/dist/{chunk-7KDBDMNC.js → chunk-KLBOWJ74.js} +1 -1
  19. package/dist/{chunk-EQPQTLW6.js → chunk-LANDJIAI.js} +6 -6
  20. package/dist/{chunk-MRBKGFTL.js → chunk-ND6AO7C5.js} +3 -3
  21. package/dist/{chunk-WQTTUC4N.js → chunk-OW34MVDN.js} +5 -5
  22. package/dist/{chunk-JOF3XIEM.js → chunk-P3BQ5DOL.js} +11 -4
  23. package/dist/{chunk-B4R2VB2A.js → chunk-QZQEXME2.js} +4 -0
  24. package/dist/{chunk-R7BQBSDT.js → chunk-WA3DVVWG.js} +55 -28
  25. package/dist/{chunk-6LFV4VFI.js → chunk-WSYCND2U.js} +1 -1
  26. package/dist/{chunk-FT2SQOL4.js → chunk-XHZJJWLI.js} +1 -1
  27. package/dist/{chunk-IFHKPZHA.js → chunk-YJADJH5U.js} +11 -11
  28. package/dist/{connected-teams-SWHJWK2A.js → connected-teams-J4I5AKMR.js} +2 -2
  29. package/dist/{connected-teams-TLVKZPJP.js → connected-teams-M5XNXRI5.js} +2 -2
  30. package/dist/{coordinator-prompt-OOBSQCWQ.js → coordinator-prompt-3DFZRMXE.js} +5 -5
  31. package/dist/{coordinator-prompt-HHRKQWCX.js → coordinator-prompt-UWHSD65V.js} +5 -5
  32. package/dist/{cursor-recover-S5ORPW34.js → cursor-recover-JBTIDARH.js} +1 -1
  33. package/dist/{cursor-recover-G34BYKL2.js → cursor-recover-ZUJG6XRQ.js} +1 -1
  34. package/dist/{global-workspace-YZWYHLQY.js → global-workspace-KJQS5S2C.js} +6 -6
  35. package/dist/{global-workspace-KYEBFWKB.js → global-workspace-UR4E3CCG.js} +6 -6
  36. package/dist/index.cjs +626 -153
  37. package/dist/index.d.cts +37 -5
  38. package/dist/index.d.ts +37 -5
  39. package/dist/index.js +545 -121
  40. package/dist/mcp/run-stdio.cjs +426 -177
  41. package/dist/mcp/run-stdio.js +330 -127
  42. package/dist/{paths-TZX4L5X7.js → paths-QCI3MTO5.js} +3 -1
  43. package/dist/{paths-JQM2XRAD.js → paths-UMEDHOS5.js} +3 -1
  44. package/dist/{thread-store-XAKWCV2E.js → thread-store-4OUEQ2DB.js} +2 -2
  45. package/dist/{thread-store-V7NOOXKE.js → thread-store-DQJGDKIJ.js} +2 -2
  46. package/dist/{workspaces-ENVUDK6C.js → workspaces-NTIVI6DI.js} +7 -7
  47. package/dist/{workspaces-NNPD7QVV.js → workspaces-TKJXPNV4.js} +7 -7
  48. package/dist/{worktree-EO5QAGJU.js → worktree-NKWN5ZTS.js} +4 -4
  49. package/dist/{worktree-DA4BOV7G.js → worktree-PEUPFAKL.js} +4 -4
  50. package/package.json +1 -1
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. 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.",
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. On status error, lastError (and text) is the failure \u2014 switch agent, tell the user, or retry; do not treat empty text as success.",
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:",
@@ -9308,17 +9357,17 @@ __export(plan_file_exports, {
9308
9357
  });
9309
9358
  function ensureAttachmentsGitignore2(worktreePath) {
9310
9359
  const gitignoreAbs = (0, import_node_path37.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
9311
- if ((0, import_node_fs40.existsSync)(gitignoreAbs)) return;
9312
- (0, import_node_fs40.mkdirSync)((0, import_node_path37.dirname)(gitignoreAbs), { recursive: true });
9313
- (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");
9314
9363
  }
9315
9364
  function planFileAbs(worktreePath) {
9316
9365
  return (0, import_node_path37.join)(worktreePath, PLAN_FILE_REL);
9317
9366
  }
9318
9367
  function readTextIfPresent2(abs) {
9319
- if (!(0, import_node_fs40.existsSync)(abs)) return null;
9368
+ if (!(0, import_node_fs41.existsSync)(abs)) return null;
9320
9369
  try {
9321
- const content = (0, import_node_fs40.readFileSync)(abs, "utf8");
9370
+ const content = (0, import_node_fs41.readFileSync)(abs, "utf8");
9322
9371
  return content.trim() ? content : null;
9323
9372
  } catch {
9324
9373
  return null;
@@ -9330,16 +9379,16 @@ function readPlanFile(worktreePath) {
9330
9379
  function writePlanFile(worktreePath, content) {
9331
9380
  ensureAttachmentsGitignore2(worktreePath);
9332
9381
  const abs = planFileAbs(worktreePath);
9333
- (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 });
9334
9383
  const body = content.trimEnd() + (content.endsWith("\n") ? "" : "\n");
9335
- (0, import_node_fs40.writeFileSync)(abs, body, "utf8");
9384
+ (0, import_node_fs41.writeFileSync)(abs, body, "utf8");
9336
9385
  return PLAN_FILE_REL;
9337
9386
  }
9338
- var import_node_fs40, import_node_path37;
9387
+ var import_node_fs41, import_node_path37;
9339
9388
  var init_plan_file = __esm({
9340
9389
  "src/plan/plan-file.ts"() {
9341
9390
  "use strict";
9342
- import_node_fs40 = require("fs");
9391
+ import_node_fs41 = require("fs");
9343
9392
  import_node_path37 = require("path");
9344
9393
  init_workspace_scratch();
9345
9394
  init_plan_present();
@@ -9356,9 +9405,9 @@ function recoverFinishedCursorRun(opts) {
9356
9405
  const agentId = opts.agentId.trim();
9357
9406
  if (!agentId) return null;
9358
9407
  const runsPath = (0, import_node_path38.join)(appDataDir(), "cursor-sdk-store", "runs.ndjson");
9359
- if (!(0, import_node_fs41.existsSync)(runsPath)) return null;
9408
+ if (!(0, import_node_fs42.existsSync)(runsPath)) return null;
9360
9409
  try {
9361
- const lines = (0, import_node_fs41.readFileSync)(runsPath, "utf8").split("\n");
9410
+ const lines = (0, import_node_fs42.readFileSync)(runsPath, "utf8").split("\n");
9362
9411
  let best = null;
9363
9412
  for (const line of lines) {
9364
9413
  const trimmed = line.trim();
@@ -9384,11 +9433,11 @@ function recoverFinishedCursorRun(opts) {
9384
9433
  return null;
9385
9434
  }
9386
9435
  }
9387
- var import_node_fs41, import_node_path38;
9436
+ var import_node_fs42, import_node_path38;
9388
9437
  var init_cursor_recover = __esm({
9389
9438
  "src/agents/cursor-recover.ts"() {
9390
9439
  "use strict";
9391
- import_node_fs41 = require("fs");
9440
+ import_node_fs42 = require("fs");
9392
9441
  import_node_path38 = require("path");
9393
9442
  init_paths();
9394
9443
  }
@@ -9477,6 +9526,8 @@ __export(index_exports, {
9477
9526
  SLACK_LISTEN_TIMEOUT_REPLY: () => SLACK_LISTEN_TIMEOUT_REPLY,
9478
9527
  SLACK_OAUTH_CANCELLED: () => SLACK_OAUTH_CANCELLED,
9479
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,
9480
9531
  SLACK_REPLY_FORMATTING: () => SLACK_REPLY_FORMATTING,
9481
9532
  SLACK_SEEN_REACTION: () => SLACK_SEEN_REACTION,
9482
9533
  SlackOAuthCancelledError: () => SlackOAuthCancelledError,
@@ -9634,6 +9685,7 @@ __export(index_exports, {
9634
9685
  formatSlackInboundPrompt: () => formatSlackInboundPrompt,
9635
9686
  formatSlackRepliesForTurn: () => formatSlackRepliesForTurn,
9636
9687
  formatSlackSignedReply: () => formatSlackSignedReply,
9688
+ formatSlackWorkingText: () => formatSlackWorkingText,
9637
9689
  formatTranscriptMarkdown: () => formatTranscriptMarkdown,
9638
9690
  formatUiReminder: () => formatUiReminder,
9639
9691
  formatWorkspaceInventory: () => formatWorkspaceInventory,
@@ -9929,6 +9981,7 @@ __export(index_exports, {
9929
9981
  thisProcessShouldDrainAgentQueues: () => thisProcessShouldDrainAgentQueues,
9930
9982
  threadDisplayLabel: () => threadDisplayLabel,
9931
9983
  threadFilePath: () => threadFilePath,
9984
+ threadLivePath: () => threadLivePath,
9932
9985
  threadLockPath: () => threadLockPath,
9933
9986
  threadRequestsBrightsyMcp: () => threadRequestsBrightsyMcp,
9934
9987
  threadsDir: () => threadsDir,
@@ -9952,6 +10005,7 @@ __export(index_exports, {
9952
10005
  userClaudeMcpConfigPath: () => userClaudeMcpConfigPath,
9953
10006
  userCursorMcpConfigPath: () => userCursorMcpConfigPath,
9954
10007
  validateLinearApiKey: () => validateLinearApiKey,
10008
+ waitForPidExit: () => waitForPidExit,
9955
10009
  warmGithubAgentAuth: () => warmGithubAgentAuth,
9956
10010
  withAgentInstructions: () => withAgentInstructions,
9957
10011
  withExportedPath: () => withExportedPath,
@@ -11022,7 +11076,7 @@ function createAgentStreamCoalescer(emit, opts) {
11022
11076
  const intervalMs = opts?.intervalMs ?? 32;
11023
11077
  let pending = null;
11024
11078
  let timer = null;
11025
- const flush = () => {
11079
+ const flush2 = () => {
11026
11080
  if (timer) {
11027
11081
  clearTimeout(timer);
11028
11082
  timer = null;
@@ -11034,13 +11088,13 @@ function createAgentStreamCoalescer(emit, opts) {
11034
11088
  };
11035
11089
  const schedule = () => {
11036
11090
  if (timer) return;
11037
- timer = setTimeout(flush, intervalMs);
11091
+ timer = setTimeout(flush2, intervalMs);
11038
11092
  timer.unref?.();
11039
11093
  };
11040
11094
  return {
11041
11095
  push(event) {
11042
11096
  if (!isTextEvent(event)) {
11043
- flush();
11097
+ flush2();
11044
11098
  emit(event);
11045
11099
  return;
11046
11100
  }
@@ -11048,12 +11102,12 @@ function createAgentStreamCoalescer(emit, opts) {
11048
11102
  if (pending && pending.type === event.type) {
11049
11103
  pending.data += event.data;
11050
11104
  } else {
11051
- flush();
11105
+ flush2();
11052
11106
  pending = { type: event.type, data: event.data };
11053
11107
  }
11054
11108
  schedule();
11055
11109
  },
11056
- flush
11110
+ flush: flush2
11057
11111
  };
11058
11112
  }
11059
11113
 
@@ -12966,11 +13020,11 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
12966
13020
  }
12967
13021
  return out;
12968
13022
  }
12969
- function stageBuffersAsAttachments(worktreePath, buffers) {
12970
- if (buffers.length === 0) return [];
13023
+ function stageBuffersAsAttachments(worktreePath, buffers2) {
13024
+ if (buffers2.length === 0) return [];
12971
13025
  const dir = ensureAttachmentsDir(worktreePath);
12972
13026
  const out = [];
12973
- for (const item of buffers) {
13027
+ for (const item of buffers2) {
12974
13028
  const originalName = (item.name || "file").replace(/[/\\]/g, "_") || "file";
12975
13029
  try {
12976
13030
  const buf = Buffer.from(item.dataBase64, "base64");
@@ -12990,8 +13044,8 @@ function stageBuffersAsAttachments(worktreePath, buffers) {
12990
13044
  }
12991
13045
  return out;
12992
13046
  }
12993
- function attachmentsFromBuffers(buffers) {
12994
- return buffers.map((item) => {
13047
+ function attachmentsFromBuffers(buffers2) {
13048
+ return buffers2.map((item) => {
12995
13049
  const name = (item.name || "file").replace(/[/\\]/g, "_") || "file";
12996
13050
  try {
12997
13051
  const buf = Buffer.from(item.dataBase64, "base64");
@@ -13648,8 +13702,11 @@ function worktreeBindingFrom(from) {
13648
13702
  };
13649
13703
  }
13650
13704
  function threadsSharingWorktree(worktreePath) {
13705
+ if (!worktreePath) return [];
13651
13706
  const key = normalizeWorktreePath(worktreePath);
13652
- 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));
13653
13710
  }
13654
13711
  function formatTranscriptMarkdown(title, messages) {
13655
13712
  const body = formatMessagesAsTranscript(messages, { tools: "full" });
@@ -14928,7 +14985,7 @@ async function refreshSlackReplyBadges(opts) {
14928
14985
  }
14929
14986
 
14930
14987
  // src/orchestrator/orchestrator.ts
14931
- var import_node_fs42 = require("fs");
14988
+ var import_node_fs43 = require("fs");
14932
14989
  init_error_detail();
14933
14990
  init_agents();
14934
14991
  init_worktree();
@@ -15188,10 +15245,131 @@ async function cloneRepoIntoSideboard(opts) {
15188
15245
  // src/orchestrator/orchestrator.ts
15189
15246
  init_thread_store();
15190
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
+ }
15191
15369
 
15192
15370
  // src/review/request-review.ts
15193
15371
  var import_node_crypto8 = require("crypto");
15194
- var import_node_fs39 = require("fs");
15372
+ var import_node_fs40 = require("fs");
15195
15373
  var import_node_path36 = require("path");
15196
15374
  init_global_workspace();
15197
15375
  init_thread_store();
@@ -15340,9 +15518,9 @@ function shouldRefreshReviewRequestTemplate(content) {
15340
15518
  return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
15341
15519
  }
15342
15520
  function readTextIfPresent(abs) {
15343
- if (!(0, import_node_fs39.existsSync)(abs)) return null;
15521
+ if (!(0, import_node_fs40.existsSync)(abs)) return null;
15344
15522
  try {
15345
- const content = (0, import_node_fs39.readFileSync)(abs, "utf8");
15523
+ const content = (0, import_node_fs40.readFileSync)(abs, "utf8");
15346
15524
  return content.trim() ? content : null;
15347
15525
  } catch {
15348
15526
  return null;
@@ -15350,9 +15528,9 @@ function readTextIfPresent(abs) {
15350
15528
  }
15351
15529
  function ensureAttachmentsGitignore(worktreePath) {
15352
15530
  const gitignoreAbs = (0, import_node_path36.join)(worktreePath, ATTACHMENTS_DIR, ".gitignore");
15353
- if ((0, import_node_fs39.existsSync)(gitignoreAbs)) return;
15354
- (0, import_node_fs39.mkdirSync)((0, import_node_path36.dirname)(gitignoreAbs), { recursive: true });
15355
- (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");
15356
15534
  }
15357
15535
  function resolveReviewGuidelines(worktreePath) {
15358
15536
  const repoAbs = (0, import_node_path36.join)(worktreePath, REPO_REVIEW_PATH);
@@ -15386,8 +15564,8 @@ function resolveReviewGuidelines(worktreePath) {
15386
15564
  };
15387
15565
  }
15388
15566
  ensureAttachmentsGitignore(worktreePath);
15389
- (0, import_node_fs39.mkdirSync)((0, import_node_path36.dirname)(localAbs), { recursive: true });
15390
- (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");
15391
15569
  return {
15392
15570
  path: REVIEW_REQUEST_PATH,
15393
15571
  name: REVIEW_REQUEST_NAME,
@@ -15409,7 +15587,7 @@ function ensureReviewRequestFile(worktreePath) {
15409
15587
  const localAbs = (0, import_node_path36.join)(worktreePath, REVIEW_REQUEST_PATH);
15410
15588
  const localContent = readTextIfPresent(localAbs) ?? readTextIfPresent((0, import_node_path36.join)(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
15411
15589
  if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
15412
- 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;
15413
15591
  return {
15414
15592
  path: path2,
15415
15593
  name: REVIEW_REQUEST_NAME,
@@ -15417,8 +15595,8 @@ function ensureReviewRequestFile(worktreePath) {
15417
15595
  source: "local"
15418
15596
  };
15419
15597
  }
15420
- (0, import_node_fs39.mkdirSync)((0, import_node_path36.dirname)(repoAbs), { recursive: true });
15421
- (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");
15422
15600
  return {
15423
15601
  path: REPO_REVIEW_PATH,
15424
15602
  name: REPO_REVIEW_NAME,
@@ -15627,6 +15805,15 @@ function isPidAlive(pid) {
15627
15805
  return false;
15628
15806
  }
15629
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;
15630
15817
  function writeLiveStatus(threadId, status, lastError) {
15631
15818
  const latest = readThread(threadId);
15632
15819
  if (!latest || latest.status === "archived") return latest;
@@ -15704,7 +15891,7 @@ var Orchestrator = class {
15704
15891
  }
15705
15892
  continue;
15706
15893
  }
15707
- if (!(0, import_node_fs42.existsSync)(thread.worktreePath)) {
15894
+ if (!(0, import_node_fs43.existsSync)(thread.worktreePath)) {
15708
15895
  setStatus(thread.id, "broken", "Worktree missing on disk");
15709
15896
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
15710
15897
  continue;
@@ -16025,14 +16212,10 @@ var Orchestrator = class {
16025
16212
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
16026
16213
  const livePid = current.agentPid;
16027
16214
  const foreignLive = !inFlight && typeof livePid === "number" && livePid > 0 && isPidAlive(livePid);
16028
- if (inFlight) {
16215
+ if (inFlight || foreignLive) {
16029
16216
  this.stop(thread.id, { clearQueue: false, continueQueue: true });
16030
- } else {
16031
- if (foreignLive) {
16032
- this.stop(thread.id, { clearQueue: false, continueQueue: true });
16033
- }
16034
- void this.drainQueue(thread.id);
16035
16217
  }
16218
+ void this.drainQueue(thread.id);
16036
16219
  return this.requireThread(thread.id);
16037
16220
  }
16038
16221
  async drainQueue(threadId) {
@@ -16061,7 +16244,18 @@ var Orchestrator = class {
16061
16244
  }
16062
16245
  const livePid = thread.agentPid;
16063
16246
  if (typeof livePid === "number" && livePid > 0 && isPidAlive(livePid)) {
16064
- 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
+ }
16065
16259
  continue;
16066
16260
  }
16067
16261
  const prompt = thread.queue[0];
@@ -16218,6 +16412,7 @@ var Orchestrator = class {
16218
16412
  { cachedPrefix, prompt: agentPrompt },
16219
16413
  (event) => {
16220
16414
  this.emit({ type: "turn_output", threadId, event });
16415
+ noteTurnLiveEvent(threadId, event);
16221
16416
  if (event.type === "session_id") {
16222
16417
  updateThread(threadId, { sessionId: event.data });
16223
16418
  }
@@ -16422,6 +16617,7 @@ var Orchestrator = class {
16422
16617
  this.processes.delete(`${threadId}:agent`);
16423
16618
  this.stoppedTurns.delete(threadId);
16424
16619
  this.runningCount = Math.max(0, this.runningCount - 1);
16620
+ clearTurnLive(threadId);
16425
16621
  try {
16426
16622
  updateThread(threadId, { agentPid: null });
16427
16623
  } catch {
@@ -16695,51 +16891,56 @@ var Orchestrator = class {
16695
16891
  }
16696
16892
  return created;
16697
16893
  }
16698
- async waitForTurn(threadRef, timeoutMs = 6e5) {
16894
+ async waitForTurn(threadRef, timeoutMs = 6e5, opts) {
16699
16895
  const thread = this.requireThread(threadRef);
16896
+ if (!["running", "queued"].includes(thread.status)) {
16897
+ return thread;
16898
+ }
16700
16899
  const start = Date.now();
16701
16900
  return new Promise((resolve, reject) => {
16702
- if (!["running", "queued"].includes(thread.status)) {
16703
- resolve(thread);
16704
- return;
16705
- }
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
+ };
16706
16910
  const off = this.on((event) => {
16707
16911
  if (event.type === "turn_finished" && event.threadId === thread.id) {
16708
- off();
16709
16912
  const latest = readThread(thread.id);
16710
16913
  if (!latest) {
16711
- reject(new Error(`Thread not found: ${thread.id}`));
16914
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
16712
16915
  return;
16713
16916
  }
16714
- resolve(latest);
16917
+ finish(() => resolve(latest));
16715
16918
  }
16716
16919
  if (event.type === "error" && event.threadId === thread.id) {
16717
- off();
16718
16920
  const latest = readThread(thread.id);
16719
16921
  if (!latest) {
16720
- reject(new Error(`Thread not found: ${thread.id}`));
16922
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
16721
16923
  return;
16722
16924
  }
16723
- resolve(latest);
16925
+ finish(() => resolve(latest));
16724
16926
  }
16725
16927
  });
16726
- const timer = setInterval(() => {
16727
- if (Date.now() - start > timeoutMs) {
16728
- clearInterval(timer);
16729
- off();
16730
- reject(new Error("wait_for_turn timed out"));
16731
- }
16928
+ timer = setInterval(() => {
16732
16929
  const current = readThread(thread.id);
16733
16930
  if (!current) {
16734
- clearInterval(timer);
16735
- off();
16736
- reject(new Error(`Thread not found: ${thread.id}`));
16931
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
16737
16932
  return;
16738
16933
  }
16739
16934
  if (!["running", "queued"].includes(current.status)) {
16740
- clearInterval(timer);
16741
- off();
16742
- 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
+ }
16743
16944
  }
16744
16945
  }, 200);
16745
16946
  });
@@ -16749,11 +16950,16 @@ var Orchestrator = class {
16749
16950
  const lastAgent = [...thread.messages].reverse().find((m) => m.role === "agent");
16750
16951
  const lastError = thread.lastError ?? null;
16751
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;
16752
16955
  return {
16753
16956
  text: text3,
16754
16957
  status: thread.status,
16755
16958
  sessionId: thread.sessionId,
16756
- lastError
16959
+ lastError,
16960
+ stillRunning,
16961
+ progress: live?.summary ?? null,
16962
+ lastActivityAt: live?.updatedAt ?? null
16757
16963
  };
16758
16964
  }
16759
16965
  assertNotGlobal(thread, action) {
@@ -17179,7 +17385,19 @@ var Orchestrator = class {
17179
17385
  const thread = this.requireThread(threadRef);
17180
17386
  return threadsSharingWorktree(thread.worktreePath);
17181
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
+ }
17182
17396
  async archive(threadRef) {
17397
+ const thread = this.requireThread(threadRef);
17398
+ return this.enqueueWorktreeTeardown(thread, () => this.archiveUnlocked(threadRef));
17399
+ }
17400
+ async archiveUnlocked(threadRef) {
17183
17401
  const thread = this.requireThread(threadRef);
17184
17402
  this.stop(thread.id);
17185
17403
  this.releaseOrchestratorCaffeinate(thread);
@@ -17215,6 +17433,10 @@ var Orchestrator = class {
17215
17433
  return archived;
17216
17434
  }
17217
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) {
17218
17440
  const thread = this.requireThread(threadRef);
17219
17441
  this.stop(thread.id);
17220
17442
  this.releaseOrchestratorCaffeinate(thread);
@@ -17249,13 +17471,12 @@ var Orchestrator = class {
17249
17471
  this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
17250
17472
  return restored2;
17251
17473
  }
17252
- if (!(0, import_node_fs42.existsSync)(thread.worktreePath)) {
17474
+ if (!(0, import_node_fs43.existsSync)(thread.worktreePath)) {
17253
17475
  const { createThreadWorktree: createThreadWorktree2 } = await Promise.resolve().then(() => (init_worktree(), worktree_exports));
17254
- const { execa: execa7 } = await import("execa");
17255
17476
  const slug = thread.worktreePath.split("/").pop();
17256
17477
  const dest = thread.worktreePath;
17257
- await execa7("git", ["worktree", "add", dest, thread.branchName], {
17258
- cwd: thread.repoPath
17478
+ await withRepoGitLock(thread.repoPath, async () => {
17479
+ await git(["worktree", "add", dest, thread.branchName], thread.repoPath);
17259
17480
  });
17260
17481
  void createThreadWorktree2;
17261
17482
  void slug;
@@ -17507,6 +17728,15 @@ function mcpArchiveBlockedReason(thread) {
17507
17728
  // src/mcp/server.ts
17508
17729
  init_profile();
17509
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
+
17510
17740
  // src/mcp/slack-tools.ts
17511
17741
  var import_zod = require("zod");
17512
17742
 
@@ -18178,7 +18408,9 @@ async function startMcpServer() {
18178
18408
  const threads = orch.getThreads(true);
18179
18409
  const lines = threads.map((t) => {
18180
18410
  const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : (0, import_node_path39.basename)(t.repoPath) || t.repoPath;
18181
- 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}`;
18182
18414
  });
18183
18415
  return {
18184
18416
  content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
@@ -18187,13 +18419,14 @@ async function startMcpServer() {
18187
18419
  );
18188
18420
  server.tool(
18189
18421
  "get_thread",
18190
- "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.",
18191
18423
  { ref: import_zod3.z.string() },
18192
18424
  async ({ ref }) => {
18193
18425
  const t = orch.getThread(ref);
18194
18426
  if (!t) {
18195
18427
  return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
18196
18428
  }
18429
+ const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
18197
18430
  const summary = {
18198
18431
  id: t.id,
18199
18432
  title: t.title,
@@ -18208,7 +18441,10 @@ async function startMcpServer() {
18208
18441
  messageCount: t.messages.length,
18209
18442
  devPort: t.devPort,
18210
18443
  prUrl: t.prUrl,
18211
- 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
18212
18448
  };
18213
18449
  return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
18214
18450
  }
@@ -18534,13 +18770,15 @@ async function startMcpServer() {
18534
18770
  );
18535
18771
  server.tool(
18536
18772
  "wait_for_turn",
18537
- "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread or ask_git. Returns status, text, and lastError. If status is error, lastError/text is the failure \u2014 switch agent, tell the user, or retry; do not treat empty text as success.",
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.",
18538
18774
  {
18539
18775
  ref: import_zod3.z.string(),
18540
18776
  timeoutMs: import_zod3.z.number().optional()
18541
18777
  },
18542
18778
  async ({ ref, timeoutMs }) => {
18543
- const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
18779
+ const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
18780
+ resolveIfStillRunning: true
18781
+ });
18544
18782
  const result = orch.getTurnResult(thread.id);
18545
18783
  return {
18546
18784
  content: [
@@ -18550,7 +18788,11 @@ async function startMcpServer() {
18550
18788
  id: thread.id,
18551
18789
  status: result.status,
18552
18790
  text: result.text,
18553
- lastError: result.lastError
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
18554
18796
  })
18555
18797
  }
18556
18798
  ]
@@ -18559,11 +18801,21 @@ async function startMcpServer() {
18559
18801
  );
18560
18802
  server.tool(
18561
18803
  "get_turn_result",
18562
- "Final assistant message (and lastError when the turn failed). Not the full transcript.",
18804
+ "Assistant message when the turn finished, or live progress while stillRunning. Not the full transcript.",
18563
18805
  { ref: import_zod3.z.string() },
18564
18806
  async ({ ref }) => {
18565
18807
  const result = orch.getTurnResult(ref);
18566
- 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
+ };
18567
18819
  }
18568
18820
  );
18569
18821
  server.tool(
@@ -18680,7 +18932,7 @@ async function startMcpServer() {
18680
18932
  );
18681
18933
  server.tool(
18682
18934
  "request_review",
18683
- '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.',
18684
18936
  { ref: import_zod3.z.string().describe("Worktree thread id/ref to review") },
18685
18937
  async ({ ref }) => {
18686
18938
  try {
@@ -18708,7 +18960,7 @@ async function startMcpServer() {
18708
18960
  );
18709
18961
  server.tool(
18710
18962
  "ask_git",
18711
- "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.",
18712
18964
  {
18713
18965
  ref: import_zod3.z.string().describe("Worktree thread id/ref"),
18714
18966
  action: import_zod3.z.enum(AGENT_GIT_ACTIONS).describe(
@@ -18759,7 +19011,7 @@ async function startMcpServer() {
18759
19011
  );
18760
19012
  server.tool(
18761
19013
  "fork_worktree",
18762
- "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.",
18763
19015
  {
18764
19016
  ref: import_zod3.z.string().describe("Worktree thread id/ref to fork"),
18765
19017
  through_index: import_zod3.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -18804,7 +19056,7 @@ async function startMcpServer() {
18804
19056
  );
18805
19057
  server.tool(
18806
19058
  "fork_chat",
18807
- "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.",
18808
19060
  {
18809
19061
  ref: import_zod3.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
18810
19062
  through_index: import_zod3.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -19487,7 +19739,7 @@ init_connected_teams();
19487
19739
  init_injected_mcp();
19488
19740
 
19489
19741
  // src/agents/user-mcp-config.ts
19490
- var import_node_fs43 = require("fs");
19742
+ var import_node_fs44 = require("fs");
19491
19743
  var import_node_os12 = require("os");
19492
19744
  var import_node_path40 = require("path");
19493
19745
  init_paths();
@@ -19523,16 +19775,16 @@ function mergeSideboardIntoMcpServersJson(existing, sideboard) {
19523
19775
  }
19524
19776
  function writeMergedMcpServersJson(configPath, sideboard) {
19525
19777
  let existing = {};
19526
- if ((0, import_node_fs43.existsSync)(configPath)) {
19778
+ if ((0, import_node_fs44.existsSync)(configPath)) {
19527
19779
  try {
19528
- existing = JSON.parse((0, import_node_fs43.readFileSync)(configPath, "utf8"));
19780
+ existing = JSON.parse((0, import_node_fs44.readFileSync)(configPath, "utf8"));
19529
19781
  } catch {
19530
19782
  existing = {};
19531
19783
  }
19532
19784
  }
19533
19785
  const next = mergeSideboardIntoMcpServersJson(existing, sideboard);
19534
- (0, import_node_fs43.mkdirSync)((0, import_node_path40.dirname)(configPath), { recursive: true });
19535
- (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)}
19536
19788
  `);
19537
19789
  }
19538
19790
  function launchFromResolved(server) {
@@ -19550,7 +19802,7 @@ async function registerPackagedUserMcpClients() {
19550
19802
  const cursor = userCursorMcpConfigPath();
19551
19803
  writeMergedMcpServersJson(cursor, launch);
19552
19804
  const claude = userClaudeMcpConfigPath();
19553
- if ((0, import_node_fs43.existsSync)(claude)) {
19805
+ if ((0, import_node_fs44.existsSync)(claude)) {
19554
19806
  writeMergedMcpServersJson(claude, launch);
19555
19807
  return { cursor, claude };
19556
19808
  }
@@ -20056,6 +20308,8 @@ function connectSession(ws, opts, log) {
20056
20308
 
20057
20309
  // src/slack/relay-protocol.ts
20058
20310
  var import_node_crypto11 = require("crypto");
20311
+ var SLACK_RELAY_PING_INTERVAL_MS = 2e4;
20312
+ var SLACK_RELAY_PONG_TIMEOUT_MS = 15e3;
20059
20313
  function slackRelaySessionKey(teamId, userId, deviceId) {
20060
20314
  return `${teamId.trim()}:${userId.trim()}:${deviceId.trim()}`;
20061
20315
  }
@@ -20241,10 +20495,21 @@ function connectSession2(ws, opts, log) {
20241
20495
  const allowedUsers = new Set(
20242
20496
  opts.workspaces.map((w) => `${w.teamId.trim()}:${w.userId.trim()}`)
20243
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
+ };
20244
20508
  const finish = (err) => {
20245
20509
  if (settled) return;
20246
20510
  settled = true;
20247
20511
  pending.clear();
20512
+ stopKeepalive();
20248
20513
  opts.signal?.removeEventListener("abort", onAbort);
20249
20514
  if (err) reject(err);
20250
20515
  else resolve();
@@ -20257,6 +20522,25 @@ function connectSession2(ws, opts, log) {
20257
20522
  finish();
20258
20523
  };
20259
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
+ };
20260
20544
  ws.addEventListener("open", () => {
20261
20545
  const label = opts.deviceLabel?.trim() || opts.deviceId.trim();
20262
20546
  log(`Relay connected \xB7 ${label}`);
@@ -20271,6 +20555,8 @@ function connectSession2(ws, opts, log) {
20271
20555
  userToken: wsInfo.userToken
20272
20556
  });
20273
20557
  }
20558
+ pingOnce();
20559
+ pingTimer = setInterval(pingOnce, pingMs);
20274
20560
  });
20275
20561
  ws.addEventListener("error", () => {
20276
20562
  finish(new Error("WebSocket error"));
@@ -20282,7 +20568,13 @@ function connectSession2(ws, opts, log) {
20282
20568
  const raw = typeof ev.data === "string" ? ev.data : String(ev.data ?? "");
20283
20569
  const msg = parseSlackRelayServerMessage(raw);
20284
20570
  if (!msg) return;
20285
- if (msg.type === "pong") return;
20571
+ if (msg.type === "pong") {
20572
+ if (pongTimer) {
20573
+ clearTimeout(pongTimer);
20574
+ pongTimer = null;
20575
+ }
20576
+ return;
20577
+ }
20286
20578
  if (msg.type === "error") {
20287
20579
  log(`relay error: ${msg.message}`);
20288
20580
  return;
@@ -20405,6 +20697,13 @@ function slackReplyThreadTs(msg) {
20405
20697
  if (msg.kind === "mention") return msg.ts;
20406
20698
  return void 0;
20407
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
+ }
20408
20707
  var SLACK_SEEN_REACTION = "eyes";
20409
20708
  function writeTokenForTeam(teamId) {
20410
20709
  const workspaces = listSlackWorkspacesRaw();
@@ -20441,8 +20740,8 @@ async function ackSlackInboundSeen(msg, opts) {
20441
20740
  log(`react error: ${errMsg}${hint}`);
20442
20741
  }
20443
20742
  }
20444
- async function postSlackText(target, text3, opts) {
20445
- const stub = {
20743
+ function replyStub(target) {
20744
+ return {
20446
20745
  teamId: target.teamId,
20447
20746
  channelId: target.channelId,
20448
20747
  ts: target.threadTs ?? "",
@@ -20450,12 +20749,15 @@ async function postSlackText(target, text3, opts) {
20450
20749
  text: "",
20451
20750
  kind: "dm"
20452
20751
  };
20752
+ }
20753
+ async function postSlackText(target, text3, opts) {
20453
20754
  if (opts.postReply) {
20454
- await opts.postReply(stub, text3);
20455
- 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;
20456
20758
  }
20457
20759
  const token = writeTokenForTeam(target.teamId);
20458
- await slackApi(
20760
+ const json = await slackApi(
20459
20761
  token,
20460
20762
  "chat.postMessage",
20461
20763
  {
@@ -20465,18 +20767,163 @@ async function postSlackText(target, text3, opts) {
20465
20767
  },
20466
20768
  opts.fetchImpl
20467
20769
  );
20770
+ const ts = json.ts?.trim();
20771
+ return ts ? { ts } : null;
20468
20772
  }
20469
- async function postSlackReply(msg, text3, opts) {
20470
- 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",
20471
20782
  {
20472
- teamId: msg.teamId,
20473
- channelId: msg.channelId,
20474
- threadTs: slackReplyThreadTs(msg)
20783
+ channel: target.channelId,
20784
+ ts,
20785
+ text: text3
20475
20786
  },
20476
- signForThisMac(text3),
20477
- 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
20478
20804
  );
20479
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
+ }
20480
20927
  var lastRelayed = /* @__PURE__ */ new Map();
20481
20928
  function markRelayed(threadId, text3) {
20482
20929
  lastRelayed.set(threadId, `${threadId}:${text3}`);
@@ -20568,10 +21015,15 @@ async function handleSlackInbound(msg, opts) {
20568
21015
  });
20569
21016
  };
20570
21017
  bindReplyTarget(fresh.id);
21018
+ const progress = startSlackTurnProgress(msg, () => fresh.id, opts);
20571
21019
  const runTurn = async (threadId) => {
20572
21020
  await orch.send(threadId, prompt);
20573
21021
  await orch.waitForTurn(threadId, 14 * 60 * 1e3);
20574
21022
  };
21023
+ const dropProgress = async (reason) => {
21024
+ log(reason);
21025
+ await progress.discard();
21026
+ };
20575
21027
  let reply;
20576
21028
  try {
20577
21029
  try {
@@ -20584,12 +21036,12 @@ async function handleSlackInbound(msg, opts) {
20584
21036
  await runTurn(fresh.id);
20585
21037
  }
20586
21038
  if (slackInboundSuperseded(opts)) {
20587
- log(`turn finished ${msg.ts} (superseded)`);
21039
+ await dropProgress(`turn finished ${msg.ts} (superseded)`);
20588
21040
  return;
20589
21041
  }
20590
21042
  let after = readThread(fresh.id);
20591
21043
  if (after?.status === "stopped") {
20592
- log(`turn finished ${msg.ts} (interrupted, skip post)`);
21044
+ await dropProgress(`turn finished ${msg.ts} (interrupted, skip post)`);
20593
21045
  return;
20594
21046
  }
20595
21047
  if (!after || after.status === "archived") {
@@ -20598,46 +21050,48 @@ async function handleSlackInbound(msg, opts) {
20598
21050
  bindReplyTarget(fresh.id);
20599
21051
  await runTurn(fresh.id);
20600
21052
  if (slackInboundSuperseded(opts)) {
20601
- log(`turn finished ${msg.ts} (superseded)`);
21053
+ await dropProgress(`turn finished ${msg.ts} (superseded)`);
20602
21054
  return;
20603
21055
  }
20604
21056
  after = readThread(fresh.id);
20605
21057
  if (!after || after.status === "stopped" || after.status === "archived") {
20606
- log(`turn finished ${msg.ts} (interrupted, skip post)`);
21058
+ await dropProgress(`turn finished ${msg.ts} (interrupted, skip post)`);
20607
21059
  return;
20608
21060
  }
20609
21061
  }
20610
21062
  reply = orch.getTurnResult(fresh.id).text.trim();
20611
21063
  } catch (err) {
20612
21064
  if (slackInboundSuperseded(opts)) {
20613
- log(`turn error ${msg.ts} (superseded)`);
21065
+ await dropProgress(`turn error ${msg.ts} (superseded)`);
20614
21066
  return;
20615
21067
  }
20616
21068
  const errMsg = err instanceof Error ? err.message : String(err);
20617
21069
  if (slackCoordinatorGone(err)) {
20618
- log(`turn finished ${msg.ts} (thread gone, skip post)`);
21070
+ await dropProgress(`turn finished ${msg.ts} (thread gone, skip post)`);
20619
21071
  return;
20620
21072
  }
20621
21073
  const timedOut = /timed out|timeout/i.test(errMsg);
20622
21074
  const errorReply = timedOut ? SLACK_LISTEN_TIMEOUT_REPLY : `Sideboard coordinator error: ${errMsg}`;
20623
- await postSlackReply(msg, errorReply, opts).catch((postErr) => {
21075
+ try {
21076
+ await progress.finishWith(errorReply);
21077
+ log(`replied error ${msg.ts}: ${errMsg}`);
21078
+ } catch (postErr) {
20624
21079
  const postMsg = postErr instanceof Error ? postErr.message : String(postErr);
20625
21080
  log(`post error: ${postMsg}`);
20626
- });
20627
- log(`replied error ${msg.ts}: ${errMsg}`);
21081
+ }
20628
21082
  if (!timedOut) throw err;
20629
21083
  return;
20630
21084
  }
20631
21085
  if (!reply) {
20632
- log(`post error: no agent text to send for ${msg.ts}`);
21086
+ await dropProgress(`post error: no agent text to send for ${msg.ts}`);
20633
21087
  return;
20634
21088
  }
20635
21089
  if (alreadyRelayed(fresh.id, reply)) {
20636
- log(`turn finished ${msg.ts} (already posted)`);
21090
+ await dropProgress(`turn finished ${msg.ts} (already posted)`);
20637
21091
  return;
20638
21092
  }
20639
21093
  try {
20640
- await postSlackReply(msg, reply, opts);
21094
+ await progress.finishWith(reply);
20641
21095
  markRelayed(fresh.id, reply);
20642
21096
  log(`replied ${msg.ts} (${reply.length} chars)`);
20643
21097
  } catch (err) {
@@ -20964,7 +21418,7 @@ var import_node_http3 = require("http");
20964
21418
  var import_ws3 = require("ws");
20965
21419
 
20966
21420
  // src/slack/relay-static.ts
20967
- var import_node_fs44 = require("fs");
21421
+ var import_node_fs45 = require("fs");
20968
21422
  var import_promises = require("fs/promises");
20969
21423
  var import_node_path41 = __toESM(require("path"), 1);
20970
21424
  var TYPES = {
@@ -21023,7 +21477,7 @@ function sendFile(req, res, file, size) {
21023
21477
  res.end();
21024
21478
  return true;
21025
21479
  }
21026
- (0, import_node_fs44.createReadStream)(file).pipe(res);
21480
+ (0, import_node_fs45.createReadStream)(file).pipe(res);
21027
21481
  return true;
21028
21482
  }
21029
21483
  async function tryServeStatic(req, res, root) {
@@ -21188,6 +21642,20 @@ async function startSlackRelayServer(opts) {
21188
21642
  }
21189
21643
  }
21190
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);
21191
21659
  log("desktop connected");
21192
21660
  ws.on("message", (data) => {
21193
21661
  const raw = typeof data === "string" ? data : data.toString("utf8");
@@ -21299,6 +21767,8 @@ async function startSlackRelayServer(opts) {
21299
21767
  SLACK_LISTEN_TIMEOUT_REPLY,
21300
21768
  SLACK_OAUTH_CANCELLED,
21301
21769
  SLACK_OAUTH_REDIRECT,
21770
+ SLACK_PROGRESS_DELAY_MS,
21771
+ SLACK_PROGRESS_EDIT_MS,
21302
21772
  SLACK_REPLY_FORMATTING,
21303
21773
  SLACK_SEEN_REACTION,
21304
21774
  SlackOAuthCancelledError,
@@ -21456,6 +21926,7 @@ async function startSlackRelayServer(opts) {
21456
21926
  formatSlackInboundPrompt,
21457
21927
  formatSlackRepliesForTurn,
21458
21928
  formatSlackSignedReply,
21929
+ formatSlackWorkingText,
21459
21930
  formatTranscriptMarkdown,
21460
21931
  formatUiReminder,
21461
21932
  formatWorkspaceInventory,
@@ -21751,6 +22222,7 @@ async function startSlackRelayServer(opts) {
21751
22222
  thisProcessShouldDrainAgentQueues,
21752
22223
  threadDisplayLabel,
21753
22224
  threadFilePath,
22225
+ threadLivePath,
21754
22226
  threadLockPath,
21755
22227
  threadRequestsBrightsyMcp,
21756
22228
  threadsDir,
@@ -21774,6 +22246,7 @@ async function startSlackRelayServer(opts) {
21774
22246
  userClaudeMcpConfigPath,
21775
22247
  userCursorMcpConfigPath,
21776
22248
  validateLinearApiKey,
22249
+ waitForPidExit,
21777
22250
  warmGithubAgentAuth,
21778
22251
  withAgentInstructions,
21779
22252
  withExportedPath,