@sideboard-ai/core 0.1.44 → 0.1.46

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.
@@ -133,11 +133,15 @@ function humanizeAgentFailDetail(detail) {
133
133
  }
134
134
  function formatTurnExitError(exitCode, stderrSummary) {
135
135
  const code = exitCode ?? 1;
136
- const detail = humanizeAgentFailDetail(stderrSummary);
136
+ const raw = stderrSummary.trim();
137
+ if (/^exit\s*\d+$/i.test(raw)) {
138
+ return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
139
+ }
140
+ const detail = humanizeAgentFailDetail(raw);
137
141
  if (!detail) {
138
142
  return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
139
143
  }
140
- if (looksLikeAgentFailureMessage(stderrSummary)) return detail;
144
+ if (looksLikeAgentFailureMessage(raw)) return detail;
141
145
  return `exit ${code}: ${detail}`;
142
146
  }
143
147
  var NODE_VERSION_FOOTER;
@@ -477,7 +481,9 @@ function normalizeThread(raw) {
477
481
  attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
478
482
  prTitle: raw.prTitle ?? null,
479
483
  userSetTitle: Boolean(raw.userSetTitle),
480
- activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : []
484
+ activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
485
+ quotaResumeAt: raw.quotaResumeAt ?? null,
486
+ quotaContinuedFromId: raw.quotaContinuedFromId ?? null
481
487
  };
482
488
  }
483
489
  function createEmptyThread(partial) {
@@ -2631,6 +2637,8 @@ __export(app_settings_exports, {
2631
2637
  isLinearConnected: () => isLinearConnected,
2632
2638
  loadAppSettings: () => loadAppSettings,
2633
2639
  maxConcurrentAgents: () => maxConcurrentAgents,
2640
+ orchestrationQuotaFallbackAgent: () => orchestrationQuotaFallbackAgent,
2641
+ orchestrationQuotaOnLimit: () => orchestrationQuotaOnLimit,
2634
2642
  resolveClaudeExecutable: () => resolveClaudeExecutable,
2635
2643
  resolveEffectiveIssueSource: () => resolveEffectiveIssueSource,
2636
2644
  resolveThreadDefaults: () => resolveThreadDefaults,
@@ -2742,6 +2750,12 @@ function normalizeAdvanced(raw) {
2742
2750
  if (typeof source.autoCleanupOrphans === "boolean") {
2743
2751
  out.autoCleanupOrphans = source.autoCleanupOrphans;
2744
2752
  }
2753
+ if (source.orchestrationQuotaOnLimit === "switch_agent" || source.orchestrationQuotaOnLimit === "wait_reset") {
2754
+ out.orchestrationQuotaOnLimit = source.orchestrationQuotaOnLimit;
2755
+ }
2756
+ if (typeof source.orchestrationQuotaFallbackAgent === "string" && DEFAULT_AGENTS.has(source.orchestrationQuotaFallbackAgent) && source.orchestrationQuotaFallbackAgent !== "brightsy") {
2757
+ out.orchestrationQuotaFallbackAgent = source.orchestrationQuotaFallbackAgent;
2758
+ }
2745
2759
  return out;
2746
2760
  }
2747
2761
  function normalizeSettings(raw) {
@@ -2976,6 +2990,12 @@ function updateAdvancedSettings(patch) {
2976
2990
  if (typeof patch.autoCleanupOrphans === "boolean") {
2977
2991
  advanced.autoCleanupOrphans = patch.autoCleanupOrphans;
2978
2992
  }
2993
+ if (patch.orchestrationQuotaOnLimit === "switch_agent" || patch.orchestrationQuotaOnLimit === "wait_reset") {
2994
+ advanced.orchestrationQuotaOnLimit = patch.orchestrationQuotaOnLimit;
2995
+ }
2996
+ if (typeof patch.orchestrationQuotaFallbackAgent === "string" && DEFAULT_AGENTS.has(patch.orchestrationQuotaFallbackAgent) && patch.orchestrationQuotaFallbackAgent !== "brightsy") {
2997
+ advanced.orchestrationQuotaFallbackAgent = patch.orchestrationQuotaFallbackAgent;
2998
+ }
2979
2999
  return saveAppSettings({ ...current, advanced });
2980
3000
  }
2981
3001
  function autoRenameBranchEnabled(settings = loadAppSettings()) {
@@ -2996,6 +3016,16 @@ function deleteBranchOnPurgeEnabled(settings = loadAppSettings()) {
2996
3016
  function autoCleanupOrphansEnabled(settings = loadAppSettings()) {
2997
3017
  return Boolean(settings.advanced.autoCleanupOrphans);
2998
3018
  }
3019
+ function orchestrationQuotaOnLimit(settings = loadAppSettings()) {
3020
+ return settings.advanced.orchestrationQuotaOnLimit ?? "switch_agent";
3021
+ }
3022
+ function orchestrationQuotaFallbackAgent(settings = loadAppSettings()) {
3023
+ const preferred = settings.advanced.orchestrationQuotaFallbackAgent;
3024
+ if (preferred && DEFAULT_AGENTS.has(preferred) && preferred !== "brightsy") {
3025
+ return preferred;
3026
+ }
3027
+ return "cursor";
3028
+ }
2999
3029
  function maxConcurrentAgents(settings = loadAppSettings()) {
3000
3030
  const n = settings.advanced.maxConcurrent;
3001
3031
  if (typeof n === "number" && Number.isFinite(n)) {
@@ -3102,6 +3132,36 @@ var init_cloud_connect_constants = __esm({
3102
3132
  }
3103
3133
  });
3104
3134
 
3135
+ // src/agents/orchestrator-capable.ts
3136
+ function isOrchestratorCapableAgent(agent) {
3137
+ return Boolean(
3138
+ agent && ORCHESTRATOR_AGENT_KINDS.includes(agent)
3139
+ );
3140
+ }
3141
+ function assertOrchestratorCapableAgent(agent, context = "orchestration") {
3142
+ if (!isOrchestratorCapableAgent(agent)) {
3143
+ throw new Error(
3144
+ `${agent} cannot run ${context} \u2014 it does not support Sideboard MCP. Use Claude, Cursor, Codex, or OpenCode.`
3145
+ );
3146
+ }
3147
+ return agent;
3148
+ }
3149
+ function coerceOrchestratorAgent(agent, fallback = "claude") {
3150
+ return isOrchestratorCapableAgent(agent) ? agent : fallback;
3151
+ }
3152
+ var ORCHESTRATOR_AGENT_KINDS;
3153
+ var init_orchestrator_capable = __esm({
3154
+ "src/agents/orchestrator-capable.ts"() {
3155
+ "use strict";
3156
+ ORCHESTRATOR_AGENT_KINDS = [
3157
+ "claude",
3158
+ "codex",
3159
+ "opencode",
3160
+ "cursor"
3161
+ ];
3162
+ }
3163
+ });
3164
+
3105
3165
  // src/orchestrator/coordinator-prompt.ts
3106
3166
  var coordinator_prompt_exports = {};
3107
3167
  __export(coordinator_prompt_exports, {
@@ -3223,11 +3283,14 @@ var init_coordinator_prompt = __esm({
3223
3283
  "Discover:",
3224
3284
  "- list_workspaces \u2014 registered repos (path + github slug when known)",
3225
3285
  "- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces (issues: Linear API or GitHub Issues)",
3286
+ "- list_models \u2014 only when you need a specific model (rare); otherwise leave model unset = Auto",
3226
3287
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
3227
3288
  "Workspaces:",
3228
3289
  "- add_workspace / remove_workspace \u2014 register or unregister a git repo",
3229
3290
  "Worktree threads (chats):",
3230
3291
  "- create_thread \u2014 create a worktree + chat from branch | pr | ticket; pass repoPath + parentThreadId",
3292
+ "- fork_worktree \u2014 fork a worktree chat into a NEW git worktree + chat (transcript attached); optional agent; leave model unset (Auto) unless you have a reason. Not for orchestration chats.",
3293
+ "- fork_chat \u2014 fork a worktree chat (same worktree tab) OR a Global orchestration chat (new orchestration tab); optional agent; leave model unset (Auto) unless you have a reason. Remote coordinators: use this to continue another orchestration chat on a different agent after session limits.",
3231
3294
  "- send_to_thread \u2014 queue a prompt (start/continue a chat turn); pass force_stop: true to interrupt mid-turn / clear stale queued prompts before replacing with a new request",
3232
3295
  "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply",
3233
3296
  "- stop_thread \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
@@ -3325,6 +3388,7 @@ function createGlobalChat(opts) {
3325
3388
  const explicit = opts.title?.trim();
3326
3389
  const title = explicit && explicit !== CLOUD_ORCHESTRATOR_GOAL ? explicit : allocateTeamName(takenTeamSlugsForOrchestration()).name;
3327
3390
  const sourceRef = opts.sourceRef?.trim() || (isCloud ? CLOUD_ORCHESTRATOR_GOAL : title);
3391
+ const agent = assertOrchestratorCapableAgent(opts.agent);
3328
3392
  const thread = createEmptyThread({
3329
3393
  title,
3330
3394
  // Stick nicknames the same way chat tabs do (avoid later sync overwrites).
@@ -3334,7 +3398,7 @@ function createGlobalChat(opts) {
3334
3398
  branchName: "global",
3335
3399
  worktreePath: globalAgentCwd(),
3336
3400
  repoPath: GLOBAL_WORKSPACE_ID,
3337
- agent: opts.agent,
3401
+ agent,
3338
3402
  autonomy: opts.autonomy ?? "default",
3339
3403
  model: opts.model ?? null,
3340
3404
  effort: opts.effort ?? "high",
@@ -3396,6 +3460,7 @@ var init_global_workspace = __esm({
3396
3460
  "src/store/global-workspace.ts"() {
3397
3461
  "use strict";
3398
3462
  init_cloud_connect_constants();
3463
+ init_orchestrator_capable();
3399
3464
  init_teams();
3400
3465
  init_coordinator_prompt();
3401
3466
  init_paths();
@@ -4128,6 +4193,45 @@ async function buildInjectedMcpServers(opts) {
4128
4193
  }
4129
4194
  return servers;
4130
4195
  }
4196
+ function toCursorMcpServers(servers) {
4197
+ const out = {};
4198
+ for (const s of servers) {
4199
+ out[s.name] = {
4200
+ command: s.command,
4201
+ ...s.args ? { args: s.args } : {},
4202
+ ...s.env ? { env: s.env } : {}
4203
+ };
4204
+ }
4205
+ return out;
4206
+ }
4207
+ function toCodexMcpConfigArgs(servers) {
4208
+ const args = [];
4209
+ for (const s of servers) {
4210
+ const prefix = `mcp_servers.${s.name}`;
4211
+ args.push("-c", `${prefix}.command=${JSON.stringify(s.command)}`);
4212
+ if (s.args?.length) {
4213
+ args.push("-c", `${prefix}.args=${JSON.stringify(s.args)}`);
4214
+ }
4215
+ if (s.env) {
4216
+ for (const [key, value] of Object.entries(s.env)) {
4217
+ args.push("-c", `${prefix}.env.${key}=${JSON.stringify(value)}`);
4218
+ }
4219
+ }
4220
+ }
4221
+ return args;
4222
+ }
4223
+ function toOpencodeMcpConfigContent(servers) {
4224
+ const mcp = {};
4225
+ for (const s of servers) {
4226
+ mcp[s.name] = {
4227
+ type: "local",
4228
+ command: [s.command, ...s.args ?? []],
4229
+ enabled: true,
4230
+ ...s.env && Object.keys(s.env).length > 0 ? { environment: s.env } : {}
4231
+ };
4232
+ }
4233
+ return JSON.stringify({ mcp });
4234
+ }
4131
4235
  function writeMcpServersConfig(servers) {
4132
4236
  if (servers.length === 0) return null;
4133
4237
  const mcpServers = {};
@@ -4628,6 +4732,7 @@ var init_codex = __esm({
4628
4732
  import_node_path11 = require("path");
4629
4733
  init_run();
4630
4734
  init_error_detail();
4735
+ init_injected_mcp();
4631
4736
  init_turn_input();
4632
4737
  init_types();
4633
4738
  CODEX_PROMPT_ARG_MAX = 2e5;
@@ -4686,6 +4791,11 @@ var init_codex = __esm({
4686
4791
  }
4687
4792
  const mode = permissionMode(thread);
4688
4793
  const model = thread.model?.trim();
4794
+ const injected = await buildInjectedMcpServers({
4795
+ includeSideboard: true,
4796
+ includeBrightsy: isBrightsyConnected()
4797
+ });
4798
+ const mcpOverrides = toCodexMcpConfigArgs(injected);
4689
4799
  const args = [
4690
4800
  "exec",
4691
4801
  ...sessionId ? ["resume", sessionId] : [],
@@ -4697,7 +4807,8 @@ var init_codex = __esm({
4697
4807
  mode.codexSandbox,
4698
4808
  "--ask-for-approval",
4699
4809
  "never",
4700
- ...model ? ["--model", model] : []
4810
+ ...model ? ["--model", model] : [],
4811
+ ...mcpOverrides
4701
4812
  ];
4702
4813
  return {
4703
4814
  file: "codex",
@@ -4982,6 +5093,7 @@ var init_cursor = __esm({
4982
5093
  init_run();
4983
5094
  init_app_settings();
4984
5095
  init_cursor_events();
5096
+ init_injected_mcp();
4985
5097
  init_node_launch();
4986
5098
  init_turn_input();
4987
5099
  init_cursor_events();
@@ -5032,6 +5144,11 @@ var init_cursor = __esm({
5032
5144
  const prompt = flattenTurnInput(input);
5033
5145
  const agentId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
5034
5146
  const apiKey = resolveCursorApiKey() || void 0;
5147
+ const injected = await buildInjectedMcpServers({
5148
+ includeSideboard: true,
5149
+ includeBrightsy: isBrightsyConnected()
5150
+ });
5151
+ const mcpServers = toCursorMcpServers(injected);
5035
5152
  const req = {
5036
5153
  prompt,
5037
5154
  cwd: thread.worktreePath,
@@ -5040,7 +5157,8 @@ var init_cursor = __esm({
5040
5157
  effort: thread.effort,
5041
5158
  fast: thread.fast,
5042
5159
  planMode: thread.planMode,
5043
- apiKey
5160
+ apiKey,
5161
+ ...Object.keys(mcpServers).length > 0 ? { mcpServers } : {}
5044
5162
  };
5045
5163
  const runner = cursorRunnerPath();
5046
5164
  const isTs = runner.endsWith(".ts");
@@ -5134,6 +5252,7 @@ var init_opencode = __esm({
5134
5252
  "use strict";
5135
5253
  init_run();
5136
5254
  init_error_detail();
5255
+ init_injected_mcp();
5137
5256
  init_turn_input();
5138
5257
  init_types();
5139
5258
  FALLBACK_OPENCODE_MODELS = [
@@ -5194,6 +5313,11 @@ var init_opencode = __esm({
5194
5313
  if (model) {
5195
5314
  args.push("--model", model);
5196
5315
  }
5316
+ const injected = await buildInjectedMcpServers({
5317
+ includeSideboard: true,
5318
+ includeBrightsy: isBrightsyConnected()
5319
+ });
5320
+ const mcpContent = injected.length > 0 ? toOpencodeMcpConfigContent(injected) : null;
5197
5321
  return {
5198
5322
  file: "opencode",
5199
5323
  args,
@@ -5202,7 +5326,8 @@ var init_opencode = __esm({
5202
5326
  // message is given (see resolveRunInput in opencode's run.ts).
5203
5327
  stdin: prompt,
5204
5328
  env: {
5205
- OPENCODE_PERMISSION: mode.opencodePermission
5329
+ OPENCODE_PERMISSION: mode.opencodePermission,
5330
+ ...mcpContent ? { OPENCODE_CONFIG_CONTENT: mcpContent } : {}
5206
5331
  }
5207
5332
  };
5208
5333
  },
@@ -5322,6 +5447,201 @@ var init_opencode = __esm({
5322
5447
  }
5323
5448
  });
5324
5449
 
5450
+ // src/agents/list-models.ts
5451
+ async function listBrightsyModels() {
5452
+ try {
5453
+ const targets = await listBrightsyChatTargets();
5454
+ const accountId = targets.activeAccountId;
5455
+ const models = (targets.models ?? []).map((m) => ({
5456
+ id: encodeBrightsyTarget("model", m.id, accountId),
5457
+ displayName: m.name || m.id,
5458
+ description: m.description ?? void 0
5459
+ }));
5460
+ const agents = (targets.agents ?? []).map((a) => ({
5461
+ id: encodeBrightsyTarget("agent", a.id, accountId),
5462
+ displayName: a.name || a.id,
5463
+ description: a.description ?? "Brightsy agent target"
5464
+ }));
5465
+ return [...models, ...agents].slice(0, 80);
5466
+ } catch {
5467
+ return [];
5468
+ }
5469
+ }
5470
+ async function listModelsForAgent(agent) {
5471
+ const kinds = agent ? [agent] : ["claude", "codex", "opencode", "cursor", "brightsy"];
5472
+ const out = [];
5473
+ for (const kind of kinds) {
5474
+ if (kind === "claude") {
5475
+ out.push({
5476
+ agent: kind,
5477
+ auto: true,
5478
+ models: CLAUDE_MODEL_CATALOG,
5479
+ note: "Default Auto \u2014 only pass a model id when you have a reason."
5480
+ });
5481
+ continue;
5482
+ }
5483
+ if (kind === "codex") {
5484
+ out.push({
5485
+ agent: kind,
5486
+ auto: true,
5487
+ models: await listCodexModels(),
5488
+ note: "Default Auto \u2014 only pass a model slug when you have a reason."
5489
+ });
5490
+ continue;
5491
+ }
5492
+ if (kind === "opencode") {
5493
+ out.push({
5494
+ agent: kind,
5495
+ auto: true,
5496
+ models: await listOpencodeModels(),
5497
+ note: "Default Auto \u2014 only pass a provider/model id when you have a reason."
5498
+ });
5499
+ continue;
5500
+ }
5501
+ if (kind === "cursor") {
5502
+ out.push({
5503
+ agent: kind,
5504
+ auto: true,
5505
+ models: await listCursorModels(),
5506
+ note: 'Default Auto \u2014 only pass a model id when you have a reason (or use "default").'
5507
+ });
5508
+ continue;
5509
+ }
5510
+ if (kind === "brightsy") {
5511
+ const models = await listBrightsyModels();
5512
+ out.push({
5513
+ agent: kind,
5514
+ auto: true,
5515
+ models,
5516
+ note: models.length ? "Default Auto / Default agent \u2014 only pass a model/agent id when you have a reason." : "Brightsy not logged in or no targets \u2014 leave model unset for Default."
5517
+ });
5518
+ }
5519
+ }
5520
+ return out;
5521
+ }
5522
+ var CLAUDE_MODEL_CATALOG;
5523
+ var init_list_models = __esm({
5524
+ "src/agents/list-models.ts"() {
5525
+ "use strict";
5526
+ init_brightsy();
5527
+ init_brightsy_targets();
5528
+ init_codex();
5529
+ init_cursor();
5530
+ init_opencode();
5531
+ CLAUDE_MODEL_CATALOG = [
5532
+ { id: "fable", displayName: "Fable" },
5533
+ { id: "opus", displayName: "Opus" },
5534
+ { id: "sonnet", displayName: "Sonnet" },
5535
+ { id: "haiku", displayName: "Haiku" }
5536
+ ];
5537
+ }
5538
+ });
5539
+
5540
+ // src/agents/session-quota.ts
5541
+ function isSessionQuotaLimit(text) {
5542
+ const lower = text.trim().toLowerCase();
5543
+ if (!lower) return false;
5544
+ if (/credit balance is too low|out of credits|insufficient.?quota|billing/.test(lower)) {
5545
+ return false;
5546
+ }
5547
+ if (/prompt is too long|context.*(too long|exceed)|conversation too long/.test(lower)) {
5548
+ return false;
5549
+ }
5550
+ return /you've hit your/.test(lower) || /hit your (session|weekly|opus) limit/.test(lower) || /usage limit/.test(lower) || /rate.?limit|too many requests|\b429\b/.test(lower) && /reset/i.test(text);
5551
+ }
5552
+ function parseSessionQuotaResetAt(text, now = /* @__PURE__ */ new Date()) {
5553
+ const absolute = text.match(
5554
+ /resets\s+(?:at\s+)?(\d{1,2}):(\d{2})\s*(am|pm)(?:\s*\(([^)]+)\))?/i
5555
+ );
5556
+ if (absolute) {
5557
+ const hour12 = Number(absolute[1]);
5558
+ const minute = Number(absolute[2]);
5559
+ const ampm = absolute[3].toLowerCase();
5560
+ const timeZone = absolute[4]?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone;
5561
+ let hour = hour12 % 12;
5562
+ if (ampm === "pm") hour += 12;
5563
+ const at = zonedWallTimeToUtc(now, hour, minute, timeZone);
5564
+ if (!at) return null;
5565
+ if (at.getTime() <= now.getTime() + 3e4) {
5566
+ const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1e3);
5567
+ return zonedWallTimeToUtc(tomorrow, hour, minute, timeZone);
5568
+ }
5569
+ return at;
5570
+ }
5571
+ const relative = text.match(
5572
+ /resets\s+in\s+(\d+)\s*(minutes?|hours?|days?)/i
5573
+ );
5574
+ if (relative) {
5575
+ const n = Number(relative[1]);
5576
+ const unit = relative[2].toLowerCase();
5577
+ const ms = unit.startsWith("day") ? n * 24 * 60 * 60 * 1e3 : unit.startsWith("hour") ? n * 60 * 60 * 1e3 : n * 60 * 1e3;
5578
+ return new Date(now.getTime() + ms);
5579
+ }
5580
+ return null;
5581
+ }
5582
+ function zonedWallTimeToUtc(day, hour, minute, timeZone) {
5583
+ try {
5584
+ const cal = new Intl.DateTimeFormat("en-US", {
5585
+ timeZone,
5586
+ year: "numeric",
5587
+ month: "2-digit",
5588
+ day: "2-digit"
5589
+ });
5590
+ const parts = Object.fromEntries(
5591
+ cal.formatToParts(day).filter((p) => p.type !== "literal").map((p) => [p.type, p.value])
5592
+ );
5593
+ const year = Number(parts.year);
5594
+ const month = Number(parts.month);
5595
+ const date = Number(parts.day);
5596
+ if (![year, month, date].every((n) => Number.isFinite(n))) return null;
5597
+ const utcGuess = Date.UTC(year, month - 1, date, hour, minute, 0);
5598
+ const dtf = new Intl.DateTimeFormat("en-US", {
5599
+ timeZone,
5600
+ year: "numeric",
5601
+ month: "2-digit",
5602
+ day: "2-digit",
5603
+ hour: "2-digit",
5604
+ minute: "2-digit",
5605
+ second: "2-digit",
5606
+ hourCycle: "h23"
5607
+ });
5608
+ const asParts = Object.fromEntries(
5609
+ dtf.formatToParts(new Date(utcGuess)).filter((p) => p.type !== "literal").map((p) => [p.type, p.value])
5610
+ );
5611
+ const asUtc = Date.UTC(
5612
+ Number(asParts.year),
5613
+ Number(asParts.month) - 1,
5614
+ Number(asParts.day),
5615
+ Number(asParts.hour),
5616
+ Number(asParts.minute),
5617
+ Number(asParts.second || "0")
5618
+ );
5619
+ const offset = asUtc - utcGuess;
5620
+ return new Date(utcGuess - offset);
5621
+ } catch {
5622
+ return null;
5623
+ }
5624
+ }
5625
+ function resolveQuotaFallbackAgent(current, preferred) {
5626
+ const ordered = [
5627
+ ...preferred && preferred !== "brightsy" ? [preferred] : [],
5628
+ ...FALLBACK_ORDER.filter((a) => a !== preferred)
5629
+ ];
5630
+ return ordered.find((a) => a !== current) ?? (current === "cursor" ? "codex" : "cursor");
5631
+ }
5632
+ var FALLBACK_ORDER;
5633
+ var init_session_quota = __esm({
5634
+ "src/agents/session-quota.ts"() {
5635
+ "use strict";
5636
+ FALLBACK_ORDER = [
5637
+ "cursor",
5638
+ "codex",
5639
+ "opencode",
5640
+ "claude"
5641
+ ];
5642
+ }
5643
+ });
5644
+
5325
5645
  // src/agents/install.ts
5326
5646
  function getAgentSetupInfo(agent) {
5327
5647
  return SETUP[agent];
@@ -5502,11 +5822,15 @@ var init_install = __esm({
5502
5822
  // src/agents/index.ts
5503
5823
  var agents_exports = {};
5504
5824
  __export(agents_exports, {
5825
+ CLAUDE_MODEL_CATALOG: () => CLAUDE_MODEL_CATALOG,
5826
+ ORCHESTRATOR_AGENT_KINDS: () => ORCHESTRATOR_AGENT_KINDS,
5505
5827
  PLAN_MODE_INSTRUCTION: () => PLAN_MODE_INSTRUCTION,
5506
5828
  allAdapters: () => allAdapters,
5829
+ assertOrchestratorCapableAgent: () => assertOrchestratorCapableAgent,
5507
5830
  brightsyAdapter: () => brightsyAdapter,
5508
5831
  claudeAdapter: () => claudeAdapter,
5509
5832
  codexAdapter: () => codexAdapter,
5833
+ coerceOrchestratorAgent: () => coerceOrchestratorAgent,
5510
5834
  cursorAdapter: () => cursorAdapter,
5511
5835
  cursorSdkMessageToEvents: () => cursorSdkMessageToEvents,
5512
5836
  decodeBrightsyTarget: () => decodeBrightsyTarget,
@@ -5516,17 +5840,22 @@ __export(agents_exports, {
5516
5840
  getAgentSetupInfo: () => getAgentSetupInfo,
5517
5841
  installAgent: () => installAgent,
5518
5842
  isCursorAutoModel: () => isCursorAutoModel,
5843
+ isOrchestratorCapableAgent: () => isOrchestratorCapableAgent,
5844
+ isSessionQuotaLimit: () => isSessionQuotaLimit,
5519
5845
  listAgentSetupInfo: () => listAgentSetupInfo,
5520
5846
  listBrightsyChatTargets: () => listBrightsyChatTargets,
5521
5847
  listCodexModels: () => listCodexModels,
5522
5848
  listCursorModels: () => listCursorModels,
5849
+ listModelsForAgent: () => listModelsForAgent,
5523
5850
  listOpencodeModels: () => listOpencodeModels,
5524
5851
  loginAgent: () => loginAgent,
5525
5852
  openInSystemTerminal: () => openInSystemTerminal,
5526
5853
  opencodeAdapter: () => opencodeAdapter,
5527
5854
  parseCursorRunnerLine: () => parseCursorRunnerLine,
5855
+ parseSessionQuotaResetAt: () => parseSessionQuotaResetAt,
5528
5856
  permissionMode: () => permissionMode,
5529
- resolveCursorModelId: () => resolveCursorModelId
5857
+ resolveCursorModelId: () => resolveCursorModelId,
5858
+ resolveQuotaFallbackAgent: () => resolveQuotaFallbackAgent
5530
5859
  });
5531
5860
  function getAdapter(kind) {
5532
5861
  return adapters[kind];
@@ -5552,6 +5881,9 @@ var init_agents = __esm({
5552
5881
  init_cursor_events();
5553
5882
  init_cursor();
5554
5883
  init_opencode();
5884
+ init_list_models();
5885
+ init_session_quota();
5886
+ init_orchestrator_capable();
5555
5887
  init_path();
5556
5888
  init_install();
5557
5889
  adapters = {
@@ -5749,6 +6081,7 @@ init_app_settings();
5749
6081
  init_global_workspace();
5750
6082
  init_brightsy();
5751
6083
  init_agents();
6084
+ init_orchestrator_capable();
5752
6085
 
5753
6086
  // src/agents/message-parts.ts
5754
6087
  function asRecord(input) {
@@ -5990,6 +6323,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
5990
6323
  const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await Promise.resolve().then(() => (init_coordinator_prompt(), coordinator_prompt_exports));
5991
6324
  ensureGlobalCoordinatorCwd2();
5992
6325
  }
6326
+ if (isOrchestratorThread(thread)) {
6327
+ assertOrchestratorCapableAgent(thread.agent);
6328
+ }
5993
6329
  const adapter = getAdapter(thread.agent);
5994
6330
  const cmd = await adapter.buildTurn(thread, input);
5995
6331
  if (cmd.cwd !== thread.worktreePath) {
@@ -6808,6 +7144,9 @@ async function listLinearIssues(agent, repoPath) {
6808
7144
  return adapter.listLinearIssues(repoPath);
6809
7145
  }
6810
7146
 
7147
+ // src/orchestrator/orchestrator.ts
7148
+ init_orchestrator_capable();
7149
+
6811
7150
  // src/threads/chat-tabs.ts
6812
7151
  var import_node_crypto2 = require("crypto");
6813
7152
 
@@ -7089,6 +7428,7 @@ async function maybeCompactContext(thread, thresholds = {}, summarize = summariz
7089
7428
  init_teams();
7090
7429
  init_worktree_labels();
7091
7430
  init_global_workspace();
7431
+ init_orchestrator_capable();
7092
7432
  init_thread_store();
7093
7433
  init_worktree_labels();
7094
7434
  function sameWorktreePath(a, b) {
@@ -7152,6 +7492,10 @@ function createChatTab(input) {
7152
7492
  const binding = worktreeBindingFrom(from);
7153
7493
  const explicitTitle = input.title?.trim();
7154
7494
  const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
7495
+ const nextAgent = input.agent ?? from.agent;
7496
+ if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
7497
+ assertOrchestratorCapableAgent(nextAgent);
7498
+ }
7155
7499
  const thread = createEmptyThread({
7156
7500
  title,
7157
7501
  // Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
@@ -7159,7 +7503,7 @@ function createChatTab(input) {
7159
7503
  // shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
7160
7504
  userSetTitle: true,
7161
7505
  ...binding,
7162
- agent: input.agent ?? from.agent,
7506
+ agent: nextAgent,
7163
7507
  model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
7164
7508
  effort: input.effort !== void 0 ? input.effort : from.effort,
7165
7509
  fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
@@ -7175,12 +7519,19 @@ function forkChatTab(input) {
7175
7519
  const from = requireThread(input.threadId);
7176
7520
  const slice = forkMessageSlice(from, input.throughIndex);
7177
7521
  const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
7178
- return createChatTab({
7522
+ const tab = createChatTab({
7179
7523
  fromThreadId: input.threadId,
7180
7524
  agent: input.agent ?? from.agent,
7525
+ model: input.model,
7181
7526
  title: input.title?.trim() || void 0,
7182
7527
  attachments: [attachment]
7183
7528
  });
7529
+ if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
7530
+ const next = { ...tab, parentThreadId: from.id };
7531
+ writeThread(next);
7532
+ return next;
7533
+ }
7534
+ return tab;
7184
7535
  }
7185
7536
 
7186
7537
  // src/review/request-review.ts
@@ -7237,6 +7588,7 @@ async function requestReview(threadRef, send) {
7237
7588
 
7238
7589
  // src/threads/fork-worktree.ts
7239
7590
  init_thread_store();
7591
+ init_global_workspace();
7240
7592
  function requireThread2(idOrRef) {
7241
7593
  const thread = findThreadByRef(idOrRef) ?? null;
7242
7594
  if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
@@ -7244,16 +7596,28 @@ function requireThread2(idOrRef) {
7244
7596
  }
7245
7597
  async function forkThreadWorktree(input, onSetupLine) {
7246
7598
  const from = requireThread2(input.threadId);
7599
+ if (isOrchestratorThread(from)) {
7600
+ throw new Error(
7601
+ "fork_worktree targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
7602
+ );
7603
+ }
7604
+ if (!from.branchName?.trim() || !from.repoPath?.trim()) {
7605
+ throw new Error(
7606
+ `Cannot fork worktree: thread ${from.id} has no branch/repo (need a real worktree chat).`
7607
+ );
7608
+ }
7247
7609
  const slice = forkMessageSlice(from, input.throughIndex);
7248
7610
  const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
7611
+ const nextAgent = input.agent ?? from.agent;
7612
+ const nextModel = input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model;
7249
7613
  const thread = await createThread(
7250
7614
  {
7251
7615
  sourceType: "branch",
7252
7616
  sourceRef: from.branchName,
7253
7617
  repoPath: from.repoPath,
7254
- agent: input.agent ?? from.agent,
7618
+ agent: nextAgent,
7255
7619
  autonomy: from.autonomy,
7256
- model: from.model,
7620
+ model: nextModel,
7257
7621
  effort: from.effort,
7258
7622
  fast: from.fast,
7259
7623
  planMode: from.planMode,
@@ -7266,6 +7630,115 @@ async function forkThreadWorktree(input, onSetupLine) {
7266
7630
  return thread;
7267
7631
  }
7268
7632
 
7633
+ // src/orchestrator/quota-failover.ts
7634
+ var import_node_crypto4 = require("crypto");
7635
+ init_session_quota();
7636
+ init_app_settings();
7637
+ init_global_workspace();
7638
+ init_thread_store();
7639
+ function planOrchestrationQuotaFailover(thread, limitText, opts) {
7640
+ if (!isOrchestratorThread(thread)) return null;
7641
+ if (!isSessionQuotaLimit(limitText)) return null;
7642
+ const onLimit = opts?.onLimit ?? orchestrationQuotaOnLimit();
7643
+ const resumeAt = parseSessionQuotaResetAt(limitText, opts?.now);
7644
+ if (thread.quotaContinuedFromId) {
7645
+ if (resumeAt) {
7646
+ return {
7647
+ action: "wait_reset",
7648
+ reason: "Already continued once; waiting for quota reset instead.",
7649
+ limitText,
7650
+ resumeAt
7651
+ };
7652
+ }
7653
+ return {
7654
+ action: "none",
7655
+ reason: "Already continued once; no parseable reset time.",
7656
+ limitText
7657
+ };
7658
+ }
7659
+ if (onLimit === "wait_reset") {
7660
+ if (!resumeAt) {
7661
+ return {
7662
+ action: "none",
7663
+ reason: "wait_reset configured but reset time could not be parsed.",
7664
+ limitText
7665
+ };
7666
+ }
7667
+ return {
7668
+ action: "wait_reset",
7669
+ reason: "Settings: wait for quota reset.",
7670
+ limitText,
7671
+ resumeAt
7672
+ };
7673
+ }
7674
+ const preferred = opts?.fallbackAgent ?? orchestrationQuotaFallbackAgent();
7675
+ const fallbackAgent = resolveQuotaFallbackAgent(thread.agent, preferred);
7676
+ return {
7677
+ action: "switch_agent",
7678
+ reason: `Continue on ${fallbackAgent} (Auto) after ${thread.agent} session limit.`,
7679
+ limitText,
7680
+ fallbackAgent
7681
+ };
7682
+ }
7683
+ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
7684
+ const children = listThreads({ includeArchived: false }).filter((t) => t.parentThreadId === from.id && t.status !== "archived").slice(0, 40).map(
7685
+ (t) => `- ${t.title} \xB7 ${t.status} \xB7 ${t.agent} \xB7 sideboard://thread/${t.id}`
7686
+ );
7687
+ const recent = from.messages.slice(-8).map((m) => {
7688
+ const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
7689
+ const text = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
7690
+ return text ? `- ${role}: ${text}` : null;
7691
+ }).filter(Boolean);
7692
+ const body = [
7693
+ `# Orchestration handoff`,
7694
+ "",
7695
+ `Previous chat: ${from.title} (\`${from.id}\`) on **${from.agent}** hit a session/usage limit.`,
7696
+ `Limit: ${limitText.trim()}`,
7697
+ `Continuing on **${fallbackAgent}** with Auto model.`,
7698
+ "",
7699
+ `## Goal`,
7700
+ from.sourceRef?.trim() || "(none)",
7701
+ "",
7702
+ `## Child threads`,
7703
+ children.length ? children.join("\n") : "(none listed \u2014 call list_threads)",
7704
+ "",
7705
+ `## Recent turns (truncated)`,
7706
+ recent.length ? recent.join("\n") : "(none)",
7707
+ "",
7708
+ `## Instructions`,
7709
+ `- Continue fleet orchestration from this handoff.`,
7710
+ `- Prefer Sideboard MCP (list_threads, get_thread, send_to_thread, \u2026) for live status.`,
7711
+ `- Leave model Auto unless there is a specific reason to pin one.`,
7712
+ `- Do not wait on the limited ${from.agent} account; keep going on ${fallbackAgent}.`
7713
+ ].join("\n");
7714
+ return {
7715
+ id: (0, import_node_crypto4.randomUUID)(),
7716
+ name: "Orchestration quota handoff.md",
7717
+ kind: "transcript",
7718
+ content: body
7719
+ };
7720
+ }
7721
+ var QUOTA_CONTINUE_PROMPT = (fromAgent, fallback) => [
7722
+ `${fromAgent} hit a session/usage limit. Continue this orchestration on ${fallback} using the attached handoff.`,
7723
+ "Call list_threads for live fleet status, then proceed with the goal. Leave model Auto unless needed."
7724
+ ].join(" ");
7725
+ 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.";
7726
+ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
7727
+ const handoff = buildQuotaHandoffAttachment(from, limitText, fallbackAgent);
7728
+ const tab = createChatTab({
7729
+ fromThreadId: from.id,
7730
+ agent: fallbackAgent,
7731
+ model: null,
7732
+ attachments: [handoff]
7733
+ });
7734
+ return updateThread(tab.id, {
7735
+ parentThreadId: from.id,
7736
+ quotaContinuedFromId: from.id,
7737
+ sourceRef: from.sourceRef,
7738
+ sourceType: "orchestration"
7739
+ });
7740
+ }
7741
+
7269
7742
  // src/threads/adopt.ts
7270
7743
  var import_node_child_process = require("child_process");
7271
7744
  var import_node_fs20 = require("fs");
@@ -8469,7 +8942,7 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
8469
8942
  // src/composer/stage-files.ts
8470
8943
  var import_node_fs23 = require("fs");
8471
8944
  var import_node_path22 = require("path");
8472
- var import_node_crypto4 = require("crypto");
8945
+ var import_node_crypto5 = require("crypto");
8473
8946
  var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
8474
8947
  "png",
8475
8948
  "jpg",
@@ -8525,7 +8998,7 @@ function uniqueAttachmentName(dir, originalName) {
8525
8998
  const candidate = `${stem}-${i}${ext}`;
8526
8999
  if (!(0, import_node_fs23.existsSync)((0, import_node_path22.join)(dir, candidate))) return candidate;
8527
9000
  }
8528
- return `${stem}-${(0, import_node_crypto4.randomUUID)()}${ext}`;
9001
+ return `${stem}-${(0, import_node_crypto5.randomUUID)()}${ext}`;
8529
9002
  }
8530
9003
  function previewDataUrlFromBuf(filePath, buf) {
8531
9004
  if (!isImageFilePath(filePath)) return void 0;
@@ -8537,7 +9010,7 @@ function attachmentFromBuffer(name, buf, opts) {
8537
9010
  if (isImageFilePath(name)) {
8538
9011
  const pathHint = opts.path ? `\`${opts.path}\`` : opts.sourceLabel || name;
8539
9012
  return {
8540
- id: (0, import_node_crypto4.randomUUID)(),
9013
+ id: (0, import_node_crypto5.randomUUID)(),
8541
9014
  name,
8542
9015
  kind: "file",
8543
9016
  path: opts.path,
@@ -8550,7 +9023,7 @@ function attachmentFromBuffer(name, buf, opts) {
8550
9023
  }
8551
9024
  if (buf.length > MAX_INLINE_BYTES) {
8552
9025
  return {
8553
- id: (0, import_node_crypto4.randomUUID)(),
9026
+ id: (0, import_node_crypto5.randomUUID)(),
8554
9027
  name,
8555
9028
  kind: "file",
8556
9029
  path: opts.path,
@@ -8559,7 +9032,7 @@ function attachmentFromBuffer(name, buf, opts) {
8559
9032
  }
8560
9033
  if (buf.includes(0)) {
8561
9034
  return {
8562
- id: (0, import_node_crypto4.randomUUID)(),
9035
+ id: (0, import_node_crypto5.randomUUID)(),
8563
9036
  name,
8564
9037
  kind: "file",
8565
9038
  path: opts.path,
@@ -8567,7 +9040,7 @@ function attachmentFromBuffer(name, buf, opts) {
8567
9040
  };
8568
9041
  }
8569
9042
  return {
8570
- id: (0, import_node_crypto4.randomUUID)(),
9043
+ id: (0, import_node_crypto5.randomUUID)(),
8571
9044
  name,
8572
9045
  kind: "file",
8573
9046
  path: opts.path,
@@ -8591,7 +9064,7 @@ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
8591
9064
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
8592
9065
  } catch (err) {
8593
9066
  out.push({
8594
- id: (0, import_node_crypto4.randomUUID)(),
9067
+ id: (0, import_node_crypto5.randomUUID)(),
8595
9068
  name: originalName,
8596
9069
  kind: "file",
8597
9070
  content: `(could not attach ${abs}: ${err instanceof Error ? err.message : String(err)})`
@@ -8615,7 +9088,7 @@ function stageBuffersAsAttachments(worktreePath, buffers) {
8615
9088
  out.push(attachmentFromBuffer(name, buf, { path: rel }));
8616
9089
  } catch (err) {
8617
9090
  out.push({
8618
- id: (0, import_node_crypto4.randomUUID)(),
9091
+ id: (0, import_node_crypto5.randomUUID)(),
8619
9092
  name: originalName,
8620
9093
  kind: "file",
8621
9094
  content: `(could not attach ${originalName}: ${err instanceof Error ? err.message : String(err)})`
@@ -8629,7 +9102,7 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
8629
9102
  for (const rel of relativePaths) {
8630
9103
  if (!rel || rel.includes("..") || rel.startsWith("/")) {
8631
9104
  out.push({
8632
- id: (0, import_node_crypto4.randomUUID)(),
9105
+ id: (0, import_node_crypto5.randomUUID)(),
8633
9106
  name: (0, import_node_path22.basename)(rel) || "file",
8634
9107
  kind: "file",
8635
9108
  content: `(invalid path: ${rel})`
@@ -8645,7 +9118,7 @@ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
8645
9118
  out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
8646
9119
  } catch (err) {
8647
9120
  out.push({
8648
- id: (0, import_node_crypto4.randomUUID)(),
9121
+ id: (0, import_node_crypto5.randomUUID)(),
8649
9122
  name,
8650
9123
  kind: "file",
8651
9124
  content: `(could not read ${rel}: ${err instanceof Error ? err.message : String(err)})`
@@ -8901,6 +9374,8 @@ var Orchestrator = class {
8901
9374
  haltDrain = /* @__PURE__ */ new Set();
8902
9375
  /** WIP snapshot SHA at the start of the latest agent turn (per thread). */
8903
9376
  turnBaselines = /* @__PURE__ */ new Map();
9377
+ /** Timers for orchestration session-quota auto-resume. */
9378
+ quotaResumeTimers = /* @__PURE__ */ new Map();
8904
9379
  maxConcurrent;
8905
9380
  runningCount = 0;
8906
9381
  constructor(opts) {
@@ -8980,6 +9455,113 @@ var Orchestrator = class {
8980
9455
  void this.drainQueue(thread.id);
8981
9456
  }
8982
9457
  }
9458
+ this.schedulePendingQuotaResumes();
9459
+ }
9460
+ clearQuotaResumeTimer(threadId) {
9461
+ const timer = this.quotaResumeTimers.get(threadId);
9462
+ if (timer) clearTimeout(timer);
9463
+ this.quotaResumeTimers.delete(threadId);
9464
+ }
9465
+ /** Schedule (or fire) auto-retry after a provider session/usage limit reset. */
9466
+ scheduleQuotaResume(threadId, resumeAt) {
9467
+ this.clearQuotaResumeTimer(threadId);
9468
+ updateThread(threadId, { quotaResumeAt: resumeAt.toISOString() });
9469
+ const delay = Math.max(5e3, resumeAt.getTime() - Date.now());
9470
+ const capped = Math.min(delay, 2147483647);
9471
+ const timer = setTimeout(() => {
9472
+ this.quotaResumeTimers.delete(threadId);
9473
+ void this.resumeAfterQuotaWait(threadId);
9474
+ }, capped);
9475
+ this.quotaResumeTimers.set(threadId, timer);
9476
+ }
9477
+ schedulePendingQuotaResumes() {
9478
+ for (const thread of listThreads({ includeArchived: false })) {
9479
+ if (!thread.quotaResumeAt) continue;
9480
+ const at = new Date(thread.quotaResumeAt);
9481
+ if (Number.isNaN(at.getTime())) continue;
9482
+ if (at.getTime() <= Date.now()) {
9483
+ void this.resumeAfterQuotaWait(thread.id);
9484
+ } else if (!this.quotaResumeTimers.has(thread.id)) {
9485
+ this.scheduleQuotaResume(thread.id, at);
9486
+ }
9487
+ }
9488
+ }
9489
+ async resumeAfterQuotaWait(threadId) {
9490
+ const thread = findThreadByRef(threadId);
9491
+ if (!thread || thread.status === "archived") return;
9492
+ this.clearQuotaResumeTimer(threadId);
9493
+ try {
9494
+ updateThread(threadId, { quotaResumeAt: null });
9495
+ } catch {
9496
+ return;
9497
+ }
9498
+ if (thread.status === "running" || this.activeTurns.has(threadId) || this.startingTurns.has(threadId)) {
9499
+ return;
9500
+ }
9501
+ await this.send(threadId, QUOTA_RESUME_PROMPT);
9502
+ }
9503
+ /**
9504
+ * Host-side continue when an orchestration chat hits a provider session/usage
9505
+ * limit (not context size): switch agent (Auto) or wait until reset.
9506
+ */
9507
+ async maybeHandleOrchestrationQuotaFailover(threadId, limitText) {
9508
+ const thread = findThreadByRef(threadId);
9509
+ if (!thread) return;
9510
+ const plan = planOrchestrationQuotaFailover(thread, limitText);
9511
+ if (!plan || plan.action === "none") return;
9512
+ if (plan.action === "wait_reset" && plan.resumeAt) {
9513
+ this.haltDrain.add(threadId);
9514
+ this.scheduleQuotaResume(threadId, plan.resumeAt);
9515
+ setStatus(threadId, "idle", null);
9516
+ appendMessage(threadId, {
9517
+ role: "agent",
9518
+ text: `Sideboard will auto-retry this orchestration around ${plan.resumeAt.toLocaleString()} when the session limit resets.`,
9519
+ ts: (/* @__PURE__ */ new Date()).toISOString()
9520
+ });
9521
+ this.emit({
9522
+ type: "quota_failover",
9523
+ threadId,
9524
+ action: "wait_reset",
9525
+ message: plan.reason,
9526
+ resumeAt: plan.resumeAt.toISOString()
9527
+ });
9528
+ this.emit({ type: "status_changed", threadId, status: "idle" });
9529
+ return;
9530
+ }
9531
+ if (plan.action === "switch_agent" && plan.fallbackAgent) {
9532
+ this.haltDrain.add(threadId);
9533
+ const next = createQuotaFailoverChat(
9534
+ thread,
9535
+ plan.fallbackAgent,
9536
+ plan.limitText
9537
+ );
9538
+ this.clearQuotaResumeTimer(threadId);
9539
+ try {
9540
+ updateThread(threadId, { quotaResumeAt: null });
9541
+ } catch {
9542
+ }
9543
+ appendMessage(threadId, {
9544
+ role: "agent",
9545
+ text: `Session limit on ${thread.agent}. Sideboard continued on ${plan.fallbackAgent} (Auto) in [${next.title}](sideboard://thread/${next.id}).`,
9546
+ ts: (/* @__PURE__ */ new Date()).toISOString()
9547
+ });
9548
+ this.emit({
9549
+ type: "quota_failover",
9550
+ threadId,
9551
+ action: "switch_agent",
9552
+ toThreadId: next.id,
9553
+ message: plan.reason
9554
+ });
9555
+ this.emit({
9556
+ type: "status_changed",
9557
+ threadId: next.id,
9558
+ status: next.status
9559
+ });
9560
+ await this.send(
9561
+ next.id,
9562
+ QUOTA_CONTINUE_PROMPT(thread.agent, plan.fallbackAgent)
9563
+ );
9564
+ }
8983
9565
  }
8984
9566
  getThreads(includeArchived = false) {
8985
9567
  return listThreads({ includeArchived });
@@ -9338,11 +9920,16 @@ var Orchestrator = class {
9338
9920
  }
9339
9921
  }
9340
9922
  }
9341
- const failureOnlyMessage = exitCode !== 0 && looksLikeAgentFailureMessage(assistantText) && !parts.some((p) => p.type === "tool" || p.type === "thinking");
9342
- if (!failureOnlyMessage && (assistantText || parts.length > 0)) {
9923
+ const lastStderr = summarizeTurnStderr(stderrTail);
9924
+ const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
9925
+ let chatText = assistantText;
9926
+ if (exitCode !== 0 && !chatText && looksLikeAgentFailureMessage(detail)) {
9927
+ chatText = humanizeAgentFailDetail(detail);
9928
+ }
9929
+ if (chatText || parts.length > 0) {
9343
9930
  appendMessage(threadId, {
9344
9931
  role: "agent",
9345
- text: assistantText,
9932
+ text: chatText,
9346
9933
  parts: parts.length > 0 ? parts : void 0,
9347
9934
  durationMs: Math.max(0, Date.now() - turnStartedAt),
9348
9935
  usage,
@@ -9361,13 +9948,12 @@ var Orchestrator = class {
9361
9948
  this.emit({ type: "status_changed", threadId, status: "stopped" });
9362
9949
  this.emit({ type: "turn_finished", threadId, exitCode });
9363
9950
  } else {
9364
- const lastStderr = summarizeTurnStderr(stderrTail);
9365
- const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
9366
9951
  const failDetail = formatTurnExitError(exitCode, detail);
9952
+ const explainedInChat = exitCode !== 0 && Boolean(chatText) && (looksLikeAgentFailureMessage(chatText) || failDetail && chatText.includes(failDetail.replace(/^exit\s*\d+:\s*/i, "").trim()));
9367
9953
  setStatus(
9368
9954
  threadId,
9369
9955
  exitCode === 0 ? "idle" : "error",
9370
- exitCode === 0 ? null : failDetail
9956
+ exitCode === 0 || explainedInChat ? null : failDetail
9371
9957
  );
9372
9958
  this.emit({
9373
9959
  type: "status_changed",
@@ -9375,6 +9961,10 @@ var Orchestrator = class {
9375
9961
  status: exitCode === 0 ? "idle" : "error"
9376
9962
  });
9377
9963
  this.emit({ type: "turn_finished", threadId, exitCode });
9964
+ if (exitCode !== 0) {
9965
+ const blob = [chatText, detail].filter(Boolean).join("\n");
9966
+ void this.maybeHandleOrchestrationQuotaFailover(threadId, blob);
9967
+ }
9378
9968
  }
9379
9969
  } catch (err) {
9380
9970
  const message = err instanceof Error ? err.message : String(err);
@@ -9388,6 +9978,7 @@ var Orchestrator = class {
9388
9978
  this.emit({ type: "error", threadId, message });
9389
9979
  this.emit({ type: "status_changed", threadId, status: "error" });
9390
9980
  this.emit({ type: "turn_finished", threadId, exitCode: 1 });
9981
+ void this.maybeHandleOrchestrationQuotaFailover(threadId, message);
9391
9982
  }
9392
9983
  } finally {
9393
9984
  this.startingTurns.delete(threadId);
@@ -9872,6 +10463,9 @@ var Orchestrator = class {
9872
10463
  `Cannot switch agent provider mid-chat (${thread.agent} \u2192 ${patch.agent}). Start a new chat tab instead.`
9873
10464
  );
9874
10465
  }
10466
+ if (isOrchestratorThread(thread)) {
10467
+ assertOrchestratorCapableAgent(patch.agent);
10468
+ }
9875
10469
  next.agent = patch.agent;
9876
10470
  if (patch.agent !== "claude" && patch.model === void 0) next.model = null;
9877
10471
  next.sessionId = null;
@@ -10154,6 +10748,7 @@ async function listIssues(repoPath) {
10154
10748
 
10155
10749
  // src/mcp/server.ts
10156
10750
  init_global_workspace();
10751
+ init_list_models();
10157
10752
 
10158
10753
  // src/mcp/archive-guard.ts
10159
10754
  init_global_workspace();
@@ -10570,6 +11165,120 @@ async function startMcpServer() {
10570
11165
  }
10571
11166
  }
10572
11167
  );
11168
+ const agentEnum = import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
11169
+ server.tool(
11170
+ "list_models",
11171
+ "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.",
11172
+ {
11173
+ agent: agentEnum.optional().describe("Limit to one agent; omit for all")
11174
+ },
11175
+ async ({ agent }) => {
11176
+ try {
11177
+ const catalogs = await listModelsForAgent(agent);
11178
+ return {
11179
+ content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
11180
+ };
11181
+ } catch (err) {
11182
+ const message = err instanceof Error ? err.message : String(err);
11183
+ return { content: [{ type: "text", text: message }], isError: true };
11184
+ }
11185
+ }
11186
+ );
11187
+ server.tool(
11188
+ "fork_worktree",
11189
+ "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.",
11190
+ {
11191
+ ref: import_zod.z.string().describe("Worktree thread id/ref to fork"),
11192
+ through_index: import_zod.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
11193
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
11194
+ model: import_zod.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
11195
+ title: import_zod.z.string().optional()
11196
+ },
11197
+ async ({ ref, through_index, agent, model, title }) => {
11198
+ try {
11199
+ const source = orch.getThread(ref);
11200
+ if (source) await orch.reconcile(source.repoPath);
11201
+ const thread = await orch.forkThreadWorktree({
11202
+ threadId: ref,
11203
+ throughIndex: through_index,
11204
+ agent,
11205
+ model,
11206
+ title
11207
+ });
11208
+ return {
11209
+ content: [
11210
+ {
11211
+ type: "text",
11212
+ text: JSON.stringify({
11213
+ id: thread.id,
11214
+ title: thread.title,
11215
+ status: thread.status,
11216
+ agent: thread.agent,
11217
+ model: thread.model,
11218
+ branchName: thread.branchName,
11219
+ worktreePath: thread.worktreePath,
11220
+ fromThreadId: source?.id ?? ref,
11221
+ link: `sideboard://thread/${thread.id}`
11222
+ })
11223
+ }
11224
+ ]
11225
+ };
11226
+ } catch (err) {
11227
+ const message = err instanceof Error ? err.message : String(err);
11228
+ return { content: [{ type: "text", text: message }], isError: true };
11229
+ }
11230
+ }
11231
+ );
11232
+ server.tool(
11233
+ "fork_chat",
11234
+ "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). 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.",
11235
+ {
11236
+ ref: import_zod.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
11237
+ through_index: import_zod.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
11238
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
11239
+ model: import_zod.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
11240
+ title: import_zod.z.string().optional()
11241
+ },
11242
+ async ({ ref, through_index, agent, model, title }) => {
11243
+ try {
11244
+ const source = orch.getThread(ref);
11245
+ if (!source) {
11246
+ return {
11247
+ content: [{ type: "text", text: `Thread not found: ${ref}` }],
11248
+ isError: true
11249
+ };
11250
+ }
11251
+ const tab = orch.forkChatTab({
11252
+ threadId: source.id,
11253
+ throughIndex: through_index,
11254
+ agent,
11255
+ model,
11256
+ title
11257
+ });
11258
+ return {
11259
+ content: [
11260
+ {
11261
+ type: "text",
11262
+ text: JSON.stringify({
11263
+ id: tab.id,
11264
+ title: tab.title,
11265
+ status: tab.status,
11266
+ agent: tab.agent,
11267
+ model: tab.model,
11268
+ sourceType: tab.sourceType,
11269
+ worktreePath: tab.worktreePath,
11270
+ fromThreadId: source.id,
11271
+ link: `sideboard://thread/${tab.id}`
11272
+ })
11273
+ }
11274
+ ]
11275
+ };
11276
+ } catch (err) {
11277
+ const message = err instanceof Error ? err.message : String(err);
11278
+ return { content: [{ type: "text", text: message }], isError: true };
11279
+ }
11280
+ }
11281
+ );
10573
11282
  server.tool(
10574
11283
  "run_dev_script",
10575
11284
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",