@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.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  listWorkspaces,
5
5
  removeWorkspace,
6
6
  syncWorkspacesFromThreads
7
- } from "./chunk-MHJV4WS3.js";
7
+ } from "./chunk-27MCESES.js";
8
8
  import {
9
9
  BRIGHTSY_MCP_ALLOWED_TOOLS,
10
10
  CLAUDE_MODEL_CATALOG,
@@ -65,7 +65,7 @@ import {
65
65
  threadRequestsBrightsyMcp,
66
66
  totalTokens,
67
67
  writeInjectedMcpConfig
68
- } from "./chunk-OXC3MEFI.js";
68
+ } from "./chunk-H6LP57EF.js";
69
69
  import {
70
70
  CLOUD_COORDINATOR_BUSY_REPLY,
71
71
  CLOUD_COORDINATOR_STOPPED_REPLY,
@@ -93,7 +93,7 @@ import {
93
93
  parseForceStopMessage,
94
94
  slackCoordinatorSourceRef,
95
95
  takenTeamSlugsForOrchestration
96
- } from "./chunk-GR4JKWR4.js";
96
+ } from "./chunk-HHLZWO7U.js";
97
97
  import {
98
98
  COORDINATOR_TOOL_PLAYBOOK,
99
99
  SLACK_REPLY_FORMATTING,
@@ -102,7 +102,7 @@ import {
102
102
  enrichWorkspacesWithGithub,
103
103
  ensureGlobalCoordinatorCwd,
104
104
  formatWorkspaceInventory
105
- } from "./chunk-HYKZEP5A.js";
105
+ } from "./chunk-ETMRBJKA.js";
106
106
  import {
107
107
  brightsyConfigPath,
108
108
  brightsyMcpServerName,
@@ -114,7 +114,7 @@ import {
114
114
  loadBrightsyConfig,
115
115
  saveBrightsyConfig,
116
116
  switchBrightsyAccount
117
- } from "./chunk-QSLE4VEM.js";
117
+ } from "./chunk-2GO3YKBA.js";
118
118
  import {
119
119
  LEGACY_PLAN_FILE_REL,
120
120
  PLAN_FILE_NAME,
@@ -147,7 +147,7 @@ import {
147
147
  isThreadCaffeinated,
148
148
  releaseCaffeinateHoldForThread,
149
149
  setCaffeinateHold
150
- } from "./chunk-NZPGMNFC.js";
150
+ } from "./chunk-DCOZABNT.js";
151
151
  import {
152
152
  FAMOUS_SOCCER_TEAMS,
153
153
  addPrStackLayer,
@@ -167,6 +167,7 @@ import {
167
167
  currentBranch,
168
168
  detectGhStack,
169
169
  detectLocalMergeConflicts,
170
+ enqueueByKey,
170
171
  ensureGhPreferOrigin,
171
172
  extractGhErrorDetail,
172
173
  fetchPrHead,
@@ -226,10 +227,11 @@ import {
226
227
  takenSlugsFromThread,
227
228
  threadDisplayLabel,
228
229
  warmGithubAgentAuth,
230
+ withRepoGitLock,
229
231
  worktreeDisplayLabel,
230
232
  worktreeDisplayLabelForGroup,
231
233
  worktreeNameFromPath
232
- } from "./chunk-CIRXAYWS.js";
234
+ } from "./chunk-DHAGMJIK.js";
233
235
  import {
234
236
  ATTACHMENTS_DIR,
235
237
  LEGACY_ATTACHMENTS_DIR,
@@ -298,7 +300,7 @@ import {
298
300
  updateIntegrationsSettings,
299
301
  updateOpencodeSettings,
300
302
  writeSecureJson
301
- } from "./chunk-FT2SQOL4.js";
303
+ } from "./chunk-XHZJJWLI.js";
302
304
  import {
303
305
  stripNestedElectronEnv
304
306
  } from "./chunk-TLPJHLLM.js";
@@ -318,7 +320,7 @@ import {
318
320
  updateThread,
319
321
  withThreadLock,
320
322
  writeThread
321
- } from "./chunk-3WA37BRI.js";
323
+ } from "./chunk-KGSVLZV6.js";
322
324
  import {
323
325
  THINKING_EFFORTS,
324
326
  isThinkingEffort,
@@ -349,11 +351,12 @@ import {
349
351
  sideboardReposDir,
350
352
  sideboardWorkspacesDir,
351
353
  threadFilePath,
354
+ threadLivePath,
352
355
  threadLockPath,
353
356
  threadsDir,
354
357
  workspaceSettingsSourceLabel,
355
358
  worktreesRoot
356
- } from "./chunk-B4R2VB2A.js";
359
+ } from "./chunk-QZQEXME2.js";
357
360
  import {
358
361
  conductorBundledBinDir,
359
362
  enrichPathWithNpmGlobalBin,
@@ -1386,9 +1389,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
1386
1389
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
1387
1390
  );
1388
1391
  }
1389
- const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-KYEBFWKB.js");
1392
+ const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-UR4E3CCG.js");
1390
1393
  if (isGlobalThread2(thread)) {
1391
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-HHRKQWCX.js");
1394
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-UWHSD65V.js");
1392
1395
  ensureGlobalCoordinatorCwd2(
1393
1396
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
1394
1397
  );
@@ -3267,11 +3270,11 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
3267
3270
  }
3268
3271
  return out;
3269
3272
  }
3270
- function stageBuffersAsAttachments(worktreePath, buffers) {
3271
- if (buffers.length === 0) return [];
3273
+ function stageBuffersAsAttachments(worktreePath, buffers2) {
3274
+ if (buffers2.length === 0) return [];
3272
3275
  const dir = ensureAttachmentsDir(worktreePath);
3273
3276
  const out = [];
3274
- for (const item of buffers) {
3277
+ for (const item of buffers2) {
3275
3278
  const originalName = (item.name || "file").replace(/[/\\]/g, "_") || "file";
3276
3279
  try {
3277
3280
  const buf = Buffer.from(item.dataBase64, "base64");
@@ -3291,8 +3294,8 @@ function stageBuffersAsAttachments(worktreePath, buffers) {
3291
3294
  }
3292
3295
  return out;
3293
3296
  }
3294
- function attachmentsFromBuffers(buffers) {
3295
- return buffers.map((item) => {
3297
+ function attachmentsFromBuffers(buffers2) {
3298
+ return buffers2.map((item) => {
3296
3299
  const name = (item.name || "file").replace(/[/\\]/g, "_") || "file";
3297
3300
  try {
3298
3301
  const buf = Buffer.from(item.dataBase64, "base64");
@@ -3881,7 +3884,7 @@ async function createThread(input, _onSetupLine) {
3881
3884
  return readThread(thread.id) ?? thread;
3882
3885
  }
3883
3886
  async function listLinearIssues(agent, repoPath) {
3884
- const { getAdapter: getAdapter2 } = await import("./agents-OXZDMPSE.js");
3887
+ const { getAdapter: getAdapter2 } = await import("./agents-XZBAKBO2.js");
3885
3888
  await requireAgent(agent, { requireLinear: true });
3886
3889
  const adapter = getAdapter2(agent);
3887
3890
  if (!adapter.listLinearIssues) {
@@ -3932,8 +3935,11 @@ function worktreeBindingFrom(from) {
3932
3935
  };
3933
3936
  }
3934
3937
  function threadsSharingWorktree(worktreePath) {
3938
+ if (!worktreePath) return [];
3935
3939
  const key = normalizeWorktreePath(worktreePath);
3936
- return listThreads({ includeArchived: true }).filter((t) => sameWorktreePath(t.worktreePath, key) && t.status !== "archived").sort((a, b) => a.createdAt.localeCompare(b.createdAt));
3940
+ return listThreads({ includeArchived: true }).filter(
3941
+ (t) => Boolean(t.worktreePath) && sameWorktreePath(t.worktreePath, key) && t.status !== "archived"
3942
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
3937
3943
  }
3938
3944
  function formatTranscriptMarkdown(title, messages) {
3939
3945
  const body = formatMessagesAsTranscript(messages, { tools: "full" });
@@ -4434,7 +4440,7 @@ async function adoptThread(input) {
4434
4440
  messages: input.messages ?? []
4435
4441
  });
4436
4442
  writeThread(thread);
4437
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-NNPD7QVV.js");
4443
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-TKJXPNV4.js");
4438
4444
  await ensureWorkspace2(repoPath);
4439
4445
  return thread;
4440
4446
  }
@@ -5200,7 +5206,7 @@ async function refreshSlackReplyBadges(opts) {
5200
5206
  }
5201
5207
 
5202
5208
  // src/orchestrator/orchestrator.ts
5203
- import { existsSync as existsSync14 } from "fs";
5209
+ import { existsSync as existsSync15 } from "fs";
5204
5210
 
5205
5211
  // src/git/pr-merge-archive.ts
5206
5212
  function normalizePrState(state) {
@@ -5445,9 +5451,132 @@ async function cloneRepoIntoSideboard(opts) {
5445
5451
  return { repoPath, workspace };
5446
5452
  }
5447
5453
 
5454
+ // src/store/turn-live.ts
5455
+ import {
5456
+ existsSync as existsSync13,
5457
+ renameSync,
5458
+ unlinkSync as unlinkSync2,
5459
+ writeFileSync as writeFileSync4,
5460
+ readFileSync as readFileSync10
5461
+ } from "fs";
5462
+ var buffers = /* @__PURE__ */ new Map();
5463
+ var FLUSH_MS = 800;
5464
+ var MAX_PARTS = 80;
5465
+ function trimParts(parts) {
5466
+ if (parts.length <= MAX_PARTS) return parts;
5467
+ return parts.slice(-Math.floor(MAX_PARTS / 2));
5468
+ }
5469
+ function lastText(parts, type) {
5470
+ for (let i = parts.length - 1; i >= 0; i--) {
5471
+ const p = parts[i];
5472
+ if (p.type === type && p.text.trim()) return p.text.trim();
5473
+ }
5474
+ return "";
5475
+ }
5476
+ function summarizeTurnLive(parts) {
5477
+ const tools = parts.filter(
5478
+ (p) => p.type === "tool"
5479
+ );
5480
+ const lastTool = tools[tools.length - 1];
5481
+ const lastToolLabel = lastTool ? lastTool.description || toolDescription(lastTool.name, lastTool.input) || lastTool.name : void 0;
5482
+ const running = lastTool?.status === "running";
5483
+ const thinking = lastText(parts, "thinking");
5484
+ const text3 = lastText(parts, "text");
5485
+ const excerptRaw = thinking || text3;
5486
+ const excerpt = excerptRaw.length > 280 ? `${excerptRaw.slice(-280)}` : excerptRaw || void 0;
5487
+ let summary = "Working\u2026";
5488
+ if (lastToolLabel) {
5489
+ const verb = running ? lastToolLabel : `Finished ${lastToolLabel}`;
5490
+ summary = tools.length > 1 ? `${verb} (${tools.length} tools)` : verb;
5491
+ } else if (thinking) {
5492
+ summary = "Thinking\u2026";
5493
+ } else if (text3) {
5494
+ summary = "Writing reply\u2026";
5495
+ }
5496
+ return {
5497
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5498
+ summary,
5499
+ lastTool: lastToolLabel,
5500
+ toolCount: tools.length,
5501
+ excerpt
5502
+ };
5503
+ }
5504
+ function flush(threadId) {
5505
+ const buf = buffers.get(threadId);
5506
+ if (!buf) return;
5507
+ if (buf.timer) {
5508
+ clearTimeout(buf.timer);
5509
+ buf.timer = null;
5510
+ }
5511
+ writeTurnLive(threadId, summarizeTurnLive(buf.parts));
5512
+ }
5513
+ function scheduleFlush(threadId) {
5514
+ const buf = buffers.get(threadId);
5515
+ if (!buf) return;
5516
+ if (buf.timer) return;
5517
+ buf.timer = setTimeout(() => {
5518
+ const live = buffers.get(threadId);
5519
+ if (live) live.timer = null;
5520
+ flush(threadId);
5521
+ }, FLUSH_MS);
5522
+ }
5523
+ function noteTurnLiveEvent(threadId, event) {
5524
+ if (event.type === "session_id" || event.type === "usage" || event.type === "stderr") {
5525
+ return;
5526
+ }
5527
+ const prev = buffers.get(threadId) ?? { parts: [], timer: null };
5528
+ prev.parts = trimParts(applyAgentEvent(prev.parts, event));
5529
+ buffers.set(threadId, prev);
5530
+ if (event.type === "exit") {
5531
+ flush(threadId);
5532
+ const cur = buffers.get(threadId);
5533
+ if (cur?.timer) clearTimeout(cur.timer);
5534
+ buffers.delete(threadId);
5535
+ return;
5536
+ }
5537
+ const immediate = event.type === "tool_use" || event.type === "tool_result";
5538
+ if (immediate) flush(threadId);
5539
+ else scheduleFlush(threadId);
5540
+ }
5541
+ function writeTurnLive(threadId, progress) {
5542
+ const path2 = threadLivePath(threadId);
5543
+ const tmp = `${path2}.${process.pid}.tmp`;
5544
+ try {
5545
+ writeFileSync4(tmp, JSON.stringify(progress), "utf8");
5546
+ renameSync(tmp, path2);
5547
+ } catch {
5548
+ try {
5549
+ unlinkSync2(tmp);
5550
+ } catch {
5551
+ }
5552
+ }
5553
+ }
5554
+ function readTurnLive(threadId) {
5555
+ const path2 = threadLivePath(threadId);
5556
+ if (!existsSync13(path2)) return null;
5557
+ try {
5558
+ const raw = JSON.parse(readFileSync10(path2, "utf8"));
5559
+ if (!raw || typeof raw.summary !== "string") return null;
5560
+ return raw;
5561
+ } catch {
5562
+ return null;
5563
+ }
5564
+ }
5565
+ function clearTurnLive(threadId) {
5566
+ const buf = buffers.get(threadId);
5567
+ if (buf?.timer) clearTimeout(buf.timer);
5568
+ buffers.delete(threadId);
5569
+ const path2 = threadLivePath(threadId);
5570
+ if (!existsSync13(path2)) return;
5571
+ try {
5572
+ unlinkSync2(path2);
5573
+ } catch {
5574
+ }
5575
+ }
5576
+
5448
5577
  // src/review/request-review.ts
5449
5578
  import { randomUUID as randomUUID4 } from "crypto";
5450
- import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
5579
+ import { existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
5451
5580
  import { dirname as dirname3, join as join13 } from "path";
5452
5581
 
5453
5582
  // src/review/review-request-template.ts
@@ -5593,9 +5722,9 @@ function shouldRefreshReviewRequestTemplate(content) {
5593
5722
  return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
5594
5723
  }
5595
5724
  function readTextIfPresent(abs) {
5596
- if (!existsSync13(abs)) return null;
5725
+ if (!existsSync14(abs)) return null;
5597
5726
  try {
5598
- const content = readFileSync10(abs, "utf8");
5727
+ const content = readFileSync11(abs, "utf8");
5599
5728
  return content.trim() ? content : null;
5600
5729
  } catch {
5601
5730
  return null;
@@ -5603,9 +5732,9 @@ function readTextIfPresent(abs) {
5603
5732
  }
5604
5733
  function ensureAttachmentsGitignore(worktreePath) {
5605
5734
  const gitignoreAbs = join13(worktreePath, ATTACHMENTS_DIR, ".gitignore");
5606
- if (existsSync13(gitignoreAbs)) return;
5735
+ if (existsSync14(gitignoreAbs)) return;
5607
5736
  mkdirSync4(dirname3(gitignoreAbs), { recursive: true });
5608
- writeFileSync4(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
5737
+ writeFileSync5(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
5609
5738
  }
5610
5739
  function resolveReviewGuidelines(worktreePath) {
5611
5740
  const repoAbs = join13(worktreePath, REPO_REVIEW_PATH);
@@ -5640,7 +5769,7 @@ function resolveReviewGuidelines(worktreePath) {
5640
5769
  }
5641
5770
  ensureAttachmentsGitignore(worktreePath);
5642
5771
  mkdirSync4(dirname3(localAbs), { recursive: true });
5643
- writeFileSync4(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
5772
+ writeFileSync5(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
5644
5773
  return {
5645
5774
  path: REVIEW_REQUEST_PATH,
5646
5775
  name: REVIEW_REQUEST_NAME,
@@ -5662,7 +5791,7 @@ function ensureReviewRequestFile(worktreePath) {
5662
5791
  const localAbs = join13(worktreePath, REVIEW_REQUEST_PATH);
5663
5792
  const localContent = readTextIfPresent(localAbs) ?? readTextIfPresent(join13(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
5664
5793
  if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
5665
- const path2 = existsSync13(localAbs) ? REVIEW_REQUEST_PATH : LEGACY_REVIEW_REQUEST_PATH;
5794
+ const path2 = existsSync14(localAbs) ? REVIEW_REQUEST_PATH : LEGACY_REVIEW_REQUEST_PATH;
5666
5795
  return {
5667
5796
  path: path2,
5668
5797
  name: REVIEW_REQUEST_NAME,
@@ -5671,7 +5800,7 @@ function ensureReviewRequestFile(worktreePath) {
5671
5800
  };
5672
5801
  }
5673
5802
  mkdirSync4(dirname3(repoAbs), { recursive: true });
5674
- writeFileSync4(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
5803
+ writeFileSync5(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
5675
5804
  return {
5676
5805
  path: REPO_REVIEW_PATH,
5677
5806
  name: REPO_REVIEW_NAME,
@@ -5864,6 +5993,15 @@ function isPidAlive(pid) {
5864
5993
  return false;
5865
5994
  }
5866
5995
  }
5996
+ async function waitForPidExit(pid, timeoutMs) {
5997
+ const start = Date.now();
5998
+ while (isPidAlive(pid)) {
5999
+ if (Date.now() - start >= timeoutMs) return false;
6000
+ await new Promise((r) => setTimeout(r, 80));
6001
+ }
6002
+ return true;
6003
+ }
6004
+ var STALE_AGENT_PID_WAIT_MS = 2500;
5867
6005
  function writeLiveStatus(threadId, status, lastError) {
5868
6006
  const latest = readThread(threadId);
5869
6007
  if (!latest || latest.status === "archived") return latest;
@@ -5941,7 +6079,7 @@ var Orchestrator = class {
5941
6079
  }
5942
6080
  continue;
5943
6081
  }
5944
- if (!existsSync14(thread.worktreePath)) {
6082
+ if (!existsSync15(thread.worktreePath)) {
5945
6083
  setStatus(thread.id, "broken", "Worktree missing on disk");
5946
6084
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
5947
6085
  continue;
@@ -5960,7 +6098,7 @@ var Orchestrator = class {
5960
6098
  orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
5961
6099
  });
5962
6100
  }
5963
- const { autoCleanupOrphansEnabled: autoCleanupOrphansEnabled2 } = await import("./app-settings-BRF7VKTQ.js");
6101
+ const { autoCleanupOrphansEnabled: autoCleanupOrphansEnabled2 } = await import("./app-settings-EKJM2344.js");
5964
6102
  if (autoCleanupOrphansEnabled2() && shouldRunWorktreeCleanup() && orphans.length > 0) {
5965
6103
  await cleanupOrphanWorktrees({ repoPaths });
5966
6104
  }
@@ -6128,7 +6266,7 @@ var Orchestrator = class {
6128
6266
  }
6129
6267
  }
6130
6268
  await setup;
6131
- const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await import("./app-settings-BRF7VKTQ.js");
6269
+ const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await import("./app-settings-EKJM2344.js");
6132
6270
  if (autoRunAfterSetupEnabled2()) {
6133
6271
  try {
6134
6272
  await this.startDev(threadId);
@@ -6262,14 +6400,10 @@ var Orchestrator = class {
6262
6400
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
6263
6401
  const livePid = current.agentPid;
6264
6402
  const foreignLive = !inFlight && typeof livePid === "number" && livePid > 0 && isPidAlive(livePid);
6265
- if (inFlight) {
6403
+ if (inFlight || foreignLive) {
6266
6404
  this.stop(thread.id, { clearQueue: false, continueQueue: true });
6267
- } else {
6268
- if (foreignLive) {
6269
- this.stop(thread.id, { clearQueue: false, continueQueue: true });
6270
- }
6271
- void this.drainQueue(thread.id);
6272
6405
  }
6406
+ void this.drainQueue(thread.id);
6273
6407
  return this.requireThread(thread.id);
6274
6408
  }
6275
6409
  async drainQueue(threadId) {
@@ -6298,7 +6432,18 @@ var Orchestrator = class {
6298
6432
  }
6299
6433
  const livePid = thread.agentPid;
6300
6434
  if (typeof livePid === "number" && livePid > 0 && isPidAlive(livePid)) {
6301
- await new Promise((r) => setTimeout(r, 200));
6435
+ const exited = await waitForPidExit(livePid, STALE_AGENT_PID_WAIT_MS);
6436
+ if (!exited && isPidAlive(livePid)) {
6437
+ try {
6438
+ process.kill(livePid, "SIGKILL");
6439
+ } catch {
6440
+ }
6441
+ await waitForPidExit(livePid, 400);
6442
+ try {
6443
+ updateThread(threadId, { agentPid: null });
6444
+ } catch {
6445
+ }
6446
+ }
6302
6447
  continue;
6303
6448
  }
6304
6449
  const prompt = thread.queue[0];
@@ -6406,7 +6551,7 @@ var Orchestrator = class {
6406
6551
  }
6407
6552
  const isBrightsy = fresh.agent === "brightsy";
6408
6553
  const isOrchestration = isOrchestratorThread(fresh);
6409
- const { autoRenameBranchEnabled: autoRenameBranchEnabled2, getGithubGitAuthMode: getGithubGitAuthMode2 } = await import("./app-settings-BRF7VKTQ.js");
6554
+ const { autoRenameBranchEnabled: autoRenameBranchEnabled2, getGithubGitAuthMode: getGithubGitAuthMode2 } = await import("./app-settings-EKJM2344.js");
6410
6555
  const gitAuthMode = getGithubGitAuthMode2();
6411
6556
  const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
6412
6557
  githubSlug: await resolveGithubRepoSlug(fresh.worktreePath).catch(
@@ -6455,6 +6600,7 @@ var Orchestrator = class {
6455
6600
  { cachedPrefix, prompt: agentPrompt },
6456
6601
  (event) => {
6457
6602
  this.emit({ type: "turn_output", threadId, event });
6603
+ noteTurnLiveEvent(threadId, event);
6458
6604
  if (event.type === "session_id") {
6459
6605
  updateThread(threadId, { sessionId: event.data });
6460
6606
  }
@@ -6501,7 +6647,7 @@ var Orchestrator = class {
6501
6647
  if (this.requireThread(threadId).agent === "cursor" && exitCode !== 0 && !assistantText && parts.length === 0) {
6502
6648
  const sessionId = result.sessionId || this.requireThread(threadId).sessionId || "";
6503
6649
  if (sessionId) {
6504
- const { recoverFinishedCursorRun } = await import("./cursor-recover-G34BYKL2.js");
6650
+ const { recoverFinishedCursorRun } = await import("./cursor-recover-ZUJG6XRQ.js");
6505
6651
  for (let i = 0; i < 8; i++) {
6506
6652
  const recovered = recoverFinishedCursorRun({
6507
6653
  agentId: sessionId,
@@ -6659,6 +6805,7 @@ var Orchestrator = class {
6659
6805
  this.processes.delete(`${threadId}:agent`);
6660
6806
  this.stoppedTurns.delete(threadId);
6661
6807
  this.runningCount = Math.max(0, this.runningCount - 1);
6808
+ clearTurnLive(threadId);
6662
6809
  try {
6663
6810
  updateThread(threadId, { agentPid: null });
6664
6811
  } catch {
@@ -6932,51 +7079,56 @@ var Orchestrator = class {
6932
7079
  }
6933
7080
  return created;
6934
7081
  }
6935
- async waitForTurn(threadRef, timeoutMs = 6e5) {
7082
+ async waitForTurn(threadRef, timeoutMs = 6e5, opts) {
6936
7083
  const thread = this.requireThread(threadRef);
7084
+ if (!["running", "queued"].includes(thread.status)) {
7085
+ return thread;
7086
+ }
6937
7087
  const start = Date.now();
6938
7088
  return new Promise((resolve, reject) => {
6939
- if (!["running", "queued"].includes(thread.status)) {
6940
- resolve(thread);
6941
- return;
6942
- }
7089
+ let settled = false;
7090
+ let timer;
7091
+ const finish = (fn) => {
7092
+ if (settled) return;
7093
+ settled = true;
7094
+ if (timer) clearInterval(timer);
7095
+ off();
7096
+ fn();
7097
+ };
6943
7098
  const off = this.on((event) => {
6944
7099
  if (event.type === "turn_finished" && event.threadId === thread.id) {
6945
- off();
6946
7100
  const latest = readThread(thread.id);
6947
7101
  if (!latest) {
6948
- reject(new Error(`Thread not found: ${thread.id}`));
7102
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
6949
7103
  return;
6950
7104
  }
6951
- resolve(latest);
7105
+ finish(() => resolve(latest));
6952
7106
  }
6953
7107
  if (event.type === "error" && event.threadId === thread.id) {
6954
- off();
6955
7108
  const latest = readThread(thread.id);
6956
7109
  if (!latest) {
6957
- reject(new Error(`Thread not found: ${thread.id}`));
7110
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
6958
7111
  return;
6959
7112
  }
6960
- resolve(latest);
7113
+ finish(() => resolve(latest));
6961
7114
  }
6962
7115
  });
6963
- const timer = setInterval(() => {
6964
- if (Date.now() - start > timeoutMs) {
6965
- clearInterval(timer);
6966
- off();
6967
- reject(new Error("wait_for_turn timed out"));
6968
- }
7116
+ timer = setInterval(() => {
6969
7117
  const current = readThread(thread.id);
6970
7118
  if (!current) {
6971
- clearInterval(timer);
6972
- off();
6973
- reject(new Error(`Thread not found: ${thread.id}`));
7119
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
6974
7120
  return;
6975
7121
  }
6976
7122
  if (!["running", "queued"].includes(current.status)) {
6977
- clearInterval(timer);
6978
- off();
6979
- resolve(current);
7123
+ finish(() => resolve(current));
7124
+ return;
7125
+ }
7126
+ if (Date.now() - start > timeoutMs) {
7127
+ if (opts?.resolveIfStillRunning) {
7128
+ finish(() => resolve(current));
7129
+ } else {
7130
+ finish(() => reject(new Error("wait_for_turn timed out")));
7131
+ }
6980
7132
  }
6981
7133
  }, 200);
6982
7134
  });
@@ -6986,11 +7138,16 @@ var Orchestrator = class {
6986
7138
  const lastAgent = [...thread.messages].reverse().find((m) => m.role === "agent");
6987
7139
  const lastError = thread.lastError ?? null;
6988
7140
  const text3 = (lastAgent?.text ?? "").trim() || (thread.status === "error" ? lastError ?? "" : "");
7141
+ const stillRunning = thread.status === "running" || thread.status === "queued";
7142
+ const live = stillRunning ? readTurnLive(thread.id) : null;
6989
7143
  return {
6990
7144
  text: text3,
6991
7145
  status: thread.status,
6992
7146
  sessionId: thread.sessionId,
6993
- lastError
7147
+ lastError,
7148
+ stillRunning,
7149
+ progress: live?.summary ?? null,
7150
+ lastActivityAt: live?.updatedAt ?? null
6994
7151
  };
6995
7152
  }
6996
7153
  assertNotGlobal(thread, action) {
@@ -7153,7 +7310,7 @@ var Orchestrator = class {
7153
7310
  updateThread(thread.id, { title: meta.title });
7154
7311
  }
7155
7312
  }
7156
- const { autoArchiveOnMergeEnabled: autoArchiveOnMergeEnabled2 } = await import("./app-settings-BRF7VKTQ.js");
7313
+ const { autoArchiveOnMergeEnabled: autoArchiveOnMergeEnabled2 } = await import("./app-settings-EKJM2344.js");
7157
7314
  const latest = this.requireThread(thread.id);
7158
7315
  if (!shouldAutoArchiveOnPrMerge({
7159
7316
  previousPrState: prevState || null,
@@ -7416,7 +7573,19 @@ var Orchestrator = class {
7416
7573
  const thread = this.requireThread(threadRef);
7417
7574
  return threadsSharingWorktree(thread.worktreePath);
7418
7575
  }
7576
+ /**
7577
+ * Last-tab teardown must see siblings already archived. Serialize per
7578
+ * worktree so parallel archive/purge of sibling tabs still removes once.
7579
+ */
7580
+ enqueueWorktreeTeardown(thread, fn) {
7581
+ const key = isGlobalThread(thread) ? `archive:global:${thread.id}` : `archive:wt:${normalizeWorktreePath(thread.worktreePath)}`;
7582
+ return enqueueByKey(key, fn);
7583
+ }
7419
7584
  async archive(threadRef) {
7585
+ const thread = this.requireThread(threadRef);
7586
+ return this.enqueueWorktreeTeardown(thread, () => this.archiveUnlocked(threadRef));
7587
+ }
7588
+ async archiveUnlocked(threadRef) {
7420
7589
  const thread = this.requireThread(threadRef);
7421
7590
  this.stop(thread.id);
7422
7591
  this.releaseOrchestratorCaffeinate(thread);
@@ -7444,7 +7613,7 @@ var Orchestrator = class {
7444
7613
  this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
7445
7614
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
7446
7615
  try {
7447
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-NNPD7QVV.js");
7616
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-TKJXPNV4.js");
7448
7617
  await ensureWorkspace2(thread.repoPath);
7449
7618
  } catch {
7450
7619
  }
@@ -7452,6 +7621,10 @@ var Orchestrator = class {
7452
7621
  return archived;
7453
7622
  }
7454
7623
  async purge(threadRef, opts) {
7624
+ const thread = this.requireThread(threadRef);
7625
+ await this.enqueueWorktreeTeardown(thread, () => this.purgeUnlocked(threadRef, opts));
7626
+ }
7627
+ async purgeUnlocked(threadRef, opts) {
7455
7628
  const thread = this.requireThread(threadRef);
7456
7629
  this.stop(thread.id);
7457
7630
  this.releaseOrchestratorCaffeinate(thread);
@@ -7466,7 +7639,7 @@ var Orchestrator = class {
7466
7639
  await runArchiveScript(thread.repoPath, thread.worktreePath);
7467
7640
  } catch {
7468
7641
  }
7469
- const { deleteBranchOnPurgeEnabled: deleteBranchOnPurgeEnabled2 } = await import("./app-settings-BRF7VKTQ.js");
7642
+ const { deleteBranchOnPurgeEnabled: deleteBranchOnPurgeEnabled2 } = await import("./app-settings-EKJM2344.js");
7470
7643
  const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled2();
7471
7644
  await removeWorktree(thread.repoPath, thread.worktreePath, {
7472
7645
  deleteBranch: deleteBranch ? thread.branchName : void 0
@@ -7480,19 +7653,18 @@ var Orchestrator = class {
7480
7653
  throw new Error("Thread is not archived");
7481
7654
  }
7482
7655
  if (isGlobalThread(thread)) {
7483
- const { globalAgentCwd: globalAgentCwd2 } = await import("./paths-JQM2XRAD.js");
7656
+ const { globalAgentCwd: globalAgentCwd2 } = await import("./paths-UMEDHOS5.js");
7484
7657
  updateThread(thread.id, { worktreePath: globalAgentCwd2() });
7485
7658
  const restored2 = setStatus(thread.id, "idle");
7486
7659
  this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
7487
7660
  return restored2;
7488
7661
  }
7489
- if (!existsSync14(thread.worktreePath)) {
7490
- const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-DA4BOV7G.js");
7491
- const { execa: execa5 } = await import("execa");
7662
+ if (!existsSync15(thread.worktreePath)) {
7663
+ const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-PEUPFAKL.js");
7492
7664
  const slug = thread.worktreePath.split("/").pop();
7493
7665
  const dest = thread.worktreePath;
7494
- await execa5("git", ["worktree", "add", dest, thread.branchName], {
7495
- cwd: thread.repoPath
7666
+ await withRepoGitLock(thread.repoPath, async () => {
7667
+ await git(["worktree", "add", dest, thread.branchName], thread.repoPath);
7496
7668
  });
7497
7669
  void createThreadWorktree2;
7498
7670
  void slug;
@@ -7596,7 +7768,7 @@ async function startOrchestration(opts) {
7596
7768
  sourceRef: "default",
7597
7769
  ...createOpts
7598
7770
  }).catch(async () => {
7599
- const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-DA4BOV7G.js");
7771
+ const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-PEUPFAKL.js");
7600
7772
  const repo = await resolveRepoRoot2(repoPath);
7601
7773
  const def = await resolveDefaultBranch2(repo);
7602
7774
  return orch.createThread({
@@ -7606,7 +7778,7 @@ async function startOrchestration(opts) {
7606
7778
  repoPath: repo
7607
7779
  });
7608
7780
  });
7609
- const { updateThread: upd } = await import("./thread-store-XAKWCV2E.js");
7781
+ const { updateThread: upd } = await import("./thread-store-4OUEQ2DB.js");
7610
7782
  const updated = upd(thread.id, {
7611
7783
  sourceType: "orchestration",
7612
7784
  sourceRef: goal
@@ -7729,6 +7901,15 @@ function mcpArchiveBlockedReason(thread) {
7729
7901
  return null;
7730
7902
  }
7731
7903
 
7904
+ // src/mcp/wait-for-turn.ts
7905
+ var MCP_WAIT_FOR_TURN_MAX_MS = 45e3;
7906
+ function mcpWaitForTurnTimeoutMs(requested) {
7907
+ const n = requested ?? MCP_WAIT_FOR_TURN_MAX_MS;
7908
+ if (!Number.isFinite(n)) return MCP_WAIT_FOR_TURN_MAX_MS;
7909
+ return Math.min(Math.max(1e3, Math.floor(n)), MCP_WAIT_FOR_TURN_MAX_MS);
7910
+ }
7911
+ 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.";
7912
+
7732
7913
  // src/mcp/slack-tools.ts
7733
7914
  import { z } from "zod";
7734
7915
 
@@ -8356,7 +8537,7 @@ async function startMcpServer() {
8356
8537
  );
8357
8538
  }
8358
8539
  try {
8359
- const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-BRF7VKTQ.js");
8540
+ const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-EKJM2344.js");
8360
8541
  orch.setMaxConcurrent(maxConcurrentAgents2());
8361
8542
  } catch {
8362
8543
  }
@@ -8399,7 +8580,9 @@ async function startMcpServer() {
8399
8580
  const threads = orch.getThreads(true);
8400
8581
  const lines = threads.map((t) => {
8401
8582
  const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : basename4(t.repoPath) || t.repoPath;
8402
- 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}` : ""}`;
8583
+ const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
8584
+ const progress = live?.summary ? ` ${live.summary}` : "";
8585
+ 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}`;
8403
8586
  });
8404
8587
  return {
8405
8588
  content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
@@ -8408,13 +8591,14 @@ async function startMcpServer() {
8408
8591
  );
8409
8592
  server.tool(
8410
8593
  "get_thread",
8411
- "Get a compact thread summary by id/ref",
8594
+ "Get a compact thread summary by id/ref. While running, includes progress (last tool/thinking) and lastActivityAt.",
8412
8595
  { ref: z3.string() },
8413
8596
  async ({ ref }) => {
8414
8597
  const t = orch.getThread(ref);
8415
8598
  if (!t) {
8416
8599
  return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
8417
8600
  }
8601
+ const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
8418
8602
  const summary = {
8419
8603
  id: t.id,
8420
8604
  title: t.title,
@@ -8429,7 +8613,10 @@ async function startMcpServer() {
8429
8613
  messageCount: t.messages.length,
8430
8614
  devPort: t.devPort,
8431
8615
  prUrl: t.prUrl,
8432
- lastError: t.lastError ?? null
8616
+ lastError: t.lastError ?? null,
8617
+ stillRunning: t.status === "running" || t.status === "queued",
8618
+ progress: live?.summary ?? null,
8619
+ lastActivityAt: live?.updatedAt ?? null
8433
8620
  };
8434
8621
  return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
8435
8622
  }
@@ -8565,8 +8752,8 @@ async function startMcpServer() {
8565
8752
  registerSlackTools(server);
8566
8753
  registerLinearTools(server);
8567
8754
  if (sideboardMcpProfile() !== "worktree") {
8568
- const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-I7SG6UQR.js");
8569
- const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-BRF7VKTQ.js");
8755
+ const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-X3JNPR42.js");
8756
+ const { resolveNewThreadOptions: resolveNewThreadOptions2, resolveThreadDefaults: resolveThreadDefaults2 } = await import("./app-settings-EKJM2344.js");
8570
8757
  const accountDefaults = resolveThreadDefaults2();
8571
8758
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
8572
8759
  server.tool(
@@ -8755,13 +8942,15 @@ async function startMcpServer() {
8755
8942
  );
8756
8943
  server.tool(
8757
8944
  "wait_for_turn",
8758
- "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.",
8945
+ "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.",
8759
8946
  {
8760
8947
  ref: z3.string(),
8761
8948
  timeoutMs: z3.number().optional()
8762
8949
  },
8763
8950
  async ({ ref, timeoutMs }) => {
8764
- const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
8951
+ const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
8952
+ resolveIfStillRunning: true
8953
+ });
8765
8954
  const result = orch.getTurnResult(thread.id);
8766
8955
  return {
8767
8956
  content: [
@@ -8771,7 +8960,11 @@ async function startMcpServer() {
8771
8960
  id: thread.id,
8772
8961
  status: result.status,
8773
8962
  text: result.text,
8774
- lastError: result.lastError
8963
+ lastError: result.lastError,
8964
+ stillRunning: result.stillRunning,
8965
+ progress: result.progress,
8966
+ lastActivityAt: result.lastActivityAt,
8967
+ hint: result.stillRunning ? MCP_WAIT_STILL_RUNNING_HINT : void 0
8775
8968
  })
8776
8969
  }
8777
8970
  ]
@@ -8780,11 +8973,21 @@ async function startMcpServer() {
8780
8973
  );
8781
8974
  server.tool(
8782
8975
  "get_turn_result",
8783
- "Final assistant message (and lastError when the turn failed). Not the full transcript.",
8976
+ "Assistant message when the turn finished, or live progress while stillRunning. Not the full transcript.",
8784
8977
  { ref: z3.string() },
8785
8978
  async ({ ref }) => {
8786
8979
  const result = orch.getTurnResult(ref);
8787
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
8980
+ return {
8981
+ content: [
8982
+ {
8983
+ type: "text",
8984
+ text: JSON.stringify({
8985
+ ...result,
8986
+ hint: result.stillRunning ? MCP_WAIT_STILL_RUNNING_HINT : void 0
8987
+ })
8988
+ }
8989
+ ]
8990
+ };
8788
8991
  }
8789
8992
  );
8790
8993
  server.tool(
@@ -8901,7 +9104,7 @@ async function startMcpServer() {
8901
9104
  );
8902
9105
  server.tool(
8903
9106
  "request_review",
8904
- '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.',
9107
+ '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.',
8905
9108
  { ref: z3.string().describe("Worktree thread id/ref to review") },
8906
9109
  async ({ ref }) => {
8907
9110
  try {
@@ -8929,7 +9132,7 @@ async function startMcpServer() {
8929
9132
  );
8930
9133
  server.tool(
8931
9134
  "ask_git",
8932
- "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.",
9135
+ "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.",
8933
9136
  {
8934
9137
  ref: z3.string().describe("Worktree thread id/ref"),
8935
9138
  action: z3.enum(AGENT_GIT_ACTIONS).describe(
@@ -8980,7 +9183,7 @@ async function startMcpServer() {
8980
9183
  );
8981
9184
  server.tool(
8982
9185
  "fork_worktree",
8983
- "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.",
9186
+ "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.",
8984
9187
  {
8985
9188
  ref: z3.string().describe("Worktree thread id/ref to fork"),
8986
9189
  through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -9025,7 +9228,7 @@ async function startMcpServer() {
9025
9228
  );
9026
9229
  server.tool(
9027
9230
  "fork_chat",
9028
- "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.",
9231
+ "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.",
9029
9232
  {
9030
9233
  ref: z3.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
9031
9234
  through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -9692,7 +9895,7 @@ async function runCloudConnect(opts) {
9692
9895
  }
9693
9896
 
9694
9897
  // src/agents/user-mcp-config.ts
9695
- import { existsSync as existsSync15, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
9898
+ import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
9696
9899
  import { homedir as homedir2 } from "os";
9697
9900
  import { dirname as dirname4, join as join14 } from "path";
9698
9901
  function userCursorMcpConfigPath() {
@@ -9726,16 +9929,16 @@ function mergeSideboardIntoMcpServersJson(existing, sideboard) {
9726
9929
  }
9727
9930
  function writeMergedMcpServersJson(configPath, sideboard) {
9728
9931
  let existing = {};
9729
- if (existsSync15(configPath)) {
9932
+ if (existsSync16(configPath)) {
9730
9933
  try {
9731
- existing = JSON.parse(readFileSync11(configPath, "utf8"));
9934
+ existing = JSON.parse(readFileSync12(configPath, "utf8"));
9732
9935
  } catch {
9733
9936
  existing = {};
9734
9937
  }
9735
9938
  }
9736
9939
  const next = mergeSideboardIntoMcpServersJson(existing, sideboard);
9737
9940
  mkdirSync5(dirname4(configPath), { recursive: true });
9738
- writeFileSync5(configPath, `${JSON.stringify(next, null, 2)}
9941
+ writeFileSync6(configPath, `${JSON.stringify(next, null, 2)}
9739
9942
  `);
9740
9943
  }
9741
9944
  function launchFromResolved(server) {
@@ -9753,7 +9956,7 @@ async function registerPackagedUserMcpClients() {
9753
9956
  const cursor = userCursorMcpConfigPath();
9754
9957
  writeMergedMcpServersJson(cursor, launch);
9755
9958
  const claude = userClaudeMcpConfigPath();
9756
- if (existsSync15(claude)) {
9959
+ if (existsSync16(claude)) {
9757
9960
  writeMergedMcpServersJson(claude, launch);
9758
9961
  return { cursor, claude };
9759
9962
  }
@@ -10251,6 +10454,8 @@ function connectSession(ws, opts, log) {
10251
10454
 
10252
10455
  // src/slack/relay-protocol.ts
10253
10456
  import { randomBytes as randomBytes3 } from "crypto";
10457
+ var SLACK_RELAY_PING_INTERVAL_MS = 2e4;
10458
+ var SLACK_RELAY_PONG_TIMEOUT_MS = 15e3;
10254
10459
  function slackRelaySessionKey(teamId, userId, deviceId) {
10255
10460
  return `${teamId.trim()}:${userId.trim()}:${deviceId.trim()}`;
10256
10461
  }
@@ -10436,10 +10641,21 @@ function connectSession2(ws, opts, log) {
10436
10641
  const allowedUsers = new Set(
10437
10642
  opts.workspaces.map((w) => `${w.teamId.trim()}:${w.userId.trim()}`)
10438
10643
  );
10644
+ const pingMs = opts.pingIntervalMs ?? SLACK_RELAY_PING_INTERVAL_MS;
10645
+ const pongMs = opts.pongTimeoutMs ?? SLACK_RELAY_PONG_TIMEOUT_MS;
10646
+ let pingTimer = null;
10647
+ let pongTimer = null;
10648
+ const stopKeepalive = () => {
10649
+ if (pingTimer) clearInterval(pingTimer);
10650
+ if (pongTimer) clearTimeout(pongTimer);
10651
+ pingTimer = null;
10652
+ pongTimer = null;
10653
+ };
10439
10654
  const finish = (err) => {
10440
10655
  if (settled) return;
10441
10656
  settled = true;
10442
10657
  pending.clear();
10658
+ stopKeepalive();
10443
10659
  opts.signal?.removeEventListener("abort", onAbort);
10444
10660
  if (err) reject(err);
10445
10661
  else resolve();
@@ -10452,6 +10668,25 @@ function connectSession2(ws, opts, log) {
10452
10668
  finish();
10453
10669
  };
10454
10670
  opts.signal?.addEventListener("abort", onAbort, { once: true });
10671
+ const armPongTimeout = () => {
10672
+ if (pongTimer) clearTimeout(pongTimer);
10673
+ pongTimer = setTimeout(() => {
10674
+ log("relay error: ping timeout");
10675
+ try {
10676
+ ws.close();
10677
+ } catch {
10678
+ }
10679
+ finish(new Error("relay ping timeout"));
10680
+ }, pongMs);
10681
+ };
10682
+ const pingOnce = () => {
10683
+ try {
10684
+ send(ws, { type: "ping" });
10685
+ armPongTimeout();
10686
+ } catch (err) {
10687
+ finish(err instanceof Error ? err : new Error("relay ping failed"));
10688
+ }
10689
+ };
10455
10690
  ws.addEventListener("open", () => {
10456
10691
  const label = opts.deviceLabel?.trim() || opts.deviceId.trim();
10457
10692
  log(`Relay connected \xB7 ${label}`);
@@ -10466,6 +10701,8 @@ function connectSession2(ws, opts, log) {
10466
10701
  userToken: wsInfo.userToken
10467
10702
  });
10468
10703
  }
10704
+ pingOnce();
10705
+ pingTimer = setInterval(pingOnce, pingMs);
10469
10706
  });
10470
10707
  ws.addEventListener("error", () => {
10471
10708
  finish(new Error("WebSocket error"));
@@ -10477,7 +10714,13 @@ function connectSession2(ws, opts, log) {
10477
10714
  const raw = typeof ev.data === "string" ? ev.data : String(ev.data ?? "");
10478
10715
  const msg = parseSlackRelayServerMessage(raw);
10479
10716
  if (!msg) return;
10480
- if (msg.type === "pong") return;
10717
+ if (msg.type === "pong") {
10718
+ if (pongTimer) {
10719
+ clearTimeout(pongTimer);
10720
+ pongTimer = null;
10721
+ }
10722
+ return;
10723
+ }
10481
10724
  if (msg.type === "error") {
10482
10725
  log(`relay error: ${msg.message}`);
10483
10726
  return;
@@ -10600,6 +10843,13 @@ function slackReplyThreadTs(msg) {
10600
10843
  if (msg.kind === "mention") return msg.ts;
10601
10844
  return void 0;
10602
10845
  }
10846
+ var SLACK_PROGRESS_DELAY_MS = 2e4;
10847
+ var SLACK_PROGRESS_EDIT_MS = 15e3;
10848
+ function formatSlackWorkingText(summary) {
10849
+ const line = (summary ?? "").trim();
10850
+ if (!line || /^working/i.test(line)) return "Working\u2026";
10851
+ return `Working\u2026 ${line}`;
10852
+ }
10603
10853
  var SLACK_SEEN_REACTION = "eyes";
10604
10854
  function writeTokenForTeam(teamId) {
10605
10855
  const workspaces = listSlackWorkspacesRaw();
@@ -10636,8 +10886,8 @@ async function ackSlackInboundSeen(msg, opts) {
10636
10886
  log(`react error: ${errMsg}${hint}`);
10637
10887
  }
10638
10888
  }
10639
- async function postSlackText(target, text3, opts) {
10640
- const stub = {
10889
+ function replyStub(target) {
10890
+ return {
10641
10891
  teamId: target.teamId,
10642
10892
  channelId: target.channelId,
10643
10893
  ts: target.threadTs ?? "",
@@ -10645,12 +10895,15 @@ async function postSlackText(target, text3, opts) {
10645
10895
  text: "",
10646
10896
  kind: "dm"
10647
10897
  };
10898
+ }
10899
+ async function postSlackText(target, text3, opts) {
10648
10900
  if (opts.postReply) {
10649
- await opts.postReply(stub, text3);
10650
- return;
10901
+ const result = await opts.postReply(replyStub(target), text3);
10902
+ const ts2 = result && typeof result === "object" && typeof result.ts === "string" ? result.ts.trim() : "";
10903
+ return ts2 ? { ts: ts2 } : null;
10651
10904
  }
10652
10905
  const token = writeTokenForTeam(target.teamId);
10653
- await slackApi(
10906
+ const json = await slackApi(
10654
10907
  token,
10655
10908
  "chat.postMessage",
10656
10909
  {
@@ -10660,18 +10913,163 @@ async function postSlackText(target, text3, opts) {
10660
10913
  },
10661
10914
  opts.fetchImpl
10662
10915
  );
10916
+ const ts = json.ts?.trim();
10917
+ return ts ? { ts } : null;
10663
10918
  }
10664
- async function postSlackReply(msg, text3, opts) {
10665
- await postSlackText(
10919
+ async function updateSlackText(target, ts, text3, opts) {
10920
+ if (opts.updateReply) {
10921
+ await opts.updateReply(replyStub(target), ts, text3);
10922
+ return;
10923
+ }
10924
+ const token = writeTokenForTeam(target.teamId);
10925
+ await slackApi(
10926
+ token,
10927
+ "chat.update",
10666
10928
  {
10667
- teamId: msg.teamId,
10668
- channelId: msg.channelId,
10669
- threadTs: slackReplyThreadTs(msg)
10929
+ channel: target.channelId,
10930
+ ts,
10931
+ text: text3
10670
10932
  },
10671
- signForThisMac(text3),
10672
- opts
10933
+ opts.fetchImpl
10673
10934
  );
10674
10935
  }
10936
+ async function deleteSlackText(target, ts, opts) {
10937
+ if (opts.deleteReply) {
10938
+ await opts.deleteReply(replyStub(target), ts);
10939
+ return;
10940
+ }
10941
+ const token = writeTokenForTeam(target.teamId);
10942
+ await slackApi(
10943
+ token,
10944
+ "chat.delete",
10945
+ {
10946
+ channel: target.channelId,
10947
+ ts
10948
+ },
10949
+ opts.fetchImpl
10950
+ );
10951
+ }
10952
+ function replyTargetOf(msg) {
10953
+ return {
10954
+ teamId: msg.teamId,
10955
+ channelId: msg.channelId,
10956
+ threadTs: slackReplyThreadTs(msg)
10957
+ };
10958
+ }
10959
+ async function postSlackReply(msg, text3, opts) {
10960
+ await postSlackText(replyTargetOf(msg), signForThisMac(text3), opts);
10961
+ }
10962
+ function startSlackTurnProgress(msg, threadId, opts) {
10963
+ const log = opts.onLog ?? (() => void 0);
10964
+ const target = replyTargetOf(msg);
10965
+ const delayMs = opts.progressDelayMs ?? SLACK_PROGRESS_DELAY_MS;
10966
+ const editMs = opts.progressEditMs ?? SLACK_PROGRESS_EDIT_MS;
10967
+ let stopped = false;
10968
+ let postedTs = null;
10969
+ let lastBody = null;
10970
+ let chain = Promise.resolve();
10971
+ let editTimer = null;
10972
+ const run2 = (fn) => {
10973
+ const next = chain.then(fn, fn);
10974
+ chain = next.then(
10975
+ () => void 0,
10976
+ () => void 0
10977
+ );
10978
+ return next;
10979
+ };
10980
+ const stillLive = () => {
10981
+ if (stopped || slackInboundSuperseded(opts)) return false;
10982
+ const thread = readThread(threadId());
10983
+ return Boolean(thread && (thread.status === "running" || thread.status === "queued"));
10984
+ };
10985
+ const postWorking = async () => {
10986
+ if (!stillLive()) return;
10987
+ const summary = readTurnLive(threadId())?.summary ?? "Working\u2026";
10988
+ const body = formatSlackWorkingText(summary);
10989
+ try {
10990
+ const posted = await postSlackText(target, signForThisMac(body), opts);
10991
+ postedTs = posted?.ts ?? postedTs;
10992
+ lastBody = body;
10993
+ if (postedTs && !stopped && !editTimer) {
10994
+ editTimer = setInterval(() => {
10995
+ void run2(editWorking);
10996
+ }, editMs);
10997
+ }
10998
+ } catch (err) {
10999
+ const errMsg = err instanceof Error ? err.message : String(err);
11000
+ log(`progress post error: ${errMsg}`);
11001
+ }
11002
+ };
11003
+ const editWorking = async () => {
11004
+ if (!postedTs || !stillLive()) return;
11005
+ const summary = readTurnLive(threadId())?.summary ?? "Working\u2026";
11006
+ const body = formatSlackWorkingText(summary);
11007
+ if (body === lastBody) return;
11008
+ lastBody = body;
11009
+ try {
11010
+ await updateSlackText(target, postedTs, signForThisMac(body), opts);
11011
+ } catch (err) {
11012
+ const errMsg = err instanceof Error ? err.message : String(err);
11013
+ log(`progress update error: ${errMsg}`);
11014
+ }
11015
+ };
11016
+ const delayTimer = setTimeout(() => {
11017
+ void run2(postWorking);
11018
+ }, delayMs);
11019
+ const stop = () => {
11020
+ stopped = true;
11021
+ clearTimeout(delayTimer);
11022
+ if (editTimer) {
11023
+ clearInterval(editTimer);
11024
+ editTimer = null;
11025
+ }
11026
+ };
11027
+ return {
11028
+ stop,
11029
+ discard: () => {
11030
+ stop();
11031
+ return run2(async () => {
11032
+ if (!postedTs) return;
11033
+ const ts = postedTs;
11034
+ postedTs = null;
11035
+ try {
11036
+ await deleteSlackText(target, ts, opts);
11037
+ } catch (err) {
11038
+ const errMsg = err instanceof Error ? err.message : String(err);
11039
+ log(`progress delete error: ${errMsg}`);
11040
+ }
11041
+ });
11042
+ },
11043
+ finishWith: (text3) => {
11044
+ stop();
11045
+ return run2(async () => {
11046
+ const signed = signForThisMac(text3.trim());
11047
+ if (!signed) {
11048
+ if (!postedTs) return;
11049
+ const ts = postedTs;
11050
+ postedTs = null;
11051
+ try {
11052
+ await deleteSlackText(target, ts, opts);
11053
+ } catch (err) {
11054
+ const errMsg = err instanceof Error ? err.message : String(err);
11055
+ log(`progress delete error: ${errMsg}`);
11056
+ }
11057
+ return;
11058
+ }
11059
+ if (postedTs) {
11060
+ try {
11061
+ await updateSlackText(target, postedTs, signed, opts);
11062
+ return;
11063
+ } catch (err) {
11064
+ const errMsg = err instanceof Error ? err.message : String(err);
11065
+ log(`progress update error: ${errMsg}`);
11066
+ }
11067
+ }
11068
+ await postSlackText(target, signed, opts);
11069
+ });
11070
+ }
11071
+ };
11072
+ }
10675
11073
  var lastRelayed = /* @__PURE__ */ new Map();
10676
11074
  function markRelayed(threadId, text3) {
10677
11075
  lastRelayed.set(threadId, `${threadId}:${text3}`);
@@ -10763,10 +11161,15 @@ async function handleSlackInbound(msg, opts) {
10763
11161
  });
10764
11162
  };
10765
11163
  bindReplyTarget(fresh.id);
11164
+ const progress = startSlackTurnProgress(msg, () => fresh.id, opts);
10766
11165
  const runTurn = async (threadId) => {
10767
11166
  await orch.send(threadId, prompt);
10768
11167
  await orch.waitForTurn(threadId, 14 * 60 * 1e3);
10769
11168
  };
11169
+ const dropProgress = async (reason) => {
11170
+ log(reason);
11171
+ await progress.discard();
11172
+ };
10770
11173
  let reply;
10771
11174
  try {
10772
11175
  try {
@@ -10779,12 +11182,12 @@ async function handleSlackInbound(msg, opts) {
10779
11182
  await runTurn(fresh.id);
10780
11183
  }
10781
11184
  if (slackInboundSuperseded(opts)) {
10782
- log(`turn finished ${msg.ts} (superseded)`);
11185
+ await dropProgress(`turn finished ${msg.ts} (superseded)`);
10783
11186
  return;
10784
11187
  }
10785
11188
  let after = readThread(fresh.id);
10786
11189
  if (after?.status === "stopped") {
10787
- log(`turn finished ${msg.ts} (interrupted, skip post)`);
11190
+ await dropProgress(`turn finished ${msg.ts} (interrupted, skip post)`);
10788
11191
  return;
10789
11192
  }
10790
11193
  if (!after || after.status === "archived") {
@@ -10793,46 +11196,48 @@ async function handleSlackInbound(msg, opts) {
10793
11196
  bindReplyTarget(fresh.id);
10794
11197
  await runTurn(fresh.id);
10795
11198
  if (slackInboundSuperseded(opts)) {
10796
- log(`turn finished ${msg.ts} (superseded)`);
11199
+ await dropProgress(`turn finished ${msg.ts} (superseded)`);
10797
11200
  return;
10798
11201
  }
10799
11202
  after = readThread(fresh.id);
10800
11203
  if (!after || after.status === "stopped" || after.status === "archived") {
10801
- log(`turn finished ${msg.ts} (interrupted, skip post)`);
11204
+ await dropProgress(`turn finished ${msg.ts} (interrupted, skip post)`);
10802
11205
  return;
10803
11206
  }
10804
11207
  }
10805
11208
  reply = orch.getTurnResult(fresh.id).text.trim();
10806
11209
  } catch (err) {
10807
11210
  if (slackInboundSuperseded(opts)) {
10808
- log(`turn error ${msg.ts} (superseded)`);
11211
+ await dropProgress(`turn error ${msg.ts} (superseded)`);
10809
11212
  return;
10810
11213
  }
10811
11214
  const errMsg = err instanceof Error ? err.message : String(err);
10812
11215
  if (slackCoordinatorGone(err)) {
10813
- log(`turn finished ${msg.ts} (thread gone, skip post)`);
11216
+ await dropProgress(`turn finished ${msg.ts} (thread gone, skip post)`);
10814
11217
  return;
10815
11218
  }
10816
11219
  const timedOut = /timed out|timeout/i.test(errMsg);
10817
11220
  const errorReply = timedOut ? SLACK_LISTEN_TIMEOUT_REPLY : `Sideboard coordinator error: ${errMsg}`;
10818
- await postSlackReply(msg, errorReply, opts).catch((postErr) => {
11221
+ try {
11222
+ await progress.finishWith(errorReply);
11223
+ log(`replied error ${msg.ts}: ${errMsg}`);
11224
+ } catch (postErr) {
10819
11225
  const postMsg = postErr instanceof Error ? postErr.message : String(postErr);
10820
11226
  log(`post error: ${postMsg}`);
10821
- });
10822
- log(`replied error ${msg.ts}: ${errMsg}`);
11227
+ }
10823
11228
  if (!timedOut) throw err;
10824
11229
  return;
10825
11230
  }
10826
11231
  if (!reply) {
10827
- log(`post error: no agent text to send for ${msg.ts}`);
11232
+ await dropProgress(`post error: no agent text to send for ${msg.ts}`);
10828
11233
  return;
10829
11234
  }
10830
11235
  if (alreadyRelayed(fresh.id, reply)) {
10831
- log(`turn finished ${msg.ts} (already posted)`);
11236
+ await dropProgress(`turn finished ${msg.ts} (already posted)`);
10832
11237
  return;
10833
11238
  }
10834
11239
  try {
10835
- await postSlackReply(msg, reply, opts);
11240
+ await progress.finishWith(reply);
10836
11241
  markRelayed(fresh.id, reply);
10837
11242
  log(`replied ${msg.ts} (${reply.length} chars)`);
10838
11243
  } catch (err) {
@@ -11383,6 +11788,20 @@ async function startSlackRelayServer(opts) {
11383
11788
  }
11384
11789
  }
11385
11790
  };
11791
+ const ping = setInterval(() => {
11792
+ if (ws.readyState !== ws.OPEN) {
11793
+ clearInterval(ping);
11794
+ return;
11795
+ }
11796
+ try {
11797
+ ws.ping();
11798
+ } catch {
11799
+ clearInterval(ping);
11800
+ }
11801
+ }, SLACK_RELAY_PING_INTERVAL_MS);
11802
+ const stopPing = () => clearInterval(ping);
11803
+ ws.on("close", stopPing);
11804
+ ws.on("error", stopPing);
11386
11805
  log("desktop connected");
11387
11806
  ws.on("message", (data) => {
11388
11807
  const raw = typeof data === "string" ? data : data.toString("utf8");
@@ -11493,6 +11912,8 @@ export {
11493
11912
  SLACK_LISTEN_TIMEOUT_REPLY,
11494
11913
  SLACK_OAUTH_CANCELLED,
11495
11914
  SLACK_OAUTH_REDIRECT,
11915
+ SLACK_PROGRESS_DELAY_MS,
11916
+ SLACK_PROGRESS_EDIT_MS,
11496
11917
  SLACK_REPLY_FORMATTING,
11497
11918
  SLACK_SEEN_REACTION,
11498
11919
  SlackOAuthCancelledError,
@@ -11650,6 +12071,7 @@ export {
11650
12071
  formatSlackInboundPrompt,
11651
12072
  formatSlackRepliesForTurn,
11652
12073
  formatSlackSignedReply,
12074
+ formatSlackWorkingText,
11653
12075
  formatTranscriptMarkdown,
11654
12076
  formatUiReminder,
11655
12077
  formatWorkspaceInventory,
@@ -11945,6 +12367,7 @@ export {
11945
12367
  thisProcessShouldDrainAgentQueues,
11946
12368
  threadDisplayLabel,
11947
12369
  threadFilePath,
12370
+ threadLivePath,
11948
12371
  threadLockPath,
11949
12372
  threadRequestsBrightsyMcp,
11950
12373
  threadsDir,
@@ -11968,6 +12391,7 @@ export {
11968
12391
  userClaudeMcpConfigPath,
11969
12392
  userCursorMcpConfigPath,
11970
12393
  validateLinearApiKey,
12394
+ waitForPidExit,
11971
12395
  warmGithubAgentAuth,
11972
12396
  withAgentInstructions,
11973
12397
  withExportedPath,