@sideboard-ai/core 0.1.99 → 0.1.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/agents/cursor-runner.cjs +22 -22
  2. package/dist/agents/cursor-runner.js +2 -2
  3. package/dist/{agents-ESJKIQQA.js → agents-UT5GCJH3.js} +8 -8
  4. package/dist/{agents-3MWWWSMF.js → agents-XZBAKBO2.js} +9 -9
  5. package/dist/{app-settings-BRF7VKTQ.js → app-settings-EKJM2344.js} +2 -2
  6. package/dist/{app-settings-ZXPAB3QN.js → app-settings-VDJK2BZ2.js} +2 -2
  7. package/dist/{caffeinate-hold-U3QJBV67.js → caffeinate-hold-NBRUWYXO.js} +2 -2
  8. package/dist/{caffeinate-hold-I7SG6UQR.js → caffeinate-hold-X3JNPR42.js} +2 -2
  9. package/dist/{chunk-UYQYK2RY.js → chunk-27MCESES.js} +3 -3
  10. package/dist/{chunk-QSLE4VEM.js → chunk-2GO3YKBA.js} +4 -4
  11. package/dist/{chunk-DKHGWYWR.js → chunk-7YKXHBIW.js} +1 -1
  12. package/dist/{chunk-JL4SJ6V7.js → chunk-BD2ICC4D.js} +4 -0
  13. package/dist/{chunk-NZPGMNFC.js → chunk-DCOZABNT.js} +1 -1
  14. package/dist/{chunk-CIRXAYWS.js → chunk-DHAGMJIK.js} +55 -28
  15. package/dist/{chunk-XUWDLRAE.js → chunk-ETMRBJKA.js} +11 -11
  16. package/dist/{chunk-MBP3XG57.js → chunk-H6LP57EF.js} +7 -7
  17. package/dist/{chunk-FO67IJTY.js → chunk-HHLZWO7U.js} +5 -5
  18. package/dist/{chunk-GXSYI7FH.js → chunk-HUKCGRAT.js} +209 -59
  19. package/dist/{chunk-3WA37BRI.js → chunk-KGSVLZV6.js} +11 -4
  20. package/dist/{chunk-7KDBDMNC.js → chunk-KLBOWJ74.js} +1 -1
  21. package/dist/{chunk-FYS2BULQ.js → chunk-LANDJIAI.js} +213 -63
  22. package/dist/{chunk-OANJQTVG.js → chunk-ND6AO7C5.js} +3 -3
  23. package/dist/{chunk-HI2OTFFR.js → chunk-OW34MVDN.js} +5 -5
  24. package/dist/{chunk-JOF3XIEM.js → chunk-P3BQ5DOL.js} +11 -4
  25. package/dist/{chunk-B4R2VB2A.js → chunk-QZQEXME2.js} +4 -0
  26. package/dist/{chunk-R7BQBSDT.js → chunk-WA3DVVWG.js} +55 -28
  27. package/dist/{chunk-6LFV4VFI.js → chunk-WSYCND2U.js} +1 -1
  28. package/dist/{chunk-FT2SQOL4.js → chunk-XHZJJWLI.js} +1 -1
  29. package/dist/{chunk-XH2GS2LO.js → chunk-YJADJH5U.js} +11 -11
  30. package/dist/{connected-teams-SWHJWK2A.js → connected-teams-J4I5AKMR.js} +2 -2
  31. package/dist/{connected-teams-TLVKZPJP.js → connected-teams-M5XNXRI5.js} +2 -2
  32. package/dist/{coordinator-prompt-OQOOD5ET.js → coordinator-prompt-3DFZRMXE.js} +5 -5
  33. package/dist/{coordinator-prompt-AKEY4WSO.js → coordinator-prompt-UWHSD65V.js} +5 -5
  34. package/dist/{cursor-recover-S5ORPW34.js → cursor-recover-JBTIDARH.js} +1 -1
  35. package/dist/{cursor-recover-G34BYKL2.js → cursor-recover-ZUJG6XRQ.js} +1 -1
  36. package/dist/{global-workspace-WMF3BJP5.js → global-workspace-KJQS5S2C.js} +6 -6
  37. package/dist/{global-workspace-RSQXRLT7.js → global-workspace-UR4E3CCG.js} +6 -6
  38. package/dist/index.cjs +851 -232
  39. package/dist/index.d.cts +39 -6
  40. package/dist/index.d.ts +39 -6
  41. package/dist/index.js +561 -138
  42. package/dist/mcp/run-stdio.cjs +651 -256
  43. package/dist/mcp/run-stdio.js +345 -143
  44. package/dist/{paths-TZX4L5X7.js → paths-QCI3MTO5.js} +3 -1
  45. package/dist/{paths-JQM2XRAD.js → paths-UMEDHOS5.js} +3 -1
  46. package/dist/{thread-store-XAKWCV2E.js → thread-store-4OUEQ2DB.js} +2 -2
  47. package/dist/{thread-store-V7NOOXKE.js → thread-store-DQJGDKIJ.js} +2 -2
  48. package/dist/{workspaces-MZVQHRSJ.js → workspaces-NTIVI6DI.js} +7 -7
  49. package/dist/{workspaces-4ZY4QPWQ.js → workspaces-TKJXPNV4.js} +7 -7
  50. package/dist/{worktree-EO5QAGJU.js → worktree-NKWN5ZTS.js} +4 -4
  51. package/dist/{worktree-DA4BOV7G.js → worktree-PEUPFAKL.js} +4 -4
  52. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  listWorkspaces,
5
5
  removeWorkspace,
6
6
  syncWorkspacesFromThreads
7
- } from "./chunk-UYQYK2RY.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-MBP3XG57.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-FO67IJTY.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-XUWDLRAE.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,
@@ -133,20 +133,21 @@ import {
133
133
  cursorSdkMessageToEvents,
134
134
  fallbackTurnFailDetail,
135
135
  formatTurnExitError,
136
- humanizeAgentFailDetail,
137
136
  looksLikeAgentFailureMessage,
138
137
  looksLikeInvalidAgentSession,
139
138
  parseCursorRunnerLine,
140
139
  pushTurnStderr,
141
- summarizeTurnStderr
142
- } from "./chunk-GXSYI7FH.js";
140
+ shouldRetryFailedAgentTurn,
141
+ summarizeTurnStderr,
142
+ turnFailChatText
143
+ } from "./chunk-HUKCGRAT.js";
143
144
  import {
144
145
  caffeinateHoldPath,
145
146
  getCaffeinateHold,
146
147
  isThreadCaffeinated,
147
148
  releaseCaffeinateHoldForThread,
148
149
  setCaffeinateHold
149
- } from "./chunk-NZPGMNFC.js";
150
+ } from "./chunk-DCOZABNT.js";
150
151
  import {
151
152
  FAMOUS_SOCCER_TEAMS,
152
153
  addPrStackLayer,
@@ -166,6 +167,7 @@ import {
166
167
  currentBranch,
167
168
  detectGhStack,
168
169
  detectLocalMergeConflicts,
170
+ enqueueByKey,
169
171
  ensureGhPreferOrigin,
170
172
  extractGhErrorDetail,
171
173
  fetchPrHead,
@@ -225,10 +227,11 @@ import {
225
227
  takenSlugsFromThread,
226
228
  threadDisplayLabel,
227
229
  warmGithubAgentAuth,
230
+ withRepoGitLock,
228
231
  worktreeDisplayLabel,
229
232
  worktreeDisplayLabelForGroup,
230
233
  worktreeNameFromPath
231
- } from "./chunk-CIRXAYWS.js";
234
+ } from "./chunk-DHAGMJIK.js";
232
235
  import {
233
236
  ATTACHMENTS_DIR,
234
237
  LEGACY_ATTACHMENTS_DIR,
@@ -297,7 +300,7 @@ import {
297
300
  updateIntegrationsSettings,
298
301
  updateOpencodeSettings,
299
302
  writeSecureJson
300
- } from "./chunk-FT2SQOL4.js";
303
+ } from "./chunk-XHZJJWLI.js";
301
304
  import {
302
305
  stripNestedElectronEnv
303
306
  } from "./chunk-TLPJHLLM.js";
@@ -317,7 +320,7 @@ import {
317
320
  updateThread,
318
321
  withThreadLock,
319
322
  writeThread
320
- } from "./chunk-3WA37BRI.js";
323
+ } from "./chunk-KGSVLZV6.js";
321
324
  import {
322
325
  THINKING_EFFORTS,
323
326
  isThinkingEffort,
@@ -348,11 +351,12 @@ import {
348
351
  sideboardReposDir,
349
352
  sideboardWorkspacesDir,
350
353
  threadFilePath,
354
+ threadLivePath,
351
355
  threadLockPath,
352
356
  threadsDir,
353
357
  workspaceSettingsSourceLabel,
354
358
  worktreesRoot
355
- } from "./chunk-B4R2VB2A.js";
359
+ } from "./chunk-QZQEXME2.js";
356
360
  import {
357
361
  conductorBundledBinDir,
358
362
  enrichPathWithNpmGlobalBin,
@@ -1385,9 +1389,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
1385
1389
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
1386
1390
  );
1387
1391
  }
1388
- const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-RSQXRLT7.js");
1392
+ const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-UR4E3CCG.js");
1389
1393
  if (isGlobalThread2(thread)) {
1390
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-AKEY4WSO.js");
1394
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-UWHSD65V.js");
1391
1395
  ensureGlobalCoordinatorCwd2(
1392
1396
  isOrchestratorThread(thread) ? { orchestratorThreadId: thread.id } : void 0
1393
1397
  );
@@ -3266,11 +3270,11 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
3266
3270
  }
3267
3271
  return out;
3268
3272
  }
3269
- function stageBuffersAsAttachments(worktreePath, buffers) {
3270
- if (buffers.length === 0) return [];
3273
+ function stageBuffersAsAttachments(worktreePath, buffers2) {
3274
+ if (buffers2.length === 0) return [];
3271
3275
  const dir = ensureAttachmentsDir(worktreePath);
3272
3276
  const out = [];
3273
- for (const item of buffers) {
3277
+ for (const item of buffers2) {
3274
3278
  const originalName = (item.name || "file").replace(/[/\\]/g, "_") || "file";
3275
3279
  try {
3276
3280
  const buf = Buffer.from(item.dataBase64, "base64");
@@ -3290,8 +3294,8 @@ function stageBuffersAsAttachments(worktreePath, buffers) {
3290
3294
  }
3291
3295
  return out;
3292
3296
  }
3293
- function attachmentsFromBuffers(buffers) {
3294
- return buffers.map((item) => {
3297
+ function attachmentsFromBuffers(buffers2) {
3298
+ return buffers2.map((item) => {
3295
3299
  const name = (item.name || "file").replace(/[/\\]/g, "_") || "file";
3296
3300
  try {
3297
3301
  const buf = Buffer.from(item.dataBase64, "base64");
@@ -3880,7 +3884,7 @@ async function createThread(input, _onSetupLine) {
3880
3884
  return readThread(thread.id) ?? thread;
3881
3885
  }
3882
3886
  async function listLinearIssues(agent, repoPath) {
3883
- const { getAdapter: getAdapter2 } = await import("./agents-3MWWWSMF.js");
3887
+ const { getAdapter: getAdapter2 } = await import("./agents-XZBAKBO2.js");
3884
3888
  await requireAgent(agent, { requireLinear: true });
3885
3889
  const adapter = getAdapter2(agent);
3886
3890
  if (!adapter.listLinearIssues) {
@@ -3931,8 +3935,11 @@ function worktreeBindingFrom(from) {
3931
3935
  };
3932
3936
  }
3933
3937
  function threadsSharingWorktree(worktreePath) {
3938
+ if (!worktreePath) return [];
3934
3939
  const key = normalizeWorktreePath(worktreePath);
3935
- 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));
3936
3943
  }
3937
3944
  function formatTranscriptMarkdown(title, messages) {
3938
3945
  const body = formatMessagesAsTranscript(messages, { tools: "full" });
@@ -4433,7 +4440,7 @@ async function adoptThread(input) {
4433
4440
  messages: input.messages ?? []
4434
4441
  });
4435
4442
  writeThread(thread);
4436
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-4ZY4QPWQ.js");
4443
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-TKJXPNV4.js");
4437
4444
  await ensureWorkspace2(repoPath);
4438
4445
  return thread;
4439
4446
  }
@@ -5199,7 +5206,7 @@ async function refreshSlackReplyBadges(opts) {
5199
5206
  }
5200
5207
 
5201
5208
  // src/orchestrator/orchestrator.ts
5202
- import { existsSync as existsSync14 } from "fs";
5209
+ import { existsSync as existsSync15 } from "fs";
5203
5210
 
5204
5211
  // src/git/pr-merge-archive.ts
5205
5212
  function normalizePrState(state) {
@@ -5444,9 +5451,132 @@ async function cloneRepoIntoSideboard(opts) {
5444
5451
  return { repoPath, workspace };
5445
5452
  }
5446
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
+
5447
5577
  // src/review/request-review.ts
5448
5578
  import { randomUUID as randomUUID4 } from "crypto";
5449
- 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";
5450
5580
  import { dirname as dirname3, join as join13 } from "path";
5451
5581
 
5452
5582
  // src/review/review-request-template.ts
@@ -5592,9 +5722,9 @@ function shouldRefreshReviewRequestTemplate(content) {
5592
5722
  return LEGACY_REVIEW_TEMPLATE_MARKERS.every((m) => trimmed.includes(m));
5593
5723
  }
5594
5724
  function readTextIfPresent(abs) {
5595
- if (!existsSync13(abs)) return null;
5725
+ if (!existsSync14(abs)) return null;
5596
5726
  try {
5597
- const content = readFileSync10(abs, "utf8");
5727
+ const content = readFileSync11(abs, "utf8");
5598
5728
  return content.trim() ? content : null;
5599
5729
  } catch {
5600
5730
  return null;
@@ -5602,9 +5732,9 @@ function readTextIfPresent(abs) {
5602
5732
  }
5603
5733
  function ensureAttachmentsGitignore(worktreePath) {
5604
5734
  const gitignoreAbs = join13(worktreePath, ATTACHMENTS_DIR, ".gitignore");
5605
- if (existsSync13(gitignoreAbs)) return;
5735
+ if (existsSync14(gitignoreAbs)) return;
5606
5736
  mkdirSync4(dirname3(gitignoreAbs), { recursive: true });
5607
- writeFileSync4(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
5737
+ writeFileSync5(gitignoreAbs, attachmentsGitignoreBody(), "utf8");
5608
5738
  }
5609
5739
  function resolveReviewGuidelines(worktreePath) {
5610
5740
  const repoAbs = join13(worktreePath, REPO_REVIEW_PATH);
@@ -5639,7 +5769,7 @@ function resolveReviewGuidelines(worktreePath) {
5639
5769
  }
5640
5770
  ensureAttachmentsGitignore(worktreePath);
5641
5771
  mkdirSync4(dirname3(localAbs), { recursive: true });
5642
- writeFileSync4(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
5772
+ writeFileSync5(localAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
5643
5773
  return {
5644
5774
  path: REVIEW_REQUEST_PATH,
5645
5775
  name: REVIEW_REQUEST_NAME,
@@ -5661,7 +5791,7 @@ function ensureReviewRequestFile(worktreePath) {
5661
5791
  const localAbs = join13(worktreePath, REVIEW_REQUEST_PATH);
5662
5792
  const localContent = readTextIfPresent(localAbs) ?? readTextIfPresent(join13(worktreePath, LEGACY_REVIEW_REQUEST_PATH));
5663
5793
  if (localContent && !shouldRefreshReviewRequestTemplate(localContent)) {
5664
- const path2 = existsSync13(localAbs) ? REVIEW_REQUEST_PATH : LEGACY_REVIEW_REQUEST_PATH;
5794
+ const path2 = existsSync14(localAbs) ? REVIEW_REQUEST_PATH : LEGACY_REVIEW_REQUEST_PATH;
5665
5795
  return {
5666
5796
  path: path2,
5667
5797
  name: REVIEW_REQUEST_NAME,
@@ -5670,7 +5800,7 @@ function ensureReviewRequestFile(worktreePath) {
5670
5800
  };
5671
5801
  }
5672
5802
  mkdirSync4(dirname3(repoAbs), { recursive: true });
5673
- writeFileSync4(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
5803
+ writeFileSync5(repoAbs, REVIEW_REQUEST_TEMPLATE, "utf8");
5674
5804
  return {
5675
5805
  path: REPO_REVIEW_PATH,
5676
5806
  name: REPO_REVIEW_NAME,
@@ -5863,6 +5993,15 @@ function isPidAlive(pid) {
5863
5993
  return false;
5864
5994
  }
5865
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;
5866
6005
  function writeLiveStatus(threadId, status, lastError) {
5867
6006
  const latest = readThread(threadId);
5868
6007
  if (!latest || latest.status === "archived") return latest;
@@ -5940,7 +6079,7 @@ var Orchestrator = class {
5940
6079
  }
5941
6080
  continue;
5942
6081
  }
5943
- if (!existsSync14(thread.worktreePath)) {
6082
+ if (!existsSync15(thread.worktreePath)) {
5944
6083
  setStatus(thread.id, "broken", "Worktree missing on disk");
5945
6084
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
5946
6085
  continue;
@@ -5959,7 +6098,7 @@ var Orchestrator = class {
5959
6098
  orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
5960
6099
  });
5961
6100
  }
5962
- const { autoCleanupOrphansEnabled: autoCleanupOrphansEnabled2 } = await import("./app-settings-BRF7VKTQ.js");
6101
+ const { autoCleanupOrphansEnabled: autoCleanupOrphansEnabled2 } = await import("./app-settings-EKJM2344.js");
5963
6102
  if (autoCleanupOrphansEnabled2() && shouldRunWorktreeCleanup() && orphans.length > 0) {
5964
6103
  await cleanupOrphanWorktrees({ repoPaths });
5965
6104
  }
@@ -6127,7 +6266,7 @@ var Orchestrator = class {
6127
6266
  }
6128
6267
  }
6129
6268
  await setup;
6130
- const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await import("./app-settings-BRF7VKTQ.js");
6269
+ const { autoRunAfterSetupEnabled: autoRunAfterSetupEnabled2 } = await import("./app-settings-EKJM2344.js");
6131
6270
  if (autoRunAfterSetupEnabled2()) {
6132
6271
  try {
6133
6272
  await this.startDev(threadId);
@@ -6261,14 +6400,10 @@ var Orchestrator = class {
6261
6400
  const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id);
6262
6401
  const livePid = current.agentPid;
6263
6402
  const foreignLive = !inFlight && typeof livePid === "number" && livePid > 0 && isPidAlive(livePid);
6264
- if (inFlight) {
6403
+ if (inFlight || foreignLive) {
6265
6404
  this.stop(thread.id, { clearQueue: false, continueQueue: true });
6266
- } else {
6267
- if (foreignLive) {
6268
- this.stop(thread.id, { clearQueue: false, continueQueue: true });
6269
- }
6270
- void this.drainQueue(thread.id);
6271
6405
  }
6406
+ void this.drainQueue(thread.id);
6272
6407
  return this.requireThread(thread.id);
6273
6408
  }
6274
6409
  async drainQueue(threadId) {
@@ -6297,7 +6432,18 @@ var Orchestrator = class {
6297
6432
  }
6298
6433
  const livePid = thread.agentPid;
6299
6434
  if (typeof livePid === "number" && livePid > 0 && isPidAlive(livePid)) {
6300
- 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
+ }
6301
6447
  continue;
6302
6448
  }
6303
6449
  const prompt = thread.queue[0];
@@ -6405,7 +6551,7 @@ var Orchestrator = class {
6405
6551
  }
6406
6552
  const isBrightsy = fresh.agent === "brightsy";
6407
6553
  const isOrchestration = isOrchestratorThread(fresh);
6408
- const { autoRenameBranchEnabled: autoRenameBranchEnabled2, getGithubGitAuthMode: getGithubGitAuthMode2 } = await import("./app-settings-BRF7VKTQ.js");
6554
+ const { autoRenameBranchEnabled: autoRenameBranchEnabled2, getGithubGitAuthMode: getGithubGitAuthMode2 } = await import("./app-settings-EKJM2344.js");
6409
6555
  const gitAuthMode = getGithubGitAuthMode2();
6410
6556
  const worktreeDirective = isBrightsy || isOrchestration ? null : formatWorktreeDirective(fresh, {
6411
6557
  githubSlug: await resolveGithubRepoSlug(fresh.worktreePath).catch(
@@ -6454,6 +6600,7 @@ var Orchestrator = class {
6454
6600
  { cachedPrefix, prompt: agentPrompt },
6455
6601
  (event) => {
6456
6602
  this.emit({ type: "turn_output", threadId, event });
6603
+ noteTurnLiveEvent(threadId, event);
6457
6604
  if (event.type === "session_id") {
6458
6605
  updateThread(threadId, { sessionId: event.data });
6459
6606
  }
@@ -6500,7 +6647,7 @@ var Orchestrator = class {
6500
6647
  if (this.requireThread(threadId).agent === "cursor" && exitCode !== 0 && !assistantText && parts.length === 0) {
6501
6648
  const sessionId = result.sessionId || this.requireThread(threadId).sessionId || "";
6502
6649
  if (sessionId) {
6503
- const { recoverFinishedCursorRun } = await import("./cursor-recover-G34BYKL2.js");
6650
+ const { recoverFinishedCursorRun } = await import("./cursor-recover-ZUJG6XRQ.js");
6504
6651
  for (let i = 0; i < 8; i++) {
6505
6652
  const recovered = recoverFinishedCursorRun({
6506
6653
  agentId: sessionId,
@@ -6517,19 +6664,16 @@ var Orchestrator = class {
6517
6664
  }
6518
6665
  let lastStderr = summarizeTurnStderr(stderrTail);
6519
6666
  let detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
6520
- if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && looksLikeInvalidAgentSession(detail) && this.requireThread(threadId).sessionId && this.requireThread(threadId).agent !== "brightsy") {
6667
+ if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && this.requireThread(threadId).agent !== "brightsy" && shouldRetryFailedAgentTurn(detail, {
6668
+ hasSession: Boolean(this.requireThread(threadId).sessionId)
6669
+ })) {
6521
6670
  updateThread(threadId, { sessionId: null });
6522
- pushTurnStderr(
6523
- stderrTail,
6524
- "Agent session missing \u2014 starting a fresh session"
6525
- );
6671
+ const retryNote = looksLikeInvalidAgentSession(detail) ? "Agent session missing \u2014 starting a fresh session" : "Agent runner crashed \u2014 restarting Node once";
6672
+ pushTurnStderr(stderrTail, retryNote);
6526
6673
  this.emit({
6527
6674
  type: "turn_output",
6528
6675
  threadId,
6529
- event: {
6530
- type: "stderr",
6531
- data: "Agent session missing \u2014 starting a fresh session"
6532
- }
6676
+ event: { type: "stderr", data: retryNote }
6533
6677
  });
6534
6678
  const retryThread = this.requireThread(threadId);
6535
6679
  const prior = retryThread.messages.slice(0, -1);
@@ -6578,10 +6722,7 @@ var Orchestrator = class {
6578
6722
  lastStderr = summarizeTurnStderr(stderrTail);
6579
6723
  detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
6580
6724
  }
6581
- let chatText = assistantText;
6582
- if (exitCode !== 0 && !chatText && looksLikeAgentFailureMessage(detail)) {
6583
- chatText = humanizeAgentFailDetail(detail);
6584
- }
6725
+ let chatText = this.stoppedTurns.has(threadId) ? assistantText : turnFailChatText({ exitCode, assistantText, detail });
6585
6726
  if (chatText || parts.length > 0) {
6586
6727
  appendMessage(threadId, {
6587
6728
  role: "agent",
@@ -6664,6 +6805,7 @@ var Orchestrator = class {
6664
6805
  this.processes.delete(`${threadId}:agent`);
6665
6806
  this.stoppedTurns.delete(threadId);
6666
6807
  this.runningCount = Math.max(0, this.runningCount - 1);
6808
+ clearTurnLive(threadId);
6667
6809
  try {
6668
6810
  updateThread(threadId, { agentPid: null });
6669
6811
  } catch {
@@ -6937,51 +7079,56 @@ var Orchestrator = class {
6937
7079
  }
6938
7080
  return created;
6939
7081
  }
6940
- async waitForTurn(threadRef, timeoutMs = 6e5) {
7082
+ async waitForTurn(threadRef, timeoutMs = 6e5, opts) {
6941
7083
  const thread = this.requireThread(threadRef);
7084
+ if (!["running", "queued"].includes(thread.status)) {
7085
+ return thread;
7086
+ }
6942
7087
  const start = Date.now();
6943
7088
  return new Promise((resolve, reject) => {
6944
- if (!["running", "queued"].includes(thread.status)) {
6945
- resolve(thread);
6946
- return;
6947
- }
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
+ };
6948
7098
  const off = this.on((event) => {
6949
7099
  if (event.type === "turn_finished" && event.threadId === thread.id) {
6950
- off();
6951
7100
  const latest = readThread(thread.id);
6952
7101
  if (!latest) {
6953
- reject(new Error(`Thread not found: ${thread.id}`));
7102
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
6954
7103
  return;
6955
7104
  }
6956
- resolve(latest);
7105
+ finish(() => resolve(latest));
6957
7106
  }
6958
7107
  if (event.type === "error" && event.threadId === thread.id) {
6959
- off();
6960
7108
  const latest = readThread(thread.id);
6961
7109
  if (!latest) {
6962
- reject(new Error(`Thread not found: ${thread.id}`));
7110
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
6963
7111
  return;
6964
7112
  }
6965
- resolve(latest);
7113
+ finish(() => resolve(latest));
6966
7114
  }
6967
7115
  });
6968
- const timer = setInterval(() => {
6969
- if (Date.now() - start > timeoutMs) {
6970
- clearInterval(timer);
6971
- off();
6972
- reject(new Error("wait_for_turn timed out"));
6973
- }
7116
+ timer = setInterval(() => {
6974
7117
  const current = readThread(thread.id);
6975
7118
  if (!current) {
6976
- clearInterval(timer);
6977
- off();
6978
- reject(new Error(`Thread not found: ${thread.id}`));
7119
+ finish(() => reject(new Error(`Thread not found: ${thread.id}`)));
6979
7120
  return;
6980
7121
  }
6981
7122
  if (!["running", "queued"].includes(current.status)) {
6982
- clearInterval(timer);
6983
- off();
6984
- 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
+ }
6985
7132
  }
6986
7133
  }, 200);
6987
7134
  });
@@ -6989,10 +7136,18 @@ var Orchestrator = class {
6989
7136
  getTurnResult(threadRef) {
6990
7137
  const thread = this.requireThread(threadRef);
6991
7138
  const lastAgent = [...thread.messages].reverse().find((m) => m.role === "agent");
7139
+ const lastError = thread.lastError ?? null;
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;
6992
7143
  return {
6993
- text: lastAgent?.text ?? "",
7144
+ text: text3,
6994
7145
  status: thread.status,
6995
- sessionId: thread.sessionId
7146
+ sessionId: thread.sessionId,
7147
+ lastError,
7148
+ stillRunning,
7149
+ progress: live?.summary ?? null,
7150
+ lastActivityAt: live?.updatedAt ?? null
6996
7151
  };
6997
7152
  }
6998
7153
  assertNotGlobal(thread, action) {
@@ -7155,7 +7310,7 @@ var Orchestrator = class {
7155
7310
  updateThread(thread.id, { title: meta.title });
7156
7311
  }
7157
7312
  }
7158
- const { autoArchiveOnMergeEnabled: autoArchiveOnMergeEnabled2 } = await import("./app-settings-BRF7VKTQ.js");
7313
+ const { autoArchiveOnMergeEnabled: autoArchiveOnMergeEnabled2 } = await import("./app-settings-EKJM2344.js");
7159
7314
  const latest = this.requireThread(thread.id);
7160
7315
  if (!shouldAutoArchiveOnPrMerge({
7161
7316
  previousPrState: prevState || null,
@@ -7418,7 +7573,19 @@ var Orchestrator = class {
7418
7573
  const thread = this.requireThread(threadRef);
7419
7574
  return threadsSharingWorktree(thread.worktreePath);
7420
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
+ }
7421
7584
  async archive(threadRef) {
7585
+ const thread = this.requireThread(threadRef);
7586
+ return this.enqueueWorktreeTeardown(thread, () => this.archiveUnlocked(threadRef));
7587
+ }
7588
+ async archiveUnlocked(threadRef) {
7422
7589
  const thread = this.requireThread(threadRef);
7423
7590
  this.stop(thread.id);
7424
7591
  this.releaseOrchestratorCaffeinate(thread);
@@ -7446,7 +7613,7 @@ var Orchestrator = class {
7446
7613
  this.emit({ type: "status_changed", threadId: archived.id, status: "archived" });
7447
7614
  if (thread.repoPath && !isGlobalRepoPath(thread.repoPath)) {
7448
7615
  try {
7449
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-4ZY4QPWQ.js");
7616
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-TKJXPNV4.js");
7450
7617
  await ensureWorkspace2(thread.repoPath);
7451
7618
  } catch {
7452
7619
  }
@@ -7454,6 +7621,10 @@ var Orchestrator = class {
7454
7621
  return archived;
7455
7622
  }
7456
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) {
7457
7628
  const thread = this.requireThread(threadRef);
7458
7629
  this.stop(thread.id);
7459
7630
  this.releaseOrchestratorCaffeinate(thread);
@@ -7468,7 +7639,7 @@ var Orchestrator = class {
7468
7639
  await runArchiveScript(thread.repoPath, thread.worktreePath);
7469
7640
  } catch {
7470
7641
  }
7471
- const { deleteBranchOnPurgeEnabled: deleteBranchOnPurgeEnabled2 } = await import("./app-settings-BRF7VKTQ.js");
7642
+ const { deleteBranchOnPurgeEnabled: deleteBranchOnPurgeEnabled2 } = await import("./app-settings-EKJM2344.js");
7472
7643
  const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled2();
7473
7644
  await removeWorktree(thread.repoPath, thread.worktreePath, {
7474
7645
  deleteBranch: deleteBranch ? thread.branchName : void 0
@@ -7482,19 +7653,18 @@ var Orchestrator = class {
7482
7653
  throw new Error("Thread is not archived");
7483
7654
  }
7484
7655
  if (isGlobalThread(thread)) {
7485
- const { globalAgentCwd: globalAgentCwd2 } = await import("./paths-JQM2XRAD.js");
7656
+ const { globalAgentCwd: globalAgentCwd2 } = await import("./paths-UMEDHOS5.js");
7486
7657
  updateThread(thread.id, { worktreePath: globalAgentCwd2() });
7487
7658
  const restored2 = setStatus(thread.id, "idle");
7488
7659
  this.emit({ type: "status_changed", threadId: restored2.id, status: restored2.status });
7489
7660
  return restored2;
7490
7661
  }
7491
- if (!existsSync14(thread.worktreePath)) {
7492
- const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-DA4BOV7G.js");
7493
- const { execa: execa5 } = await import("execa");
7662
+ if (!existsSync15(thread.worktreePath)) {
7663
+ const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-PEUPFAKL.js");
7494
7664
  const slug = thread.worktreePath.split("/").pop();
7495
7665
  const dest = thread.worktreePath;
7496
- await execa5("git", ["worktree", "add", dest, thread.branchName], {
7497
- cwd: thread.repoPath
7666
+ await withRepoGitLock(thread.repoPath, async () => {
7667
+ await git(["worktree", "add", dest, thread.branchName], thread.repoPath);
7498
7668
  });
7499
7669
  void createThreadWorktree2;
7500
7670
  void slug;
@@ -7598,7 +7768,7 @@ async function startOrchestration(opts) {
7598
7768
  sourceRef: "default",
7599
7769
  ...createOpts
7600
7770
  }).catch(async () => {
7601
- const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-DA4BOV7G.js");
7771
+ const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-PEUPFAKL.js");
7602
7772
  const repo = await resolveRepoRoot2(repoPath);
7603
7773
  const def = await resolveDefaultBranch2(repo);
7604
7774
  return orch.createThread({
@@ -7608,7 +7778,7 @@ async function startOrchestration(opts) {
7608
7778
  repoPath: repo
7609
7779
  });
7610
7780
  });
7611
- const { updateThread: upd } = await import("./thread-store-XAKWCV2E.js");
7781
+ const { updateThread: upd } = await import("./thread-store-4OUEQ2DB.js");
7612
7782
  const updated = upd(thread.id, {
7613
7783
  sourceType: "orchestration",
7614
7784
  sourceRef: goal
@@ -7731,6 +7901,15 @@ function mcpArchiveBlockedReason(thread) {
7731
7901
  return null;
7732
7902
  }
7733
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
+
7734
7913
  // src/mcp/slack-tools.ts
7735
7914
  import { z } from "zod";
7736
7915
 
@@ -8358,7 +8537,7 @@ async function startMcpServer() {
8358
8537
  );
8359
8538
  }
8360
8539
  try {
8361
- const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-BRF7VKTQ.js");
8540
+ const { maxConcurrentAgents: maxConcurrentAgents2 } = await import("./app-settings-EKJM2344.js");
8362
8541
  orch.setMaxConcurrent(maxConcurrentAgents2());
8363
8542
  } catch {
8364
8543
  }
@@ -8401,7 +8580,9 @@ async function startMcpServer() {
8401
8580
  const threads = orch.getThreads(true);
8402
8581
  const lines = threads.map((t) => {
8403
8582
  const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : basename4(t.repoPath) || t.repoPath;
8404
- 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}`;
8405
8586
  });
8406
8587
  return {
8407
8588
  content: [{ type: "text", text: lines.join("\n") || "(no threads)" }]
@@ -8410,13 +8591,14 @@ async function startMcpServer() {
8410
8591
  );
8411
8592
  server.tool(
8412
8593
  "get_thread",
8413
- "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.",
8414
8595
  { ref: z3.string() },
8415
8596
  async ({ ref }) => {
8416
8597
  const t = orch.getThread(ref);
8417
8598
  if (!t) {
8418
8599
  return { content: [{ type: "text", text: `Thread not found: ${ref}` }], isError: true };
8419
8600
  }
8601
+ const live = t.status === "running" || t.status === "queued" ? readTurnLive(t.id) : null;
8420
8602
  const summary = {
8421
8603
  id: t.id,
8422
8604
  title: t.title,
@@ -8431,7 +8613,10 @@ async function startMcpServer() {
8431
8613
  messageCount: t.messages.length,
8432
8614
  devPort: t.devPort,
8433
8615
  prUrl: t.prUrl,
8434
- 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
8435
8620
  };
8436
8621
  return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
8437
8622
  }
@@ -8567,8 +8752,8 @@ async function startMcpServer() {
8567
8752
  registerSlackTools(server);
8568
8753
  registerLinearTools(server);
8569
8754
  if (sideboardMcpProfile() !== "worktree") {
8570
- const { getCaffeinateHold: getCaffeinateHold2, setCaffeinateHold: setCaffeinateHold2 } = await import("./caffeinate-hold-I7SG6UQR.js");
8571
- 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");
8572
8757
  const accountDefaults = resolveThreadDefaults2();
8573
8758
  const accountDefaultsHint = `Account defaults: agent=${accountDefaults.agent}, model=${accountDefaults.model?.trim() || "Auto"}, effort=${accountDefaults.effort}`;
8574
8759
  server.tool(
@@ -8757,13 +8942,15 @@ async function startMcpServer() {
8757
8942
  );
8758
8943
  server.tool(
8759
8944
  "wait_for_turn",
8760
- "Block until the thread finishes its current/queued turn (avoids polling). Use after send_to_thread or ask_git to read the agent reply.",
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.",
8761
8946
  {
8762
8947
  ref: z3.string(),
8763
8948
  timeoutMs: z3.number().optional()
8764
8949
  },
8765
8950
  async ({ ref, timeoutMs }) => {
8766
- const thread = await orch.waitForTurn(ref, timeoutMs ?? 6e5);
8951
+ const thread = await orch.waitForTurn(ref, mcpWaitForTurnTimeoutMs(timeoutMs), {
8952
+ resolveIfStillRunning: true
8953
+ });
8767
8954
  const result = orch.getTurnResult(thread.id);
8768
8955
  return {
8769
8956
  content: [
@@ -8772,7 +8959,12 @@ async function startMcpServer() {
8772
8959
  text: JSON.stringify({
8773
8960
  id: thread.id,
8774
8961
  status: result.status,
8775
- text: result.text
8962
+ text: result.text,
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
8776
8968
  })
8777
8969
  }
8778
8970
  ]
@@ -8781,11 +8973,21 @@ async function startMcpServer() {
8781
8973
  );
8782
8974
  server.tool(
8783
8975
  "get_turn_result",
8784
- "Final assistant message only (not full transcript)",
8976
+ "Assistant message when the turn finished, or live progress while stillRunning. Not the full transcript.",
8785
8977
  { ref: z3.string() },
8786
8978
  async ({ ref }) => {
8787
8979
  const result = orch.getTurnResult(ref);
8788
- 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
+ };
8789
8991
  }
8790
8992
  );
8791
8993
  server.tool(
@@ -8902,7 +9104,7 @@ async function startMcpServer() {
8902
9104
  );
8903
9105
  server.tool(
8904
9106
  "request_review",
8905
- '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.',
8906
9108
  { ref: z3.string().describe("Worktree thread id/ref to review") },
8907
9109
  async ({ ref }) => {
8908
9110
  try {
@@ -8930,7 +9132,7 @@ async function startMcpServer() {
8930
9132
  );
8931
9133
  server.tool(
8932
9134
  "ask_git",
8933
- "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.",
8934
9136
  {
8935
9137
  ref: z3.string().describe("Worktree thread id/ref"),
8936
9138
  action: z3.enum(AGENT_GIT_ACTIONS).describe(
@@ -8981,7 +9183,7 @@ async function startMcpServer() {
8981
9183
  );
8982
9184
  server.tool(
8983
9185
  "fork_worktree",
8984
- "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.",
8985
9187
  {
8986
9188
  ref: z3.string().describe("Worktree thread id/ref to fork"),
8987
9189
  through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -9026,7 +9228,7 @@ async function startMcpServer() {
9026
9228
  );
9027
9229
  server.tool(
9028
9230
  "fork_chat",
9029
- "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.",
9030
9232
  {
9031
9233
  ref: z3.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
9032
9234
  through_index: z3.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
@@ -9693,7 +9895,7 @@ async function runCloudConnect(opts) {
9693
9895
  }
9694
9896
 
9695
9897
  // src/agents/user-mcp-config.ts
9696
- 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";
9697
9899
  import { homedir as homedir2 } from "os";
9698
9900
  import { dirname as dirname4, join as join14 } from "path";
9699
9901
  function userCursorMcpConfigPath() {
@@ -9727,16 +9929,16 @@ function mergeSideboardIntoMcpServersJson(existing, sideboard) {
9727
9929
  }
9728
9930
  function writeMergedMcpServersJson(configPath, sideboard) {
9729
9931
  let existing = {};
9730
- if (existsSync15(configPath)) {
9932
+ if (existsSync16(configPath)) {
9731
9933
  try {
9732
- existing = JSON.parse(readFileSync11(configPath, "utf8"));
9934
+ existing = JSON.parse(readFileSync12(configPath, "utf8"));
9733
9935
  } catch {
9734
9936
  existing = {};
9735
9937
  }
9736
9938
  }
9737
9939
  const next = mergeSideboardIntoMcpServersJson(existing, sideboard);
9738
9940
  mkdirSync5(dirname4(configPath), { recursive: true });
9739
- writeFileSync5(configPath, `${JSON.stringify(next, null, 2)}
9941
+ writeFileSync6(configPath, `${JSON.stringify(next, null, 2)}
9740
9942
  `);
9741
9943
  }
9742
9944
  function launchFromResolved(server) {
@@ -9754,7 +9956,7 @@ async function registerPackagedUserMcpClients() {
9754
9956
  const cursor = userCursorMcpConfigPath();
9755
9957
  writeMergedMcpServersJson(cursor, launch);
9756
9958
  const claude = userClaudeMcpConfigPath();
9757
- if (existsSync15(claude)) {
9959
+ if (existsSync16(claude)) {
9758
9960
  writeMergedMcpServersJson(claude, launch);
9759
9961
  return { cursor, claude };
9760
9962
  }
@@ -10252,6 +10454,8 @@ function connectSession(ws, opts, log) {
10252
10454
 
10253
10455
  // src/slack/relay-protocol.ts
10254
10456
  import { randomBytes as randomBytes3 } from "crypto";
10457
+ var SLACK_RELAY_PING_INTERVAL_MS = 2e4;
10458
+ var SLACK_RELAY_PONG_TIMEOUT_MS = 15e3;
10255
10459
  function slackRelaySessionKey(teamId, userId, deviceId) {
10256
10460
  return `${teamId.trim()}:${userId.trim()}:${deviceId.trim()}`;
10257
10461
  }
@@ -10437,10 +10641,21 @@ function connectSession2(ws, opts, log) {
10437
10641
  const allowedUsers = new Set(
10438
10642
  opts.workspaces.map((w) => `${w.teamId.trim()}:${w.userId.trim()}`)
10439
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
+ };
10440
10654
  const finish = (err) => {
10441
10655
  if (settled) return;
10442
10656
  settled = true;
10443
10657
  pending.clear();
10658
+ stopKeepalive();
10444
10659
  opts.signal?.removeEventListener("abort", onAbort);
10445
10660
  if (err) reject(err);
10446
10661
  else resolve();
@@ -10453,6 +10668,25 @@ function connectSession2(ws, opts, log) {
10453
10668
  finish();
10454
10669
  };
10455
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
+ };
10456
10690
  ws.addEventListener("open", () => {
10457
10691
  const label = opts.deviceLabel?.trim() || opts.deviceId.trim();
10458
10692
  log(`Relay connected \xB7 ${label}`);
@@ -10467,6 +10701,8 @@ function connectSession2(ws, opts, log) {
10467
10701
  userToken: wsInfo.userToken
10468
10702
  });
10469
10703
  }
10704
+ pingOnce();
10705
+ pingTimer = setInterval(pingOnce, pingMs);
10470
10706
  });
10471
10707
  ws.addEventListener("error", () => {
10472
10708
  finish(new Error("WebSocket error"));
@@ -10478,7 +10714,13 @@ function connectSession2(ws, opts, log) {
10478
10714
  const raw = typeof ev.data === "string" ? ev.data : String(ev.data ?? "");
10479
10715
  const msg = parseSlackRelayServerMessage(raw);
10480
10716
  if (!msg) return;
10481
- if (msg.type === "pong") return;
10717
+ if (msg.type === "pong") {
10718
+ if (pongTimer) {
10719
+ clearTimeout(pongTimer);
10720
+ pongTimer = null;
10721
+ }
10722
+ return;
10723
+ }
10482
10724
  if (msg.type === "error") {
10483
10725
  log(`relay error: ${msg.message}`);
10484
10726
  return;
@@ -10601,6 +10843,13 @@ function slackReplyThreadTs(msg) {
10601
10843
  if (msg.kind === "mention") return msg.ts;
10602
10844
  return void 0;
10603
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
+ }
10604
10853
  var SLACK_SEEN_REACTION = "eyes";
10605
10854
  function writeTokenForTeam(teamId) {
10606
10855
  const workspaces = listSlackWorkspacesRaw();
@@ -10637,8 +10886,8 @@ async function ackSlackInboundSeen(msg, opts) {
10637
10886
  log(`react error: ${errMsg}${hint}`);
10638
10887
  }
10639
10888
  }
10640
- async function postSlackText(target, text3, opts) {
10641
- const stub = {
10889
+ function replyStub(target) {
10890
+ return {
10642
10891
  teamId: target.teamId,
10643
10892
  channelId: target.channelId,
10644
10893
  ts: target.threadTs ?? "",
@@ -10646,12 +10895,15 @@ async function postSlackText(target, text3, opts) {
10646
10895
  text: "",
10647
10896
  kind: "dm"
10648
10897
  };
10898
+ }
10899
+ async function postSlackText(target, text3, opts) {
10649
10900
  if (opts.postReply) {
10650
- await opts.postReply(stub, text3);
10651
- 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;
10652
10904
  }
10653
10905
  const token = writeTokenForTeam(target.teamId);
10654
- await slackApi(
10906
+ const json = await slackApi(
10655
10907
  token,
10656
10908
  "chat.postMessage",
10657
10909
  {
@@ -10661,18 +10913,163 @@ async function postSlackText(target, text3, opts) {
10661
10913
  },
10662
10914
  opts.fetchImpl
10663
10915
  );
10916
+ const ts = json.ts?.trim();
10917
+ return ts ? { ts } : null;
10664
10918
  }
10665
- async function postSlackReply(msg, text3, opts) {
10666
- 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",
10667
10928
  {
10668
- teamId: msg.teamId,
10669
- channelId: msg.channelId,
10670
- threadTs: slackReplyThreadTs(msg)
10929
+ channel: target.channelId,
10930
+ ts,
10931
+ text: text3
10671
10932
  },
10672
- signForThisMac(text3),
10673
- opts
10933
+ opts.fetchImpl
10934
+ );
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
10674
10950
  );
10675
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
+ }
10676
11073
  var lastRelayed = /* @__PURE__ */ new Map();
10677
11074
  function markRelayed(threadId, text3) {
10678
11075
  lastRelayed.set(threadId, `${threadId}:${text3}`);
@@ -10764,10 +11161,15 @@ async function handleSlackInbound(msg, opts) {
10764
11161
  });
10765
11162
  };
10766
11163
  bindReplyTarget(fresh.id);
11164
+ const progress = startSlackTurnProgress(msg, () => fresh.id, opts);
10767
11165
  const runTurn = async (threadId) => {
10768
11166
  await orch.send(threadId, prompt);
10769
11167
  await orch.waitForTurn(threadId, 14 * 60 * 1e3);
10770
11168
  };
11169
+ const dropProgress = async (reason) => {
11170
+ log(reason);
11171
+ await progress.discard();
11172
+ };
10771
11173
  let reply;
10772
11174
  try {
10773
11175
  try {
@@ -10780,12 +11182,12 @@ async function handleSlackInbound(msg, opts) {
10780
11182
  await runTurn(fresh.id);
10781
11183
  }
10782
11184
  if (slackInboundSuperseded(opts)) {
10783
- log(`turn finished ${msg.ts} (superseded)`);
11185
+ await dropProgress(`turn finished ${msg.ts} (superseded)`);
10784
11186
  return;
10785
11187
  }
10786
11188
  let after = readThread(fresh.id);
10787
11189
  if (after?.status === "stopped") {
10788
- log(`turn finished ${msg.ts} (interrupted, skip post)`);
11190
+ await dropProgress(`turn finished ${msg.ts} (interrupted, skip post)`);
10789
11191
  return;
10790
11192
  }
10791
11193
  if (!after || after.status === "archived") {
@@ -10794,46 +11196,48 @@ async function handleSlackInbound(msg, opts) {
10794
11196
  bindReplyTarget(fresh.id);
10795
11197
  await runTurn(fresh.id);
10796
11198
  if (slackInboundSuperseded(opts)) {
10797
- log(`turn finished ${msg.ts} (superseded)`);
11199
+ await dropProgress(`turn finished ${msg.ts} (superseded)`);
10798
11200
  return;
10799
11201
  }
10800
11202
  after = readThread(fresh.id);
10801
11203
  if (!after || after.status === "stopped" || after.status === "archived") {
10802
- log(`turn finished ${msg.ts} (interrupted, skip post)`);
11204
+ await dropProgress(`turn finished ${msg.ts} (interrupted, skip post)`);
10803
11205
  return;
10804
11206
  }
10805
11207
  }
10806
11208
  reply = orch.getTurnResult(fresh.id).text.trim();
10807
11209
  } catch (err) {
10808
11210
  if (slackInboundSuperseded(opts)) {
10809
- log(`turn error ${msg.ts} (superseded)`);
11211
+ await dropProgress(`turn error ${msg.ts} (superseded)`);
10810
11212
  return;
10811
11213
  }
10812
11214
  const errMsg = err instanceof Error ? err.message : String(err);
10813
11215
  if (slackCoordinatorGone(err)) {
10814
- log(`turn finished ${msg.ts} (thread gone, skip post)`);
11216
+ await dropProgress(`turn finished ${msg.ts} (thread gone, skip post)`);
10815
11217
  return;
10816
11218
  }
10817
11219
  const timedOut = /timed out|timeout/i.test(errMsg);
10818
11220
  const errorReply = timedOut ? SLACK_LISTEN_TIMEOUT_REPLY : `Sideboard coordinator error: ${errMsg}`;
10819
- await postSlackReply(msg, errorReply, opts).catch((postErr) => {
11221
+ try {
11222
+ await progress.finishWith(errorReply);
11223
+ log(`replied error ${msg.ts}: ${errMsg}`);
11224
+ } catch (postErr) {
10820
11225
  const postMsg = postErr instanceof Error ? postErr.message : String(postErr);
10821
11226
  log(`post error: ${postMsg}`);
10822
- });
10823
- log(`replied error ${msg.ts}: ${errMsg}`);
11227
+ }
10824
11228
  if (!timedOut) throw err;
10825
11229
  return;
10826
11230
  }
10827
11231
  if (!reply) {
10828
- log(`post error: no agent text to send for ${msg.ts}`);
11232
+ await dropProgress(`post error: no agent text to send for ${msg.ts}`);
10829
11233
  return;
10830
11234
  }
10831
11235
  if (alreadyRelayed(fresh.id, reply)) {
10832
- log(`turn finished ${msg.ts} (already posted)`);
11236
+ await dropProgress(`turn finished ${msg.ts} (already posted)`);
10833
11237
  return;
10834
11238
  }
10835
11239
  try {
10836
- await postSlackReply(msg, reply, opts);
11240
+ await progress.finishWith(reply);
10837
11241
  markRelayed(fresh.id, reply);
10838
11242
  log(`replied ${msg.ts} (${reply.length} chars)`);
10839
11243
  } catch (err) {
@@ -11384,6 +11788,20 @@ async function startSlackRelayServer(opts) {
11384
11788
  }
11385
11789
  }
11386
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);
11387
11805
  log("desktop connected");
11388
11806
  ws.on("message", (data) => {
11389
11807
  const raw = typeof data === "string" ? data : data.toString("utf8");
@@ -11494,6 +11912,8 @@ export {
11494
11912
  SLACK_LISTEN_TIMEOUT_REPLY,
11495
11913
  SLACK_OAUTH_CANCELLED,
11496
11914
  SLACK_OAUTH_REDIRECT,
11915
+ SLACK_PROGRESS_DELAY_MS,
11916
+ SLACK_PROGRESS_EDIT_MS,
11497
11917
  SLACK_REPLY_FORMATTING,
11498
11918
  SLACK_SEEN_REACTION,
11499
11919
  SlackOAuthCancelledError,
@@ -11651,6 +12071,7 @@ export {
11651
12071
  formatSlackInboundPrompt,
11652
12072
  formatSlackRepliesForTurn,
11653
12073
  formatSlackSignedReply,
12074
+ formatSlackWorkingText,
11654
12075
  formatTranscriptMarkdown,
11655
12076
  formatUiReminder,
11656
12077
  formatWorkspaceInventory,
@@ -11946,6 +12367,7 @@ export {
11946
12367
  thisProcessShouldDrainAgentQueues,
11947
12368
  threadDisplayLabel,
11948
12369
  threadFilePath,
12370
+ threadLivePath,
11949
12371
  threadLockPath,
11950
12372
  threadRequestsBrightsyMcp,
11951
12373
  threadsDir,
@@ -11969,6 +12391,7 @@ export {
11969
12391
  userClaudeMcpConfigPath,
11970
12392
  userCursorMcpConfigPath,
11971
12393
  validateLinearApiKey,
12394
+ waitForPidExit,
11972
12395
  warmGithubAgentAuth,
11973
12396
  withAgentInstructions,
11974
12397
  withExportedPath,