@sideboard-ai/core 0.1.44 → 0.1.45

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.
@@ -3,7 +3,7 @@ import {
3
3
  ensureWorkspace,
4
4
  removeWorkspace,
5
5
  syncWorkspacesFromThreads
6
- } from "./chunk-TXFJEXFB.js";
6
+ } from "./chunk-U3EQKJHA.js";
7
7
  import {
8
8
  GLOBAL_WORKSPACE_ID,
9
9
  createGlobalChat,
@@ -13,34 +13,41 @@ import {
13
13
  isGlobalThread,
14
14
  isOrchestratorThread,
15
15
  orchestratorSessionPoisonedByBuiltins
16
- } from "./chunk-I3PKMLFW.js";
16
+ } from "./chunk-ZNSM2DDD.js";
17
17
  import {
18
18
  coordinatorSystemPrompt,
19
19
  coordinatorTurnReminder,
20
20
  enrichWorkspacesWithGithub,
21
21
  ensureGlobalCoordinatorCwd
22
- } from "./chunk-JM2TVGNW.js";
22
+ } from "./chunk-6NAPN2N5.js";
23
23
  import {
24
24
  PLAN_MODE_INSTRUCTION,
25
25
  allAdapters,
26
26
  getAdapter,
27
- parseBrightsyCliLine
28
- } from "./chunk-XX26NCB6.js";
27
+ isSessionQuotaLimit,
28
+ listModelsForAgent,
29
+ parseBrightsyCliLine,
30
+ parseSessionQuotaResetAt,
31
+ resolveQuotaFallbackAgent
32
+ } from "./chunk-ANZ566Z5.js";
29
33
  import {
30
34
  fallbackTurnFailDetail,
31
35
  formatTurnExitError,
36
+ humanizeAgentFailDetail,
32
37
  looksLikeAgentFailureMessage,
33
38
  pushTurnStderr,
34
39
  summarizeTurnStderr
35
- } from "./chunk-PU27NUO4.js";
40
+ } from "./chunk-J5JTEJ5O.js";
36
41
  import {
37
42
  childEnvWithAppSettings,
38
43
  getLinearApiKey,
39
44
  isLinearConnected,
40
45
  loadAppSettings,
46
+ orchestrationQuotaFallbackAgent,
47
+ orchestrationQuotaOnLimit,
41
48
  resolveEffectiveIssueSource,
42
49
  updateAdvancedSettings
43
- } from "./chunk-FSIK442J.js";
50
+ } from "./chunk-WYY3J7GR.js";
44
51
  import {
45
52
  allocateTeamName,
46
53
  allocateTeamSlug,
@@ -71,7 +78,7 @@ import {
71
78
  takenSlugsFromThread,
72
79
  threadDisplayLabel,
73
80
  worktreeNameFromPath
74
- } from "./chunk-7PCTK4WO.js";
81
+ } from "./chunk-FV6FN6V5.js";
75
82
  import {
76
83
  appendMessage,
77
84
  createEmptyThread,
@@ -83,7 +90,7 @@ import {
83
90
  updateThread,
84
91
  withThreadLock,
85
92
  writeThread
86
- } from "./chunk-ENSD62HW.js";
93
+ } from "./chunk-O6W3P7V3.js";
87
94
  import {
88
95
  loadRepoSettings,
89
96
  loadWorkspaceSettings,
@@ -459,9 +466,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
459
466
  `Cannot spawn ${thread.agent}: thread ${thread.id} has no worktreePath`
460
467
  );
461
468
  }
462
- const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-FU6UIPDY.js");
469
+ const { isGlobalThread: isGlobalThread2 } = await import("./global-workspace-YFOQUGWD.js");
463
470
  if (isGlobalThread2(thread)) {
464
- const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-I5YNS27B.js");
471
+ const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await import("./coordinator-prompt-2XFYG3C5.js");
465
472
  ensureGlobalCoordinatorCwd2();
466
473
  }
467
474
  const adapter = getAdapter(thread.agent);
@@ -2640,7 +2647,7 @@ async function createThread(input, onSetupLine) {
2640
2647
  return readThread(thread.id) ?? thread;
2641
2648
  }
2642
2649
  async function listLinearIssues(agent, repoPath) {
2643
- const { getAdapter: getAdapter2 } = await import("./agents-PP3URTSF.js");
2650
+ const { getAdapter: getAdapter2 } = await import("./agents-JSHCAZUZ.js");
2644
2651
  await requireAgent(agent, { requireLinear: true });
2645
2652
  const adapter = getAdapter2(agent);
2646
2653
  if (!adapter.listLinearIssues) {
@@ -2735,12 +2742,19 @@ function forkChatTab(input) {
2735
2742
  const from = requireThread(input.threadId);
2736
2743
  const slice = forkMessageSlice(from, input.throughIndex);
2737
2744
  const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
2738
- return createChatTab({
2745
+ const tab = createChatTab({
2739
2746
  fromThreadId: input.threadId,
2740
2747
  agent: input.agent ?? from.agent,
2748
+ model: input.model,
2741
2749
  title: input.title?.trim() || void 0,
2742
2750
  attachments: [attachment]
2743
2751
  });
2752
+ if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
2753
+ const next = { ...tab, parentThreadId: from.id };
2754
+ writeThread(next);
2755
+ return next;
2756
+ }
2757
+ return tab;
2744
2758
  }
2745
2759
 
2746
2760
  // src/threads/fork-worktree.ts
@@ -2751,16 +2765,28 @@ function requireThread2(idOrRef) {
2751
2765
  }
2752
2766
  async function forkThreadWorktree(input, onSetupLine) {
2753
2767
  const from = requireThread2(input.threadId);
2768
+ if (isOrchestratorThread(from)) {
2769
+ throw new Error(
2770
+ "fork_worktree targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
2771
+ );
2772
+ }
2773
+ if (!from.branchName?.trim() || !from.repoPath?.trim()) {
2774
+ throw new Error(
2775
+ `Cannot fork worktree: thread ${from.id} has no branch/repo (need a real worktree chat).`
2776
+ );
2777
+ }
2754
2778
  const slice = forkMessageSlice(from, input.throughIndex);
2755
2779
  const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
2780
+ const nextAgent = input.agent ?? from.agent;
2781
+ const nextModel = input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model;
2756
2782
  const thread = await createThread(
2757
2783
  {
2758
2784
  sourceType: "branch",
2759
2785
  sourceRef: from.branchName,
2760
2786
  repoPath: from.repoPath,
2761
- agent: input.agent ?? from.agent,
2787
+ agent: nextAgent,
2762
2788
  autonomy: from.autonomy,
2763
- model: from.model,
2789
+ model: nextModel,
2764
2790
  effort: from.effort,
2765
2791
  fast: from.fast,
2766
2792
  planMode: from.planMode,
@@ -2862,7 +2888,7 @@ async function adoptThread(input) {
2862
2888
  messages: input.messages ?? []
2863
2889
  });
2864
2890
  writeThread(thread);
2865
- const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-RSEDTBGJ.js");
2891
+ const { ensureWorkspace: ensureWorkspace2 } = await import("./workspaces-TIKLNDW3.js");
2866
2892
  await ensureWorkspace2(repoPath);
2867
2893
  return thread;
2868
2894
  }
@@ -3343,6 +3369,111 @@ async function requestReview(threadRef, send) {
3343
3369
  import { EventEmitter } from "events";
3344
3370
  import { existsSync as existsSync12 } from "fs";
3345
3371
 
3372
+ // src/orchestrator/quota-failover.ts
3373
+ import { randomUUID as randomUUID4 } from "crypto";
3374
+ function planOrchestrationQuotaFailover(thread, limitText, opts) {
3375
+ if (!isOrchestratorThread(thread)) return null;
3376
+ if (!isSessionQuotaLimit(limitText)) return null;
3377
+ const onLimit = opts?.onLimit ?? orchestrationQuotaOnLimit();
3378
+ const resumeAt = parseSessionQuotaResetAt(limitText, opts?.now);
3379
+ if (thread.quotaContinuedFromId) {
3380
+ if (resumeAt) {
3381
+ return {
3382
+ action: "wait_reset",
3383
+ reason: "Already continued once; waiting for quota reset instead.",
3384
+ limitText,
3385
+ resumeAt
3386
+ };
3387
+ }
3388
+ return {
3389
+ action: "none",
3390
+ reason: "Already continued once; no parseable reset time.",
3391
+ limitText
3392
+ };
3393
+ }
3394
+ if (onLimit === "wait_reset") {
3395
+ if (!resumeAt) {
3396
+ return {
3397
+ action: "none",
3398
+ reason: "wait_reset configured but reset time could not be parsed.",
3399
+ limitText
3400
+ };
3401
+ }
3402
+ return {
3403
+ action: "wait_reset",
3404
+ reason: "Settings: wait for quota reset.",
3405
+ limitText,
3406
+ resumeAt
3407
+ };
3408
+ }
3409
+ const preferred = opts?.fallbackAgent ?? orchestrationQuotaFallbackAgent();
3410
+ const fallbackAgent = resolveQuotaFallbackAgent(thread.agent, preferred);
3411
+ return {
3412
+ action: "switch_agent",
3413
+ reason: `Continue on ${fallbackAgent} (Auto) after ${thread.agent} session limit.`,
3414
+ limitText,
3415
+ fallbackAgent
3416
+ };
3417
+ }
3418
+ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
3419
+ const children = listThreads({ includeArchived: false }).filter((t) => t.parentThreadId === from.id && t.status !== "archived").slice(0, 40).map(
3420
+ (t) => `- ${t.title} \xB7 ${t.status} \xB7 ${t.agent} \xB7 sideboard://thread/${t.id}`
3421
+ );
3422
+ const recent = from.messages.slice(-8).map((m) => {
3423
+ const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
3424
+ const text = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
3425
+ return text ? `- ${role}: ${text}` : null;
3426
+ }).filter(Boolean);
3427
+ const body = [
3428
+ `# Orchestration handoff`,
3429
+ "",
3430
+ `Previous chat: ${from.title} (\`${from.id}\`) on **${from.agent}** hit a session/usage limit.`,
3431
+ `Limit: ${limitText.trim()}`,
3432
+ `Continuing on **${fallbackAgent}** with Auto model.`,
3433
+ "",
3434
+ `## Goal`,
3435
+ from.sourceRef?.trim() || "(none)",
3436
+ "",
3437
+ `## Child threads`,
3438
+ children.length ? children.join("\n") : "(none listed \u2014 call list_threads)",
3439
+ "",
3440
+ `## Recent turns (truncated)`,
3441
+ recent.length ? recent.join("\n") : "(none)",
3442
+ "",
3443
+ `## Instructions`,
3444
+ `- Continue fleet orchestration from this handoff.`,
3445
+ `- Prefer Sideboard MCP (list_threads, get_thread, send_to_thread, \u2026) for live status.`,
3446
+ `- Leave model Auto unless there is a specific reason to pin one.`,
3447
+ `- Do not wait on the limited ${from.agent} account; keep going on ${fallbackAgent}.`
3448
+ ].join("\n");
3449
+ return {
3450
+ id: randomUUID4(),
3451
+ name: "Orchestration quota handoff.md",
3452
+ kind: "transcript",
3453
+ content: body
3454
+ };
3455
+ }
3456
+ var QUOTA_CONTINUE_PROMPT = (fromAgent, fallback) => [
3457
+ `${fromAgent} hit a session/usage limit. Continue this orchestration on ${fallback} using the attached handoff.`,
3458
+ "Call list_threads for live fleet status, then proceed with the goal. Leave model Auto unless needed."
3459
+ ].join(" ");
3460
+ var QUOTA_RESUME_PROMPT = "Session/usage limit window should have reset. Continue the orchestration from where you left off. Use list_threads for fleet status.";
3461
+ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
3462
+ const handoff = buildQuotaHandoffAttachment(from, limitText, fallbackAgent);
3463
+ const tab = createChatTab({
3464
+ fromThreadId: from.id,
3465
+ agent: fallbackAgent,
3466
+ model: null,
3467
+ attachments: [handoff]
3468
+ });
3469
+ return updateThread(tab.id, {
3470
+ parentThreadId: from.id,
3471
+ quotaContinuedFromId: from.id,
3472
+ sourceRef: from.sourceRef,
3473
+ sourceType: "orchestration"
3474
+ });
3475
+ }
3476
+
3346
3477
  // src/threads/sync-branch.ts
3347
3478
  async function syncThreadBranchFromGit(threadId) {
3348
3479
  const thread = readThread(threadId);
@@ -3403,6 +3534,8 @@ var Orchestrator = class {
3403
3534
  haltDrain = /* @__PURE__ */ new Set();
3404
3535
  /** WIP snapshot SHA at the start of the latest agent turn (per thread). */
3405
3536
  turnBaselines = /* @__PURE__ */ new Map();
3537
+ /** Timers for orchestration session-quota auto-resume. */
3538
+ quotaResumeTimers = /* @__PURE__ */ new Map();
3406
3539
  maxConcurrent;
3407
3540
  runningCount = 0;
3408
3541
  constructor(opts) {
@@ -3471,7 +3604,7 @@ var Orchestrator = class {
3471
3604
  orphans: orphans.map((o) => ({ path: o.path, repoPath: o.repoPath }))
3472
3605
  });
3473
3606
  }
3474
- const { autoCleanupOrphansEnabled } = await import("./app-settings-ZKVZHJPQ.js");
3607
+ const { autoCleanupOrphansEnabled } = await import("./app-settings-LYGVDGZY.js");
3475
3608
  if (autoCleanupOrphansEnabled() && shouldRunWorktreeCleanup() && orphans.length > 0) {
3476
3609
  await cleanupOrphanWorktrees({ repoPaths });
3477
3610
  }
@@ -3482,6 +3615,113 @@ var Orchestrator = class {
3482
3615
  void this.drainQueue(thread.id);
3483
3616
  }
3484
3617
  }
3618
+ this.schedulePendingQuotaResumes();
3619
+ }
3620
+ clearQuotaResumeTimer(threadId) {
3621
+ const timer = this.quotaResumeTimers.get(threadId);
3622
+ if (timer) clearTimeout(timer);
3623
+ this.quotaResumeTimers.delete(threadId);
3624
+ }
3625
+ /** Schedule (or fire) auto-retry after a provider session/usage limit reset. */
3626
+ scheduleQuotaResume(threadId, resumeAt) {
3627
+ this.clearQuotaResumeTimer(threadId);
3628
+ updateThread(threadId, { quotaResumeAt: resumeAt.toISOString() });
3629
+ const delay = Math.max(5e3, resumeAt.getTime() - Date.now());
3630
+ const capped = Math.min(delay, 2147483647);
3631
+ const timer = setTimeout(() => {
3632
+ this.quotaResumeTimers.delete(threadId);
3633
+ void this.resumeAfterQuotaWait(threadId);
3634
+ }, capped);
3635
+ this.quotaResumeTimers.set(threadId, timer);
3636
+ }
3637
+ schedulePendingQuotaResumes() {
3638
+ for (const thread of listThreads({ includeArchived: false })) {
3639
+ if (!thread.quotaResumeAt) continue;
3640
+ const at = new Date(thread.quotaResumeAt);
3641
+ if (Number.isNaN(at.getTime())) continue;
3642
+ if (at.getTime() <= Date.now()) {
3643
+ void this.resumeAfterQuotaWait(thread.id);
3644
+ } else if (!this.quotaResumeTimers.has(thread.id)) {
3645
+ this.scheduleQuotaResume(thread.id, at);
3646
+ }
3647
+ }
3648
+ }
3649
+ async resumeAfterQuotaWait(threadId) {
3650
+ const thread = findThreadByRef(threadId);
3651
+ if (!thread || thread.status === "archived") return;
3652
+ this.clearQuotaResumeTimer(threadId);
3653
+ try {
3654
+ updateThread(threadId, { quotaResumeAt: null });
3655
+ } catch {
3656
+ return;
3657
+ }
3658
+ if (thread.status === "running" || this.activeTurns.has(threadId) || this.startingTurns.has(threadId)) {
3659
+ return;
3660
+ }
3661
+ await this.send(threadId, QUOTA_RESUME_PROMPT);
3662
+ }
3663
+ /**
3664
+ * Host-side continue when an orchestration chat hits a provider session/usage
3665
+ * limit (not context size): switch agent (Auto) or wait until reset.
3666
+ */
3667
+ async maybeHandleOrchestrationQuotaFailover(threadId, limitText) {
3668
+ const thread = findThreadByRef(threadId);
3669
+ if (!thread) return;
3670
+ const plan = planOrchestrationQuotaFailover(thread, limitText);
3671
+ if (!plan || plan.action === "none") return;
3672
+ if (plan.action === "wait_reset" && plan.resumeAt) {
3673
+ this.haltDrain.add(threadId);
3674
+ this.scheduleQuotaResume(threadId, plan.resumeAt);
3675
+ setStatus(threadId, "idle", null);
3676
+ appendMessage(threadId, {
3677
+ role: "agent",
3678
+ text: `Sideboard will auto-retry this orchestration around ${plan.resumeAt.toLocaleString()} when the session limit resets.`,
3679
+ ts: (/* @__PURE__ */ new Date()).toISOString()
3680
+ });
3681
+ this.emit({
3682
+ type: "quota_failover",
3683
+ threadId,
3684
+ action: "wait_reset",
3685
+ message: plan.reason,
3686
+ resumeAt: plan.resumeAt.toISOString()
3687
+ });
3688
+ this.emit({ type: "status_changed", threadId, status: "idle" });
3689
+ return;
3690
+ }
3691
+ if (plan.action === "switch_agent" && plan.fallbackAgent) {
3692
+ this.haltDrain.add(threadId);
3693
+ const next = createQuotaFailoverChat(
3694
+ thread,
3695
+ plan.fallbackAgent,
3696
+ plan.limitText
3697
+ );
3698
+ this.clearQuotaResumeTimer(threadId);
3699
+ try {
3700
+ updateThread(threadId, { quotaResumeAt: null });
3701
+ } catch {
3702
+ }
3703
+ appendMessage(threadId, {
3704
+ role: "agent",
3705
+ text: `Session limit on ${thread.agent}. Sideboard continued on ${plan.fallbackAgent} (Auto) in [${next.title}](sideboard://thread/${next.id}).`,
3706
+ ts: (/* @__PURE__ */ new Date()).toISOString()
3707
+ });
3708
+ this.emit({
3709
+ type: "quota_failover",
3710
+ threadId,
3711
+ action: "switch_agent",
3712
+ toThreadId: next.id,
3713
+ message: plan.reason
3714
+ });
3715
+ this.emit({
3716
+ type: "status_changed",
3717
+ threadId: next.id,
3718
+ status: next.status
3719
+ });
3720
+ await this.send(
3721
+ next.id,
3722
+ QUOTA_CONTINUE_PROMPT(thread.agent, plan.fallbackAgent)
3723
+ );
3724
+ }
3485
3725
  }
3486
3726
  getThreads(includeArchived = false) {
3487
3727
  return listThreads({ includeArchived });
@@ -3498,7 +3738,7 @@ var Orchestrator = class {
3498
3738
  });
3499
3739
  });
3500
3740
  this.emit({ type: "status_changed", threadId: thread.id, status: thread.status });
3501
- const { autoRunAfterSetupEnabled } = await import("./app-settings-ZKVZHJPQ.js");
3741
+ const { autoRunAfterSetupEnabled } = await import("./app-settings-LYGVDGZY.js");
3502
3742
  if (autoRunAfterSetupEnabled()) {
3503
3743
  try {
3504
3744
  await this.startDev(thread.id);
@@ -3744,7 +3984,7 @@ var Orchestrator = class {
3744
3984
  });
3745
3985
  const artifactDirective = isBrightsy ? null : formatArtifactDirective();
3746
3986
  const settings = loadWorkspaceSettings(fresh.worktreePath, fresh.repoPath);
3747
- const { autoRenameBranchEnabled } = await import("./app-settings-ZKVZHJPQ.js");
3987
+ const { autoRenameBranchEnabled } = await import("./app-settings-LYGVDGZY.js");
3748
3988
  const renameBranchDirective = !isBrightsy && !isOrchestration && autoRenameBranchEnabled() ? formatRenameBranchDirective(fresh, {
3749
3989
  customPrompt: settings?.prompts?.renameBranch
3750
3990
  }) : null;
@@ -3840,11 +4080,16 @@ var Orchestrator = class {
3840
4080
  }
3841
4081
  }
3842
4082
  }
3843
- const failureOnlyMessage = exitCode !== 0 && looksLikeAgentFailureMessage(assistantText) && !parts.some((p) => p.type === "tool" || p.type === "thinking");
3844
- if (!failureOnlyMessage && (assistantText || parts.length > 0)) {
4083
+ const lastStderr = summarizeTurnStderr(stderrTail);
4084
+ const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
4085
+ let chatText = assistantText;
4086
+ if (exitCode !== 0 && !chatText && looksLikeAgentFailureMessage(detail)) {
4087
+ chatText = humanizeAgentFailDetail(detail);
4088
+ }
4089
+ if (chatText || parts.length > 0) {
3845
4090
  appendMessage(threadId, {
3846
4091
  role: "agent",
3847
- text: assistantText,
4092
+ text: chatText,
3848
4093
  parts: parts.length > 0 ? parts : void 0,
3849
4094
  durationMs: Math.max(0, Date.now() - turnStartedAt),
3850
4095
  usage,
@@ -3863,13 +4108,12 @@ var Orchestrator = class {
3863
4108
  this.emit({ type: "status_changed", threadId, status: "stopped" });
3864
4109
  this.emit({ type: "turn_finished", threadId, exitCode });
3865
4110
  } else {
3866
- const lastStderr = summarizeTurnStderr(stderrTail);
3867
- const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
3868
4111
  const failDetail = formatTurnExitError(exitCode, detail);
4112
+ const explainedInChat = exitCode !== 0 && Boolean(chatText) && (looksLikeAgentFailureMessage(chatText) || failDetail && chatText.includes(failDetail.replace(/^exit\s*\d+:\s*/i, "").trim()));
3869
4113
  setStatus(
3870
4114
  threadId,
3871
4115
  exitCode === 0 ? "idle" : "error",
3872
- exitCode === 0 ? null : failDetail
4116
+ exitCode === 0 || explainedInChat ? null : failDetail
3873
4117
  );
3874
4118
  this.emit({
3875
4119
  type: "status_changed",
@@ -3877,6 +4121,10 @@ var Orchestrator = class {
3877
4121
  status: exitCode === 0 ? "idle" : "error"
3878
4122
  });
3879
4123
  this.emit({ type: "turn_finished", threadId, exitCode });
4124
+ if (exitCode !== 0) {
4125
+ const blob = [chatText, detail].filter(Boolean).join("\n");
4126
+ void this.maybeHandleOrchestrationQuotaFailover(threadId, blob);
4127
+ }
3880
4128
  }
3881
4129
  } catch (err) {
3882
4130
  const message = err instanceof Error ? err.message : String(err);
@@ -3890,6 +4138,7 @@ var Orchestrator = class {
3890
4138
  this.emit({ type: "error", threadId, message });
3891
4139
  this.emit({ type: "status_changed", threadId, status: "error" });
3892
4140
  this.emit({ type: "turn_finished", threadId, exitCode: 1 });
4141
+ void this.maybeHandleOrchestrationQuotaFailover(threadId, message);
3893
4142
  }
3894
4143
  } finally {
3895
4144
  this.startingTurns.delete(threadId);
@@ -4465,7 +4714,7 @@ var Orchestrator = class {
4465
4714
  await runArchiveScript(thread.repoPath, thread.worktreePath);
4466
4715
  } catch {
4467
4716
  }
4468
- const { deleteBranchOnPurgeEnabled } = await import("./app-settings-ZKVZHJPQ.js");
4717
+ const { deleteBranchOnPurgeEnabled } = await import("./app-settings-LYGVDGZY.js");
4469
4718
  const deleteBranch = opts?.deleteBranch ?? deleteBranchOnPurgeEnabled();
4470
4719
  await removeWorktree(thread.repoPath, thread.worktreePath, {
4471
4720
  deleteBranch: deleteBranch ? thread.branchName : void 0
@@ -4484,7 +4733,7 @@ var Orchestrator = class {
4484
4733
  return setStatus(thread.id, "idle");
4485
4734
  }
4486
4735
  if (!existsSync12(thread.worktreePath)) {
4487
- const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-N4PRV4V3.js");
4736
+ const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-TEDAJ57S.js");
4488
4737
  const { execa: execa6 } = await import("execa");
4489
4738
  const slug = thread.worktreePath.split("/").pop();
4490
4739
  const dest = thread.worktreePath;
@@ -4569,7 +4818,7 @@ async function startOrchestration(opts) {
4569
4818
  sourceRef: "default",
4570
4819
  ...createOpts
4571
4820
  }).catch(async () => {
4572
- const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-N4PRV4V3.js");
4821
+ const { resolveDefaultBranch: resolveDefaultBranch2, resolveRepoRoot: resolveRepoRoot2 } = await import("./worktree-TEDAJ57S.js");
4573
4822
  const repo = await resolveRepoRoot2(repoPath);
4574
4823
  const def = await resolveDefaultBranch2(repo);
4575
4824
  return createThread({
@@ -4579,7 +4828,7 @@ async function startOrchestration(opts) {
4579
4828
  repoPath: repo
4580
4829
  });
4581
4830
  });
4582
- const { updateThread: upd } = await import("./thread-store-EHROA3VZ.js");
4831
+ const { updateThread: upd } = await import("./thread-store-OV2X6PYO.js");
4583
4832
  const updated = upd(thread.id, {
4584
4833
  sourceType: "orchestration",
4585
4834
  sourceRef: goal
@@ -5010,6 +5259,120 @@ async function startMcpServer() {
5010
5259
  }
5011
5260
  }
5012
5261
  );
5262
+ const agentEnum = z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
5263
+ server.tool(
5264
+ "list_models",
5265
+ "List models for an agent. Prefer Auto: do not call this unless you have a reason to pin a specific model (user request, cost/latency, capability). Omit agent to list all.",
5266
+ {
5267
+ agent: agentEnum.optional().describe("Limit to one agent; omit for all")
5268
+ },
5269
+ async ({ agent }) => {
5270
+ try {
5271
+ const catalogs = await listModelsForAgent(agent);
5272
+ return {
5273
+ content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
5274
+ };
5275
+ } catch (err) {
5276
+ const message = err instanceof Error ? err.message : String(err);
5277
+ return { content: [{ type: "text", text: message }], isError: true };
5278
+ }
5279
+ }
5280
+ );
5281
+ server.tool(
5282
+ "fork_worktree",
5283
+ "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.",
5284
+ {
5285
+ ref: z.string().describe("Worktree thread id/ref to fork"),
5286
+ through_index: z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
5287
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
5288
+ model: z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
5289
+ title: z.string().optional()
5290
+ },
5291
+ async ({ ref, through_index, agent, model, title }) => {
5292
+ try {
5293
+ const source = orch.getThread(ref);
5294
+ if (source) await orch.reconcile(source.repoPath);
5295
+ const thread = await orch.forkThreadWorktree({
5296
+ threadId: ref,
5297
+ throughIndex: through_index,
5298
+ agent,
5299
+ model,
5300
+ title
5301
+ });
5302
+ return {
5303
+ content: [
5304
+ {
5305
+ type: "text",
5306
+ text: JSON.stringify({
5307
+ id: thread.id,
5308
+ title: thread.title,
5309
+ status: thread.status,
5310
+ agent: thread.agent,
5311
+ model: thread.model,
5312
+ branchName: thread.branchName,
5313
+ worktreePath: thread.worktreePath,
5314
+ fromThreadId: source?.id ?? ref,
5315
+ link: `sideboard://thread/${thread.id}`
5316
+ })
5317
+ }
5318
+ ]
5319
+ };
5320
+ } catch (err) {
5321
+ const message = err instanceof Error ? err.message : String(err);
5322
+ return { content: [{ type: "text", text: message }], isError: true };
5323
+ }
5324
+ }
5325
+ );
5326
+ server.tool(
5327
+ "fork_chat",
5328
+ "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. Remote coordinators 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.",
5329
+ {
5330
+ ref: z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
5331
+ through_index: z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
5332
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
5333
+ model: z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
5334
+ title: z.string().optional()
5335
+ },
5336
+ async ({ ref, through_index, agent, model, title }) => {
5337
+ try {
5338
+ const source = orch.getThread(ref);
5339
+ if (!source) {
5340
+ return {
5341
+ content: [{ type: "text", text: `Thread not found: ${ref}` }],
5342
+ isError: true
5343
+ };
5344
+ }
5345
+ const tab = orch.forkChatTab({
5346
+ threadId: source.id,
5347
+ throughIndex: through_index,
5348
+ agent,
5349
+ model,
5350
+ title
5351
+ });
5352
+ return {
5353
+ content: [
5354
+ {
5355
+ type: "text",
5356
+ text: JSON.stringify({
5357
+ id: tab.id,
5358
+ title: tab.title,
5359
+ status: tab.status,
5360
+ agent: tab.agent,
5361
+ model: tab.model,
5362
+ sourceType: tab.sourceType,
5363
+ worktreePath: tab.worktreePath,
5364
+ fromThreadId: source.id,
5365
+ link: `sideboard://thread/${tab.id}`
5366
+ })
5367
+ }
5368
+ ]
5369
+ };
5370
+ } catch (err) {
5371
+ const message = err instanceof Error ? err.message : String(err);
5372
+ return { content: [{ type: "text", text: message }], isError: true };
5373
+ }
5374
+ }
5375
+ );
5013
5376
  server.tool(
5014
5377
  "run_dev_script",
5015
5378
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
@@ -102,11 +102,15 @@ function humanizeAgentFailDetail(detail) {
102
102
  }
103
103
  function formatTurnExitError(exitCode, stderrSummary) {
104
104
  const code = exitCode ?? 1;
105
- const detail = humanizeAgentFailDetail(stderrSummary);
105
+ const raw = stderrSummary.trim();
106
+ if (/^exit\s*\d+$/i.test(raw)) {
107
+ return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
108
+ }
109
+ const detail = humanizeAgentFailDetail(raw);
106
110
  if (!detail) {
107
111
  return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
108
112
  }
109
- if (looksLikeAgentFailureMessage(stderrSummary)) return detail;
113
+ if (looksLikeAgentFailureMessage(raw)) return detail;
110
114
  return `exit ${code}: ${detail}`;
111
115
  }
112
116
 
@@ -210,6 +214,7 @@ export {
210
214
  summarizeTurnStderr,
211
215
  looksLikeAgentFailureMessage,
212
216
  fallbackTurnFailDetail,
217
+ humanizeAgentFailDetail,
213
218
  formatTurnExitError,
214
219
  cursorSdkMessageToEvents,
215
220
  parseCursorRunnerLine
@@ -40,7 +40,9 @@ function normalizeThread(raw) {
40
40
  attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
41
41
  prTitle: raw.prTitle ?? null,
42
42
  userSetTitle: Boolean(raw.userSetTitle),
43
- activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : []
43
+ activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
44
+ quotaResumeAt: raw.quotaResumeAt ?? null,
45
+ quotaContinuedFromId: raw.quotaContinuedFromId ?? null
44
46
  };
45
47
  }
46
48
  function createEmptyThread(partial) {
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  isGlobalRepoPath
3
- } from "./chunk-I3PKMLFW.js";
3
+ } from "./chunk-ZNSM2DDD.js";
4
4
  import {
5
5
  ensureGhPreferOrigin,
6
6
  resolveRepoRoot
7
- } from "./chunk-7PCTK4WO.js";
7
+ } from "./chunk-FV6FN6V5.js";
8
8
  import {
9
9
  appDataDir
10
10
  } from "./chunk-M37RITA6.js";