@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.
package/dist/index.cjs CHANGED
@@ -412,6 +412,8 @@ __export(app_settings_exports, {
412
412
  isLinearConnected: () => isLinearConnected,
413
413
  loadAppSettings: () => loadAppSettings,
414
414
  maxConcurrentAgents: () => maxConcurrentAgents,
415
+ orchestrationQuotaFallbackAgent: () => orchestrationQuotaFallbackAgent,
416
+ orchestrationQuotaOnLimit: () => orchestrationQuotaOnLimit,
415
417
  resolveClaudeExecutable: () => resolveClaudeExecutable,
416
418
  resolveEffectiveIssueSource: () => resolveEffectiveIssueSource,
417
419
  resolveThreadDefaults: () => resolveThreadDefaults,
@@ -523,6 +525,12 @@ function normalizeAdvanced(raw) {
523
525
  if (typeof source.autoCleanupOrphans === "boolean") {
524
526
  out.autoCleanupOrphans = source.autoCleanupOrphans;
525
527
  }
528
+ if (source.orchestrationQuotaOnLimit === "switch_agent" || source.orchestrationQuotaOnLimit === "wait_reset") {
529
+ out.orchestrationQuotaOnLimit = source.orchestrationQuotaOnLimit;
530
+ }
531
+ if (typeof source.orchestrationQuotaFallbackAgent === "string" && DEFAULT_AGENTS.has(source.orchestrationQuotaFallbackAgent) && source.orchestrationQuotaFallbackAgent !== "brightsy") {
532
+ out.orchestrationQuotaFallbackAgent = source.orchestrationQuotaFallbackAgent;
533
+ }
526
534
  return out;
527
535
  }
528
536
  function normalizeSettings(raw) {
@@ -757,6 +765,12 @@ function updateAdvancedSettings(patch) {
757
765
  if (typeof patch.autoCleanupOrphans === "boolean") {
758
766
  advanced.autoCleanupOrphans = patch.autoCleanupOrphans;
759
767
  }
768
+ if (patch.orchestrationQuotaOnLimit === "switch_agent" || patch.orchestrationQuotaOnLimit === "wait_reset") {
769
+ advanced.orchestrationQuotaOnLimit = patch.orchestrationQuotaOnLimit;
770
+ }
771
+ if (typeof patch.orchestrationQuotaFallbackAgent === "string" && DEFAULT_AGENTS.has(patch.orchestrationQuotaFallbackAgent) && patch.orchestrationQuotaFallbackAgent !== "brightsy") {
772
+ advanced.orchestrationQuotaFallbackAgent = patch.orchestrationQuotaFallbackAgent;
773
+ }
760
774
  return saveAppSettings({ ...current, advanced });
761
775
  }
762
776
  function autoRenameBranchEnabled(settings = loadAppSettings()) {
@@ -777,6 +791,16 @@ function deleteBranchOnPurgeEnabled(settings = loadAppSettings()) {
777
791
  function autoCleanupOrphansEnabled(settings = loadAppSettings()) {
778
792
  return Boolean(settings.advanced.autoCleanupOrphans);
779
793
  }
794
+ function orchestrationQuotaOnLimit(settings = loadAppSettings()) {
795
+ return settings.advanced.orchestrationQuotaOnLimit ?? "switch_agent";
796
+ }
797
+ function orchestrationQuotaFallbackAgent(settings = loadAppSettings()) {
798
+ const preferred = settings.advanced.orchestrationQuotaFallbackAgent;
799
+ if (preferred && DEFAULT_AGENTS.has(preferred) && preferred !== "brightsy") {
800
+ return preferred;
801
+ }
802
+ return "cursor";
803
+ }
780
804
  function maxConcurrentAgents(settings = loadAppSettings()) {
781
805
  const n = settings.advanced.maxConcurrent;
782
806
  if (typeof n === "number" && Number.isFinite(n)) {
@@ -893,7 +917,9 @@ function normalizeThread(raw) {
893
917
  attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
894
918
  prTitle: raw.prTitle ?? null,
895
919
  userSetTitle: Boolean(raw.userSetTitle),
896
- activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : []
920
+ activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
921
+ quotaResumeAt: raw.quotaResumeAt ?? null,
922
+ quotaContinuedFromId: raw.quotaContinuedFromId ?? null
897
923
  };
898
924
  }
899
925
  function createEmptyThread(partial) {
@@ -1052,6 +1078,36 @@ var init_cloud_connect_constants = __esm({
1052
1078
  }
1053
1079
  });
1054
1080
 
1081
+ // src/agents/orchestrator-capable.ts
1082
+ function isOrchestratorCapableAgent(agent) {
1083
+ return Boolean(
1084
+ agent && ORCHESTRATOR_AGENT_KINDS.includes(agent)
1085
+ );
1086
+ }
1087
+ function assertOrchestratorCapableAgent(agent, context = "orchestration") {
1088
+ if (!isOrchestratorCapableAgent(agent)) {
1089
+ throw new Error(
1090
+ `${agent} cannot run ${context} \u2014 it does not support Sideboard MCP. Use Claude, Cursor, Codex, or OpenCode.`
1091
+ );
1092
+ }
1093
+ return agent;
1094
+ }
1095
+ function coerceOrchestratorAgent(agent, fallback = "claude") {
1096
+ return isOrchestratorCapableAgent(agent) ? agent : fallback;
1097
+ }
1098
+ var ORCHESTRATOR_AGENT_KINDS;
1099
+ var init_orchestrator_capable = __esm({
1100
+ "src/agents/orchestrator-capable.ts"() {
1101
+ "use strict";
1102
+ ORCHESTRATOR_AGENT_KINDS = [
1103
+ "claude",
1104
+ "codex",
1105
+ "opencode",
1106
+ "cursor"
1107
+ ];
1108
+ }
1109
+ });
1110
+
1055
1111
  // src/git/team-meta.ts
1056
1112
  var SOCCER_TEAM_META;
1057
1113
  var init_team_meta = __esm({
@@ -3184,11 +3240,14 @@ var init_coordinator_prompt = __esm({
3184
3240
  "Discover:",
3185
3241
  "- list_workspaces \u2014 registered repos (path + github slug when known)",
3186
3242
  "- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces (issues: Linear API or GitHub Issues)",
3243
+ "- list_models \u2014 only when you need a specific model (rare); otherwise leave model unset = Auto",
3187
3244
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
3188
3245
  "Workspaces:",
3189
3246
  "- add_workspace / remove_workspace \u2014 register or unregister a git repo",
3190
3247
  "Worktree threads (chats):",
3191
3248
  "- create_thread \u2014 create a worktree + chat from branch | pr | ticket; pass repoPath + parentThreadId",
3249
+ "- 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.",
3250
+ "- 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.",
3192
3251
  "- 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",
3193
3252
  "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply",
3194
3253
  "- stop_thread \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
@@ -3286,6 +3345,7 @@ function createGlobalChat(opts) {
3286
3345
  const explicit = opts.title?.trim();
3287
3346
  const title = explicit && explicit !== CLOUD_ORCHESTRATOR_GOAL ? explicit : allocateTeamName(takenTeamSlugsForOrchestration()).name;
3288
3347
  const sourceRef = opts.sourceRef?.trim() || (isCloud ? CLOUD_ORCHESTRATOR_GOAL : title);
3348
+ const agent = assertOrchestratorCapableAgent(opts.agent);
3289
3349
  const thread = createEmptyThread({
3290
3350
  title,
3291
3351
  // Stick nicknames the same way chat tabs do (avoid later sync overwrites).
@@ -3295,7 +3355,7 @@ function createGlobalChat(opts) {
3295
3355
  branchName: "global",
3296
3356
  worktreePath: globalAgentCwd(),
3297
3357
  repoPath: GLOBAL_WORKSPACE_ID,
3298
- agent: opts.agent,
3358
+ agent,
3299
3359
  autonomy: opts.autonomy ?? "default",
3300
3360
  model: opts.model ?? null,
3301
3361
  effort: opts.effort ?? "high",
@@ -3357,6 +3417,7 @@ var init_global_workspace = __esm({
3357
3417
  "src/store/global-workspace.ts"() {
3358
3418
  "use strict";
3359
3419
  init_cloud_connect_constants();
3420
+ init_orchestrator_capable();
3360
3421
  init_teams();
3361
3422
  init_coordinator_prompt();
3362
3423
  init_paths();
@@ -3941,11 +4002,15 @@ function humanizeAgentFailDetail(detail) {
3941
4002
  }
3942
4003
  function formatTurnExitError(exitCode, stderrSummary) {
3943
4004
  const code = exitCode ?? 1;
3944
- const detail = humanizeAgentFailDetail(stderrSummary);
4005
+ const raw = stderrSummary.trim();
4006
+ if (/^exit\s*\d+$/i.test(raw)) {
4007
+ return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
4008
+ }
4009
+ const detail = humanizeAgentFailDetail(raw);
3945
4010
  if (!detail) {
3946
4011
  return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
3947
4012
  }
3948
- if (looksLikeAgentFailureMessage(stderrSummary)) return detail;
4013
+ if (looksLikeAgentFailureMessage(raw)) return detail;
3949
4014
  return `exit ${code}: ${detail}`;
3950
4015
  }
3951
4016
  var NODE_VERSION_FOOTER;
@@ -4508,6 +4573,45 @@ async function buildInjectedMcpServers(opts) {
4508
4573
  }
4509
4574
  return servers;
4510
4575
  }
4576
+ function toCursorMcpServers(servers) {
4577
+ const out = {};
4578
+ for (const s of servers) {
4579
+ out[s.name] = {
4580
+ command: s.command,
4581
+ ...s.args ? { args: s.args } : {},
4582
+ ...s.env ? { env: s.env } : {}
4583
+ };
4584
+ }
4585
+ return out;
4586
+ }
4587
+ function toCodexMcpConfigArgs(servers) {
4588
+ const args = [];
4589
+ for (const s of servers) {
4590
+ const prefix = `mcp_servers.${s.name}`;
4591
+ args.push("-c", `${prefix}.command=${JSON.stringify(s.command)}`);
4592
+ if (s.args?.length) {
4593
+ args.push("-c", `${prefix}.args=${JSON.stringify(s.args)}`);
4594
+ }
4595
+ if (s.env) {
4596
+ for (const [key, value] of Object.entries(s.env)) {
4597
+ args.push("-c", `${prefix}.env.${key}=${JSON.stringify(value)}`);
4598
+ }
4599
+ }
4600
+ }
4601
+ return args;
4602
+ }
4603
+ function toOpencodeMcpConfigContent(servers) {
4604
+ const mcp = {};
4605
+ for (const s of servers) {
4606
+ mcp[s.name] = {
4607
+ type: "local",
4608
+ command: [s.command, ...s.args ?? []],
4609
+ enabled: true,
4610
+ ...s.env && Object.keys(s.env).length > 0 ? { environment: s.env } : {}
4611
+ };
4612
+ }
4613
+ return JSON.stringify({ mcp });
4614
+ }
4511
4615
  function writeMcpServersConfig(servers) {
4512
4616
  if (servers.length === 0) return null;
4513
4617
  const mcpServers = {};
@@ -5015,6 +5119,7 @@ var init_codex = __esm({
5015
5119
  import_node_path12 = require("path");
5016
5120
  init_run();
5017
5121
  init_error_detail();
5122
+ init_injected_mcp();
5018
5123
  init_turn_input();
5019
5124
  init_types();
5020
5125
  CODEX_PROMPT_ARG_MAX = 2e5;
@@ -5073,6 +5178,11 @@ var init_codex = __esm({
5073
5178
  }
5074
5179
  const mode = permissionMode(thread);
5075
5180
  const model = thread.model?.trim();
5181
+ const injected = await buildInjectedMcpServers({
5182
+ includeSideboard: true,
5183
+ includeBrightsy: isBrightsyConnected()
5184
+ });
5185
+ const mcpOverrides = toCodexMcpConfigArgs(injected);
5076
5186
  const args = [
5077
5187
  "exec",
5078
5188
  ...sessionId ? ["resume", sessionId] : [],
@@ -5084,7 +5194,8 @@ var init_codex = __esm({
5084
5194
  mode.codexSandbox,
5085
5195
  "--ask-for-approval",
5086
5196
  "never",
5087
- ...model ? ["--model", model] : []
5197
+ ...model ? ["--model", model] : [],
5198
+ ...mcpOverrides
5088
5199
  ];
5089
5200
  return {
5090
5201
  file: "codex",
@@ -5369,6 +5480,7 @@ var init_cursor = __esm({
5369
5480
  init_run();
5370
5481
  init_app_settings();
5371
5482
  init_cursor_events();
5483
+ init_injected_mcp();
5372
5484
  init_node_launch();
5373
5485
  init_turn_input();
5374
5486
  init_cursor_events();
@@ -5419,6 +5531,11 @@ var init_cursor = __esm({
5419
5531
  const prompt = flattenTurnInput(input);
5420
5532
  const agentId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
5421
5533
  const apiKey = resolveCursorApiKey() || void 0;
5534
+ const injected = await buildInjectedMcpServers({
5535
+ includeSideboard: true,
5536
+ includeBrightsy: isBrightsyConnected()
5537
+ });
5538
+ const mcpServers = toCursorMcpServers(injected);
5422
5539
  const req = {
5423
5540
  prompt,
5424
5541
  cwd: thread.worktreePath,
@@ -5427,7 +5544,8 @@ var init_cursor = __esm({
5427
5544
  effort: thread.effort,
5428
5545
  fast: thread.fast,
5429
5546
  planMode: thread.planMode,
5430
- apiKey
5547
+ apiKey,
5548
+ ...Object.keys(mcpServers).length > 0 ? { mcpServers } : {}
5431
5549
  };
5432
5550
  const runner = cursorRunnerPath();
5433
5551
  const isTs = runner.endsWith(".ts");
@@ -5521,6 +5639,7 @@ var init_opencode = __esm({
5521
5639
  "use strict";
5522
5640
  init_run();
5523
5641
  init_error_detail();
5642
+ init_injected_mcp();
5524
5643
  init_turn_input();
5525
5644
  init_types();
5526
5645
  FALLBACK_OPENCODE_MODELS = [
@@ -5581,6 +5700,11 @@ var init_opencode = __esm({
5581
5700
  if (model) {
5582
5701
  args.push("--model", model);
5583
5702
  }
5703
+ const injected = await buildInjectedMcpServers({
5704
+ includeSideboard: true,
5705
+ includeBrightsy: isBrightsyConnected()
5706
+ });
5707
+ const mcpContent = injected.length > 0 ? toOpencodeMcpConfigContent(injected) : null;
5584
5708
  return {
5585
5709
  file: "opencode",
5586
5710
  args,
@@ -5589,7 +5713,8 @@ var init_opencode = __esm({
5589
5713
  // message is given (see resolveRunInput in opencode's run.ts).
5590
5714
  stdin: prompt,
5591
5715
  env: {
5592
- OPENCODE_PERMISSION: mode.opencodePermission
5716
+ OPENCODE_PERMISSION: mode.opencodePermission,
5717
+ ...mcpContent ? { OPENCODE_CONFIG_CONTENT: mcpContent } : {}
5593
5718
  }
5594
5719
  };
5595
5720
  },
@@ -5709,6 +5834,201 @@ var init_opencode = __esm({
5709
5834
  }
5710
5835
  });
5711
5836
 
5837
+ // src/agents/list-models.ts
5838
+ async function listBrightsyModels() {
5839
+ try {
5840
+ const targets = await listBrightsyChatTargets();
5841
+ const accountId = targets.activeAccountId;
5842
+ const models = (targets.models ?? []).map((m) => ({
5843
+ id: encodeBrightsyTarget("model", m.id, accountId),
5844
+ displayName: m.name || m.id,
5845
+ description: m.description ?? void 0
5846
+ }));
5847
+ const agents = (targets.agents ?? []).map((a) => ({
5848
+ id: encodeBrightsyTarget("agent", a.id, accountId),
5849
+ displayName: a.name || a.id,
5850
+ description: a.description ?? "Brightsy agent target"
5851
+ }));
5852
+ return [...models, ...agents].slice(0, 80);
5853
+ } catch {
5854
+ return [];
5855
+ }
5856
+ }
5857
+ async function listModelsForAgent(agent) {
5858
+ const kinds = agent ? [agent] : ["claude", "codex", "opencode", "cursor", "brightsy"];
5859
+ const out = [];
5860
+ for (const kind of kinds) {
5861
+ if (kind === "claude") {
5862
+ out.push({
5863
+ agent: kind,
5864
+ auto: true,
5865
+ models: CLAUDE_MODEL_CATALOG,
5866
+ note: "Default Auto \u2014 only pass a model id when you have a reason."
5867
+ });
5868
+ continue;
5869
+ }
5870
+ if (kind === "codex") {
5871
+ out.push({
5872
+ agent: kind,
5873
+ auto: true,
5874
+ models: await listCodexModels(),
5875
+ note: "Default Auto \u2014 only pass a model slug when you have a reason."
5876
+ });
5877
+ continue;
5878
+ }
5879
+ if (kind === "opencode") {
5880
+ out.push({
5881
+ agent: kind,
5882
+ auto: true,
5883
+ models: await listOpencodeModels(),
5884
+ note: "Default Auto \u2014 only pass a provider/model id when you have a reason."
5885
+ });
5886
+ continue;
5887
+ }
5888
+ if (kind === "cursor") {
5889
+ out.push({
5890
+ agent: kind,
5891
+ auto: true,
5892
+ models: await listCursorModels(),
5893
+ note: 'Default Auto \u2014 only pass a model id when you have a reason (or use "default").'
5894
+ });
5895
+ continue;
5896
+ }
5897
+ if (kind === "brightsy") {
5898
+ const models = await listBrightsyModels();
5899
+ out.push({
5900
+ agent: kind,
5901
+ auto: true,
5902
+ models,
5903
+ 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."
5904
+ });
5905
+ }
5906
+ }
5907
+ return out;
5908
+ }
5909
+ var CLAUDE_MODEL_CATALOG;
5910
+ var init_list_models = __esm({
5911
+ "src/agents/list-models.ts"() {
5912
+ "use strict";
5913
+ init_brightsy();
5914
+ init_brightsy_targets();
5915
+ init_codex();
5916
+ init_cursor();
5917
+ init_opencode();
5918
+ CLAUDE_MODEL_CATALOG = [
5919
+ { id: "fable", displayName: "Fable" },
5920
+ { id: "opus", displayName: "Opus" },
5921
+ { id: "sonnet", displayName: "Sonnet" },
5922
+ { id: "haiku", displayName: "Haiku" }
5923
+ ];
5924
+ }
5925
+ });
5926
+
5927
+ // src/agents/session-quota.ts
5928
+ function isSessionQuotaLimit(text) {
5929
+ const lower = text.trim().toLowerCase();
5930
+ if (!lower) return false;
5931
+ if (/credit balance is too low|out of credits|insufficient.?quota|billing/.test(lower)) {
5932
+ return false;
5933
+ }
5934
+ if (/prompt is too long|context.*(too long|exceed)|conversation too long/.test(lower)) {
5935
+ return false;
5936
+ }
5937
+ 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);
5938
+ }
5939
+ function parseSessionQuotaResetAt(text, now = /* @__PURE__ */ new Date()) {
5940
+ const absolute = text.match(
5941
+ /resets\s+(?:at\s+)?(\d{1,2}):(\d{2})\s*(am|pm)(?:\s*\(([^)]+)\))?/i
5942
+ );
5943
+ if (absolute) {
5944
+ const hour12 = Number(absolute[1]);
5945
+ const minute = Number(absolute[2]);
5946
+ const ampm = absolute[3].toLowerCase();
5947
+ const timeZone = absolute[4]?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone;
5948
+ let hour = hour12 % 12;
5949
+ if (ampm === "pm") hour += 12;
5950
+ const at = zonedWallTimeToUtc(now, hour, minute, timeZone);
5951
+ if (!at) return null;
5952
+ if (at.getTime() <= now.getTime() + 3e4) {
5953
+ const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1e3);
5954
+ return zonedWallTimeToUtc(tomorrow, hour, minute, timeZone);
5955
+ }
5956
+ return at;
5957
+ }
5958
+ const relative = text.match(
5959
+ /resets\s+in\s+(\d+)\s*(minutes?|hours?|days?)/i
5960
+ );
5961
+ if (relative) {
5962
+ const n = Number(relative[1]);
5963
+ const unit = relative[2].toLowerCase();
5964
+ const ms = unit.startsWith("day") ? n * 24 * 60 * 60 * 1e3 : unit.startsWith("hour") ? n * 60 * 60 * 1e3 : n * 60 * 1e3;
5965
+ return new Date(now.getTime() + ms);
5966
+ }
5967
+ return null;
5968
+ }
5969
+ function zonedWallTimeToUtc(day, hour, minute, timeZone) {
5970
+ try {
5971
+ const cal = new Intl.DateTimeFormat("en-US", {
5972
+ timeZone,
5973
+ year: "numeric",
5974
+ month: "2-digit",
5975
+ day: "2-digit"
5976
+ });
5977
+ const parts = Object.fromEntries(
5978
+ cal.formatToParts(day).filter((p) => p.type !== "literal").map((p) => [p.type, p.value])
5979
+ );
5980
+ const year = Number(parts.year);
5981
+ const month = Number(parts.month);
5982
+ const date = Number(parts.day);
5983
+ if (![year, month, date].every((n) => Number.isFinite(n))) return null;
5984
+ const utcGuess = Date.UTC(year, month - 1, date, hour, minute, 0);
5985
+ const dtf = new Intl.DateTimeFormat("en-US", {
5986
+ timeZone,
5987
+ year: "numeric",
5988
+ month: "2-digit",
5989
+ day: "2-digit",
5990
+ hour: "2-digit",
5991
+ minute: "2-digit",
5992
+ second: "2-digit",
5993
+ hourCycle: "h23"
5994
+ });
5995
+ const asParts = Object.fromEntries(
5996
+ dtf.formatToParts(new Date(utcGuess)).filter((p) => p.type !== "literal").map((p) => [p.type, p.value])
5997
+ );
5998
+ const asUtc = Date.UTC(
5999
+ Number(asParts.year),
6000
+ Number(asParts.month) - 1,
6001
+ Number(asParts.day),
6002
+ Number(asParts.hour),
6003
+ Number(asParts.minute),
6004
+ Number(asParts.second || "0")
6005
+ );
6006
+ const offset = asUtc - utcGuess;
6007
+ return new Date(utcGuess - offset);
6008
+ } catch {
6009
+ return null;
6010
+ }
6011
+ }
6012
+ function resolveQuotaFallbackAgent(current, preferred) {
6013
+ const ordered = [
6014
+ ...preferred && preferred !== "brightsy" ? [preferred] : [],
6015
+ ...FALLBACK_ORDER.filter((a) => a !== preferred)
6016
+ ];
6017
+ return ordered.find((a) => a !== current) ?? (current === "cursor" ? "codex" : "cursor");
6018
+ }
6019
+ var FALLBACK_ORDER;
6020
+ var init_session_quota = __esm({
6021
+ "src/agents/session-quota.ts"() {
6022
+ "use strict";
6023
+ FALLBACK_ORDER = [
6024
+ "cursor",
6025
+ "codex",
6026
+ "opencode",
6027
+ "claude"
6028
+ ];
6029
+ }
6030
+ });
6031
+
5712
6032
  // src/agents/install.ts
5713
6033
  function getAgentSetupInfo(agent) {
5714
6034
  return SETUP[agent];
@@ -5889,11 +6209,15 @@ var init_install = __esm({
5889
6209
  // src/agents/index.ts
5890
6210
  var agents_exports = {};
5891
6211
  __export(agents_exports, {
6212
+ CLAUDE_MODEL_CATALOG: () => CLAUDE_MODEL_CATALOG,
6213
+ ORCHESTRATOR_AGENT_KINDS: () => ORCHESTRATOR_AGENT_KINDS,
5892
6214
  PLAN_MODE_INSTRUCTION: () => PLAN_MODE_INSTRUCTION,
5893
6215
  allAdapters: () => allAdapters,
6216
+ assertOrchestratorCapableAgent: () => assertOrchestratorCapableAgent,
5894
6217
  brightsyAdapter: () => brightsyAdapter,
5895
6218
  claudeAdapter: () => claudeAdapter,
5896
6219
  codexAdapter: () => codexAdapter,
6220
+ coerceOrchestratorAgent: () => coerceOrchestratorAgent,
5897
6221
  cursorAdapter: () => cursorAdapter,
5898
6222
  cursorSdkMessageToEvents: () => cursorSdkMessageToEvents,
5899
6223
  decodeBrightsyTarget: () => decodeBrightsyTarget,
@@ -5903,17 +6227,22 @@ __export(agents_exports, {
5903
6227
  getAgentSetupInfo: () => getAgentSetupInfo,
5904
6228
  installAgent: () => installAgent,
5905
6229
  isCursorAutoModel: () => isCursorAutoModel,
6230
+ isOrchestratorCapableAgent: () => isOrchestratorCapableAgent,
6231
+ isSessionQuotaLimit: () => isSessionQuotaLimit,
5906
6232
  listAgentSetupInfo: () => listAgentSetupInfo,
5907
6233
  listBrightsyChatTargets: () => listBrightsyChatTargets,
5908
6234
  listCodexModels: () => listCodexModels,
5909
6235
  listCursorModels: () => listCursorModels,
6236
+ listModelsForAgent: () => listModelsForAgent,
5910
6237
  listOpencodeModels: () => listOpencodeModels,
5911
6238
  loginAgent: () => loginAgent,
5912
6239
  openInSystemTerminal: () => openInSystemTerminal,
5913
6240
  opencodeAdapter: () => opencodeAdapter,
5914
6241
  parseCursorRunnerLine: () => parseCursorRunnerLine,
6242
+ parseSessionQuotaResetAt: () => parseSessionQuotaResetAt,
5915
6243
  permissionMode: () => permissionMode,
5916
- resolveCursorModelId: () => resolveCursorModelId
6244
+ resolveCursorModelId: () => resolveCursorModelId,
6245
+ resolveQuotaFallbackAgent: () => resolveQuotaFallbackAgent
5917
6246
  });
5918
6247
  function getAdapter(kind) {
5919
6248
  return adapters[kind];
@@ -5939,6 +6268,9 @@ var init_agents = __esm({
5939
6268
  init_cursor_events();
5940
6269
  init_cursor();
5941
6270
  init_opencode();
6271
+ init_list_models();
6272
+ init_session_quota();
6273
+ init_orchestrator_capable();
5942
6274
  init_path();
5943
6275
  init_install();
5944
6276
  adapters = {
@@ -6034,6 +6366,7 @@ var index_exports = {};
6034
6366
  __export(index_exports, {
6035
6367
  BRIGHTSY_MCP_ALLOWED_TOOLS: () => BRIGHTSY_MCP_ALLOWED_TOOLS,
6036
6368
  BrightsySideboardApi: () => BrightsySideboardApi,
6369
+ CLAUDE_MODEL_CATALOG: () => CLAUDE_MODEL_CATALOG,
6037
6370
  CLOUD_COORDINATOR_BUSY_REPLY: () => CLOUD_COORDINATOR_BUSY_REPLY,
6038
6371
  CLOUD_COORDINATOR_STOPPED_REPLY: () => CLOUD_COORDINATOR_STOPPED_REPLY,
6039
6372
  CLOUD_COORDINATOR_TIMEOUT_REPLY: () => CLOUD_COORDINATOR_TIMEOUT_REPLY,
@@ -6047,6 +6380,7 @@ __export(index_exports, {
6047
6380
  GLOBAL_WORKSPACE_ID: () => GLOBAL_WORKSPACE_ID,
6048
6381
  HARNESS_ENV_KEYS: () => HARNESS_ENV_KEYS,
6049
6382
  MAX_ANTHROPIC_CACHE_CONTROL_BLOCKS: () => MAX_ANTHROPIC_CACHE_CONTROL_BLOCKS,
6383
+ ORCHESTRATOR_AGENT_KINDS: () => ORCHESTRATOR_AGENT_KINDS,
6050
6384
  Orchestrator: () => Orchestrator,
6051
6385
  PASTE_ATTACH_MIN_CHARS: () => PASTE_ATTACH_MIN_CHARS,
6052
6386
  PASTE_ATTACH_MIN_LINES: () => PASTE_ATTACH_MIN_LINES,
@@ -6071,6 +6405,7 @@ __export(index_exports, {
6071
6405
  applyAppEnvironment: () => applyAppEnvironment,
6072
6406
  applyCompaction: () => applyCompaction,
6073
6407
  applyThreadIntoMain: () => applyThreadIntoMain,
6408
+ assertOrchestratorCapableAgent: () => assertOrchestratorCapableAgent,
6074
6409
  attachmentFromAbsolutePath: () => attachmentFromAbsolutePath,
6075
6410
  attachmentsFromBuffers: () => attachmentsFromBuffers,
6076
6411
  attachmentsFromWorktreePaths: () => attachmentsFromWorktreePaths,
@@ -6103,6 +6438,7 @@ __export(index_exports, {
6103
6438
  cleanupOrphanWorktrees: () => cleanupOrphanWorktrees,
6104
6439
  cloneRepoIntoSideboard: () => cloneRepoIntoSideboard,
6105
6440
  codexAdapter: () => codexAdapter,
6441
+ coerceOrchestratorAgent: () => coerceOrchestratorAgent,
6106
6442
  collectTakenTeamSlugs: () => collectTakenTeamSlugs,
6107
6443
  commitAll: () => commitAll,
6108
6444
  conductorDbPath: () => conductorDbPath,
@@ -6207,9 +6543,11 @@ __export(index_exports, {
6207
6543
  isGlobalThread: () => isGlobalThread,
6208
6544
  isImageFilePath: () => isImageFilePath,
6209
6545
  isLinearConnected: () => isLinearConnected,
6546
+ isOrchestratorCapableAgent: () => isOrchestratorCapableAgent,
6210
6547
  isOrchestratorThread: () => isOrchestratorThread,
6211
6548
  isPidAlive: () => isPidAlive,
6212
6549
  isPlaceholderBranch: () => isPlaceholderBranch,
6550
+ isSessionQuotaLimit: () => isSessionQuotaLimit,
6213
6551
  isThinkingEffort: () => isThinkingEffort,
6214
6552
  listAgentSetupInfo: () => listAgentSetupInfo,
6215
6553
  listBranchCommits: () => listBranchCommits,
@@ -6225,6 +6563,7 @@ __export(index_exports, {
6225
6563
  listIssues: () => listIssues,
6226
6564
  listLinearIssues: () => listLinearIssues,
6227
6565
  listLinearIssuesDirect: () => listLinearIssuesDirect,
6566
+ listModelsForAgent: () => listModelsForAgent,
6228
6567
  listOpencodeModels: () => listOpencodeModels,
6229
6568
  listPrs: () => listPrs,
6230
6569
  listRunScripts: () => listRunScripts,
@@ -6256,6 +6595,8 @@ __export(index_exports, {
6256
6595
  normalizeWorktreePath: () => normalizeWorktreePath,
6257
6596
  openInSystemTerminal: () => openInSystemTerminal,
6258
6597
  opencodeAdapter: () => opencodeAdapter,
6598
+ orchestrationQuotaFallbackAgent: () => orchestrationQuotaFallbackAgent,
6599
+ orchestrationQuotaOnLimit: () => orchestrationQuotaOnLimit,
6259
6600
  orchestrationTitleNeedsSoccerNickname: () => orchestrationTitleNeedsSoccerNickname,
6260
6601
  orchestratorSessionPoisonedByBuiltins: () => orchestratorSessionPoisonedByBuiltins,
6261
6602
  originGhRepoEnv: () => originGhRepoEnv,
@@ -6263,6 +6604,7 @@ __export(index_exports, {
6263
6604
  parseForceStopMessage: () => parseForceStopMessage,
6264
6605
  parseGithubSlugFromRemoteUrl: () => parseGithubSlugFromRemoteUrl,
6265
6606
  parseMcpList: () => parseMcpList,
6607
+ parseSessionQuotaResetAt: () => parseSessionQuotaResetAt,
6266
6608
  partsToAssistantText: () => partsToAssistantText,
6267
6609
  pastedTextStats: () => pastedTextStats,
6268
6610
  permissionMode: () => permissionMode,
@@ -6289,6 +6631,7 @@ __export(index_exports, {
6289
6631
  resolveFilesToCopy: () => resolveFilesToCopy,
6290
6632
  resolveGithubRepoSlug: () => resolveGithubRepoSlug,
6291
6633
  resolvePrSelector: () => resolvePrSelector,
6634
+ resolveQuotaFallbackAgent: () => resolveQuotaFallbackAgent,
6292
6635
  resolveRepoRoot: () => resolveRepoRoot,
6293
6636
  resolveThreadDefaults: () => resolveThreadDefaults,
6294
6637
  resolveThreadEffort: () => resolveThreadEffort,
@@ -6553,6 +6896,7 @@ init_app_settings();
6553
6896
  init_global_workspace();
6554
6897
  init_brightsy();
6555
6898
  init_agents();
6899
+ init_orchestrator_capable();
6556
6900
 
6557
6901
  // src/agents/message-parts.ts
6558
6902
  function asRecord(input) {
@@ -6797,6 +7141,9 @@ async function spawnAgentTurn(thread, input, onEvent) {
6797
7141
  const { ensureGlobalCoordinatorCwd: ensureGlobalCoordinatorCwd2 } = await Promise.resolve().then(() => (init_coordinator_prompt(), coordinator_prompt_exports));
6798
7142
  ensureGlobalCoordinatorCwd2();
6799
7143
  }
7144
+ if (isOrchestratorThread(thread)) {
7145
+ assertOrchestratorCapableAgent(thread.agent);
7146
+ }
6800
7147
  const adapter = getAdapter(thread.agent);
6801
7148
  const cmd = await adapter.buildTurn(thread, input);
6802
7149
  if (cmd.cwd !== thread.worktreePath) {
@@ -9089,6 +9436,7 @@ var import_node_crypto4 = require("crypto");
9089
9436
  init_teams();
9090
9437
  init_worktree_labels();
9091
9438
  init_global_workspace();
9439
+ init_orchestrator_capable();
9092
9440
  init_thread_store();
9093
9441
  init_worktree_labels();
9094
9442
  function sameWorktreePath(a, b) {
@@ -9152,6 +9500,10 @@ function createChatTab(input) {
9152
9500
  const binding = worktreeBindingFrom(from);
9153
9501
  const explicitTitle = input.title?.trim();
9154
9502
  const title = explicitTitle || allocateTeamName(takenTeamSlugsForChatTab(binding.worktreePath)).name;
9503
+ const nextAgent = input.agent ?? from.agent;
9504
+ if (isOrchestratorThread(from) || binding.sourceType === "orchestration") {
9505
+ assertOrchestratorCapableAgent(nextAgent);
9506
+ }
9155
9507
  const thread = createEmptyThread({
9156
9508
  title,
9157
9509
  // Chat-tab nicknames (soccer team or explicit) must stick. Post-turn
@@ -9159,7 +9511,7 @@ function createChatTab(input) {
9159
9511
  // shared worktree folder name (e.g. fork "Arsenal" → "Monaco").
9160
9512
  userSetTitle: true,
9161
9513
  ...binding,
9162
- agent: input.agent ?? from.agent,
9514
+ agent: nextAgent,
9163
9515
  model: input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model,
9164
9516
  effort: input.effort !== void 0 ? input.effort : from.effort,
9165
9517
  fast: input.fast !== void 0 ? Boolean(input.fast) : from.fast,
@@ -9175,16 +9527,24 @@ function forkChatTab(input) {
9175
9527
  const from = requireThread(input.threadId);
9176
9528
  const slice = forkMessageSlice(from, input.throughIndex);
9177
9529
  const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
9178
- return createChatTab({
9530
+ const tab = createChatTab({
9179
9531
  fromThreadId: input.threadId,
9180
9532
  agent: input.agent ?? from.agent,
9533
+ model: input.model,
9181
9534
  title: input.title?.trim() || void 0,
9182
9535
  attachments: [attachment]
9183
9536
  });
9537
+ if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
9538
+ const next = { ...tab, parentThreadId: from.id };
9539
+ writeThread(next);
9540
+ return next;
9541
+ }
9542
+ return tab;
9184
9543
  }
9185
9544
 
9186
9545
  // src/threads/fork-worktree.ts
9187
9546
  init_thread_store();
9547
+ init_global_workspace();
9188
9548
  function requireThread2(idOrRef) {
9189
9549
  const thread = findThreadByRef(idOrRef) ?? null;
9190
9550
  if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
@@ -9192,16 +9552,28 @@ function requireThread2(idOrRef) {
9192
9552
  }
9193
9553
  async function forkThreadWorktree(input, onSetupLine) {
9194
9554
  const from = requireThread2(input.threadId);
9555
+ if (isOrchestratorThread(from)) {
9556
+ throw new Error(
9557
+ "fork_worktree targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
9558
+ );
9559
+ }
9560
+ if (!from.branchName?.trim() || !from.repoPath?.trim()) {
9561
+ throw new Error(
9562
+ `Cannot fork worktree: thread ${from.id} has no branch/repo (need a real worktree chat).`
9563
+ );
9564
+ }
9195
9565
  const slice = forkMessageSlice(from, input.throughIndex);
9196
9566
  const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
9567
+ const nextAgent = input.agent ?? from.agent;
9568
+ const nextModel = input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model;
9197
9569
  const thread = await createThread(
9198
9570
  {
9199
9571
  sourceType: "branch",
9200
9572
  sourceRef: from.branchName,
9201
9573
  repoPath: from.repoPath,
9202
- agent: input.agent ?? from.agent,
9574
+ agent: nextAgent,
9203
9575
  autonomy: from.autonomy,
9204
- model: from.model,
9576
+ model: nextModel,
9205
9577
  effort: from.effort,
9206
9578
  fast: from.fast,
9207
9579
  planMode: from.planMode,
@@ -9742,6 +10114,7 @@ async function cloneRepoIntoSideboard(opts) {
9742
10114
 
9743
10115
  // src/orchestrator/orchestrator.ts
9744
10116
  init_thread_store();
10117
+ init_orchestrator_capable();
9745
10118
 
9746
10119
  // src/review/request-review.ts
9747
10120
  var import_node_crypto5 = require("crypto");
@@ -9795,6 +10168,115 @@ async function requestReview(threadRef, send) {
9795
10168
  return { tab: started, from };
9796
10169
  }
9797
10170
 
10171
+ // src/orchestrator/quota-failover.ts
10172
+ var import_node_crypto6 = require("crypto");
10173
+ init_session_quota();
10174
+ init_app_settings();
10175
+ init_global_workspace();
10176
+ init_thread_store();
10177
+ function planOrchestrationQuotaFailover(thread, limitText, opts) {
10178
+ if (!isOrchestratorThread(thread)) return null;
10179
+ if (!isSessionQuotaLimit(limitText)) return null;
10180
+ const onLimit = opts?.onLimit ?? orchestrationQuotaOnLimit();
10181
+ const resumeAt = parseSessionQuotaResetAt(limitText, opts?.now);
10182
+ if (thread.quotaContinuedFromId) {
10183
+ if (resumeAt) {
10184
+ return {
10185
+ action: "wait_reset",
10186
+ reason: "Already continued once; waiting for quota reset instead.",
10187
+ limitText,
10188
+ resumeAt
10189
+ };
10190
+ }
10191
+ return {
10192
+ action: "none",
10193
+ reason: "Already continued once; no parseable reset time.",
10194
+ limitText
10195
+ };
10196
+ }
10197
+ if (onLimit === "wait_reset") {
10198
+ if (!resumeAt) {
10199
+ return {
10200
+ action: "none",
10201
+ reason: "wait_reset configured but reset time could not be parsed.",
10202
+ limitText
10203
+ };
10204
+ }
10205
+ return {
10206
+ action: "wait_reset",
10207
+ reason: "Settings: wait for quota reset.",
10208
+ limitText,
10209
+ resumeAt
10210
+ };
10211
+ }
10212
+ const preferred = opts?.fallbackAgent ?? orchestrationQuotaFallbackAgent();
10213
+ const fallbackAgent = resolveQuotaFallbackAgent(thread.agent, preferred);
10214
+ return {
10215
+ action: "switch_agent",
10216
+ reason: `Continue on ${fallbackAgent} (Auto) after ${thread.agent} session limit.`,
10217
+ limitText,
10218
+ fallbackAgent
10219
+ };
10220
+ }
10221
+ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
10222
+ const children = listThreads({ includeArchived: false }).filter((t) => t.parentThreadId === from.id && t.status !== "archived").slice(0, 40).map(
10223
+ (t) => `- ${t.title} \xB7 ${t.status} \xB7 ${t.agent} \xB7 sideboard://thread/${t.id}`
10224
+ );
10225
+ const recent = from.messages.slice(-8).map((m) => {
10226
+ const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
10227
+ const text = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
10228
+ return text ? `- ${role}: ${text}` : null;
10229
+ }).filter(Boolean);
10230
+ const body = [
10231
+ `# Orchestration handoff`,
10232
+ "",
10233
+ `Previous chat: ${from.title} (\`${from.id}\`) on **${from.agent}** hit a session/usage limit.`,
10234
+ `Limit: ${limitText.trim()}`,
10235
+ `Continuing on **${fallbackAgent}** with Auto model.`,
10236
+ "",
10237
+ `## Goal`,
10238
+ from.sourceRef?.trim() || "(none)",
10239
+ "",
10240
+ `## Child threads`,
10241
+ children.length ? children.join("\n") : "(none listed \u2014 call list_threads)",
10242
+ "",
10243
+ `## Recent turns (truncated)`,
10244
+ recent.length ? recent.join("\n") : "(none)",
10245
+ "",
10246
+ `## Instructions`,
10247
+ `- Continue fleet orchestration from this handoff.`,
10248
+ `- Prefer Sideboard MCP (list_threads, get_thread, send_to_thread, \u2026) for live status.`,
10249
+ `- Leave model Auto unless there is a specific reason to pin one.`,
10250
+ `- Do not wait on the limited ${from.agent} account; keep going on ${fallbackAgent}.`
10251
+ ].join("\n");
10252
+ return {
10253
+ id: (0, import_node_crypto6.randomUUID)(),
10254
+ name: "Orchestration quota handoff.md",
10255
+ kind: "transcript",
10256
+ content: body
10257
+ };
10258
+ }
10259
+ var QUOTA_CONTINUE_PROMPT = (fromAgent, fallback) => [
10260
+ `${fromAgent} hit a session/usage limit. Continue this orchestration on ${fallback} using the attached handoff.`,
10261
+ "Call list_threads for live fleet status, then proceed with the goal. Leave model Auto unless needed."
10262
+ ].join(" ");
10263
+ 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.";
10264
+ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
10265
+ const handoff = buildQuotaHandoffAttachment(from, limitText, fallbackAgent);
10266
+ const tab = createChatTab({
10267
+ fromThreadId: from.id,
10268
+ agent: fallbackAgent,
10269
+ model: null,
10270
+ attachments: [handoff]
10271
+ });
10272
+ return updateThread(tab.id, {
10273
+ parentThreadId: from.id,
10274
+ quotaContinuedFromId: from.id,
10275
+ sourceRef: from.sourceRef,
10276
+ sourceType: "orchestration"
10277
+ });
10278
+ }
10279
+
9798
10280
  // src/orchestrator/orchestrator.ts
9799
10281
  init_types();
9800
10282
  init_settings();
@@ -9865,6 +10347,8 @@ var Orchestrator = class {
9865
10347
  haltDrain = /* @__PURE__ */ new Set();
9866
10348
  /** WIP snapshot SHA at the start of the latest agent turn (per thread). */
9867
10349
  turnBaselines = /* @__PURE__ */ new Map();
10350
+ /** Timers for orchestration session-quota auto-resume. */
10351
+ quotaResumeTimers = /* @__PURE__ */ new Map();
9868
10352
  maxConcurrent;
9869
10353
  runningCount = 0;
9870
10354
  constructor(opts) {
@@ -9944,6 +10428,113 @@ var Orchestrator = class {
9944
10428
  void this.drainQueue(thread.id);
9945
10429
  }
9946
10430
  }
10431
+ this.schedulePendingQuotaResumes();
10432
+ }
10433
+ clearQuotaResumeTimer(threadId) {
10434
+ const timer = this.quotaResumeTimers.get(threadId);
10435
+ if (timer) clearTimeout(timer);
10436
+ this.quotaResumeTimers.delete(threadId);
10437
+ }
10438
+ /** Schedule (or fire) auto-retry after a provider session/usage limit reset. */
10439
+ scheduleQuotaResume(threadId, resumeAt) {
10440
+ this.clearQuotaResumeTimer(threadId);
10441
+ updateThread(threadId, { quotaResumeAt: resumeAt.toISOString() });
10442
+ const delay = Math.max(5e3, resumeAt.getTime() - Date.now());
10443
+ const capped = Math.min(delay, 2147483647);
10444
+ const timer = setTimeout(() => {
10445
+ this.quotaResumeTimers.delete(threadId);
10446
+ void this.resumeAfterQuotaWait(threadId);
10447
+ }, capped);
10448
+ this.quotaResumeTimers.set(threadId, timer);
10449
+ }
10450
+ schedulePendingQuotaResumes() {
10451
+ for (const thread of listThreads({ includeArchived: false })) {
10452
+ if (!thread.quotaResumeAt) continue;
10453
+ const at = new Date(thread.quotaResumeAt);
10454
+ if (Number.isNaN(at.getTime())) continue;
10455
+ if (at.getTime() <= Date.now()) {
10456
+ void this.resumeAfterQuotaWait(thread.id);
10457
+ } else if (!this.quotaResumeTimers.has(thread.id)) {
10458
+ this.scheduleQuotaResume(thread.id, at);
10459
+ }
10460
+ }
10461
+ }
10462
+ async resumeAfterQuotaWait(threadId) {
10463
+ const thread = findThreadByRef(threadId);
10464
+ if (!thread || thread.status === "archived") return;
10465
+ this.clearQuotaResumeTimer(threadId);
10466
+ try {
10467
+ updateThread(threadId, { quotaResumeAt: null });
10468
+ } catch {
10469
+ return;
10470
+ }
10471
+ if (thread.status === "running" || this.activeTurns.has(threadId) || this.startingTurns.has(threadId)) {
10472
+ return;
10473
+ }
10474
+ await this.send(threadId, QUOTA_RESUME_PROMPT);
10475
+ }
10476
+ /**
10477
+ * Host-side continue when an orchestration chat hits a provider session/usage
10478
+ * limit (not context size): switch agent (Auto) or wait until reset.
10479
+ */
10480
+ async maybeHandleOrchestrationQuotaFailover(threadId, limitText) {
10481
+ const thread = findThreadByRef(threadId);
10482
+ if (!thread) return;
10483
+ const plan = planOrchestrationQuotaFailover(thread, limitText);
10484
+ if (!plan || plan.action === "none") return;
10485
+ if (plan.action === "wait_reset" && plan.resumeAt) {
10486
+ this.haltDrain.add(threadId);
10487
+ this.scheduleQuotaResume(threadId, plan.resumeAt);
10488
+ setStatus(threadId, "idle", null);
10489
+ appendMessage(threadId, {
10490
+ role: "agent",
10491
+ text: `Sideboard will auto-retry this orchestration around ${plan.resumeAt.toLocaleString()} when the session limit resets.`,
10492
+ ts: (/* @__PURE__ */ new Date()).toISOString()
10493
+ });
10494
+ this.emit({
10495
+ type: "quota_failover",
10496
+ threadId,
10497
+ action: "wait_reset",
10498
+ message: plan.reason,
10499
+ resumeAt: plan.resumeAt.toISOString()
10500
+ });
10501
+ this.emit({ type: "status_changed", threadId, status: "idle" });
10502
+ return;
10503
+ }
10504
+ if (plan.action === "switch_agent" && plan.fallbackAgent) {
10505
+ this.haltDrain.add(threadId);
10506
+ const next = createQuotaFailoverChat(
10507
+ thread,
10508
+ plan.fallbackAgent,
10509
+ plan.limitText
10510
+ );
10511
+ this.clearQuotaResumeTimer(threadId);
10512
+ try {
10513
+ updateThread(threadId, { quotaResumeAt: null });
10514
+ } catch {
10515
+ }
10516
+ appendMessage(threadId, {
10517
+ role: "agent",
10518
+ text: `Session limit on ${thread.agent}. Sideboard continued on ${plan.fallbackAgent} (Auto) in [${next.title}](sideboard://thread/${next.id}).`,
10519
+ ts: (/* @__PURE__ */ new Date()).toISOString()
10520
+ });
10521
+ this.emit({
10522
+ type: "quota_failover",
10523
+ threadId,
10524
+ action: "switch_agent",
10525
+ toThreadId: next.id,
10526
+ message: plan.reason
10527
+ });
10528
+ this.emit({
10529
+ type: "status_changed",
10530
+ threadId: next.id,
10531
+ status: next.status
10532
+ });
10533
+ await this.send(
10534
+ next.id,
10535
+ QUOTA_CONTINUE_PROMPT(thread.agent, plan.fallbackAgent)
10536
+ );
10537
+ }
9947
10538
  }
9948
10539
  getThreads(includeArchived = false) {
9949
10540
  return listThreads({ includeArchived });
@@ -10302,11 +10893,16 @@ var Orchestrator = class {
10302
10893
  }
10303
10894
  }
10304
10895
  }
10305
- const failureOnlyMessage = exitCode !== 0 && looksLikeAgentFailureMessage(assistantText) && !parts.some((p) => p.type === "tool" || p.type === "thinking");
10306
- if (!failureOnlyMessage && (assistantText || parts.length > 0)) {
10896
+ const lastStderr = summarizeTurnStderr(stderrTail);
10897
+ const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
10898
+ let chatText = assistantText;
10899
+ if (exitCode !== 0 && !chatText && looksLikeAgentFailureMessage(detail)) {
10900
+ chatText = humanizeAgentFailDetail(detail);
10901
+ }
10902
+ if (chatText || parts.length > 0) {
10307
10903
  appendMessage(threadId, {
10308
10904
  role: "agent",
10309
- text: assistantText,
10905
+ text: chatText,
10310
10906
  parts: parts.length > 0 ? parts : void 0,
10311
10907
  durationMs: Math.max(0, Date.now() - turnStartedAt),
10312
10908
  usage,
@@ -10325,13 +10921,12 @@ var Orchestrator = class {
10325
10921
  this.emit({ type: "status_changed", threadId, status: "stopped" });
10326
10922
  this.emit({ type: "turn_finished", threadId, exitCode });
10327
10923
  } else {
10328
- const lastStderr = summarizeTurnStderr(stderrTail);
10329
- const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
10330
10924
  const failDetail = formatTurnExitError(exitCode, detail);
10925
+ const explainedInChat = exitCode !== 0 && Boolean(chatText) && (looksLikeAgentFailureMessage(chatText) || failDetail && chatText.includes(failDetail.replace(/^exit\s*\d+:\s*/i, "").trim()));
10331
10926
  setStatus(
10332
10927
  threadId,
10333
10928
  exitCode === 0 ? "idle" : "error",
10334
- exitCode === 0 ? null : failDetail
10929
+ exitCode === 0 || explainedInChat ? null : failDetail
10335
10930
  );
10336
10931
  this.emit({
10337
10932
  type: "status_changed",
@@ -10339,6 +10934,10 @@ var Orchestrator = class {
10339
10934
  status: exitCode === 0 ? "idle" : "error"
10340
10935
  });
10341
10936
  this.emit({ type: "turn_finished", threadId, exitCode });
10937
+ if (exitCode !== 0) {
10938
+ const blob = [chatText, detail].filter(Boolean).join("\n");
10939
+ void this.maybeHandleOrchestrationQuotaFailover(threadId, blob);
10940
+ }
10342
10941
  }
10343
10942
  } catch (err) {
10344
10943
  const message = err instanceof Error ? err.message : String(err);
@@ -10352,6 +10951,7 @@ var Orchestrator = class {
10352
10951
  this.emit({ type: "error", threadId, message });
10353
10952
  this.emit({ type: "status_changed", threadId, status: "error" });
10354
10953
  this.emit({ type: "turn_finished", threadId, exitCode: 1 });
10954
+ void this.maybeHandleOrchestrationQuotaFailover(threadId, message);
10355
10955
  }
10356
10956
  } finally {
10357
10957
  this.startingTurns.delete(threadId);
@@ -10836,6 +11436,9 @@ var Orchestrator = class {
10836
11436
  `Cannot switch agent provider mid-chat (${thread.agent} \u2192 ${patch.agent}). Start a new chat tab instead.`
10837
11437
  );
10838
11438
  }
11439
+ if (isOrchestratorThread(thread)) {
11440
+ assertOrchestratorCapableAgent(patch.agent);
11441
+ }
10839
11442
  next.agent = patch.agent;
10840
11443
  if (patch.agent !== "claude" && patch.model === void 0) next.model = null;
10841
11444
  next.sessionId = null;
@@ -11062,6 +11665,7 @@ var import_zod = require("zod");
11062
11665
  var import_node_path25 = require("path");
11063
11666
  init_worktree();
11064
11667
  init_global_workspace();
11668
+ init_list_models();
11065
11669
 
11066
11670
  // src/mcp/archive-guard.ts
11067
11671
  init_global_workspace();
@@ -11478,6 +12082,120 @@ async function startMcpServer() {
11478
12082
  }
11479
12083
  }
11480
12084
  );
12085
+ const agentEnum = import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
12086
+ server.tool(
12087
+ "list_models",
12088
+ "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.",
12089
+ {
12090
+ agent: agentEnum.optional().describe("Limit to one agent; omit for all")
12091
+ },
12092
+ async ({ agent }) => {
12093
+ try {
12094
+ const catalogs = await listModelsForAgent(agent);
12095
+ return {
12096
+ content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
12097
+ };
12098
+ } catch (err) {
12099
+ const message = err instanceof Error ? err.message : String(err);
12100
+ return { content: [{ type: "text", text: message }], isError: true };
12101
+ }
12102
+ }
12103
+ );
12104
+ server.tool(
12105
+ "fork_worktree",
12106
+ "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.",
12107
+ {
12108
+ ref: import_zod.z.string().describe("Worktree thread id/ref to fork"),
12109
+ through_index: import_zod.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
12110
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
12111
+ model: import_zod.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
12112
+ title: import_zod.z.string().optional()
12113
+ },
12114
+ async ({ ref, through_index, agent, model, title }) => {
12115
+ try {
12116
+ const source = orch.getThread(ref);
12117
+ if (source) await orch.reconcile(source.repoPath);
12118
+ const thread = await orch.forkThreadWorktree({
12119
+ threadId: ref,
12120
+ throughIndex: through_index,
12121
+ agent,
12122
+ model,
12123
+ title
12124
+ });
12125
+ return {
12126
+ content: [
12127
+ {
12128
+ type: "text",
12129
+ text: JSON.stringify({
12130
+ id: thread.id,
12131
+ title: thread.title,
12132
+ status: thread.status,
12133
+ agent: thread.agent,
12134
+ model: thread.model,
12135
+ branchName: thread.branchName,
12136
+ worktreePath: thread.worktreePath,
12137
+ fromThreadId: source?.id ?? ref,
12138
+ link: `sideboard://thread/${thread.id}`
12139
+ })
12140
+ }
12141
+ ]
12142
+ };
12143
+ } catch (err) {
12144
+ const message = err instanceof Error ? err.message : String(err);
12145
+ return { content: [{ type: "text", text: message }], isError: true };
12146
+ }
12147
+ }
12148
+ );
12149
+ server.tool(
12150
+ "fork_chat",
12151
+ "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.",
12152
+ {
12153
+ ref: import_zod.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
12154
+ through_index: import_zod.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
12155
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
12156
+ model: import_zod.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
12157
+ title: import_zod.z.string().optional()
12158
+ },
12159
+ async ({ ref, through_index, agent, model, title }) => {
12160
+ try {
12161
+ const source = orch.getThread(ref);
12162
+ if (!source) {
12163
+ return {
12164
+ content: [{ type: "text", text: `Thread not found: ${ref}` }],
12165
+ isError: true
12166
+ };
12167
+ }
12168
+ const tab = orch.forkChatTab({
12169
+ threadId: source.id,
12170
+ throughIndex: through_index,
12171
+ agent,
12172
+ model,
12173
+ title
12174
+ });
12175
+ return {
12176
+ content: [
12177
+ {
12178
+ type: "text",
12179
+ text: JSON.stringify({
12180
+ id: tab.id,
12181
+ title: tab.title,
12182
+ status: tab.status,
12183
+ agent: tab.agent,
12184
+ model: tab.model,
12185
+ sourceType: tab.sourceType,
12186
+ worktreePath: tab.worktreePath,
12187
+ fromThreadId: source.id,
12188
+ link: `sideboard://thread/${tab.id}`
12189
+ })
12190
+ }
12191
+ ]
12192
+ };
12193
+ } catch (err) {
12194
+ const message = err instanceof Error ? err.message : String(err);
12195
+ return { content: [{ type: "text", text: message }], isError: true };
12196
+ }
12197
+ }
12198
+ );
11481
12199
  server.tool(
11482
12200
  "run_dev_script",
11483
12201
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
@@ -12004,6 +12722,7 @@ init_injected_mcp();
12004
12722
  0 && (module.exports = {
12005
12723
  BRIGHTSY_MCP_ALLOWED_TOOLS,
12006
12724
  BrightsySideboardApi,
12725
+ CLAUDE_MODEL_CATALOG,
12007
12726
  CLOUD_COORDINATOR_BUSY_REPLY,
12008
12727
  CLOUD_COORDINATOR_STOPPED_REPLY,
12009
12728
  CLOUD_COORDINATOR_TIMEOUT_REPLY,
@@ -12017,6 +12736,7 @@ init_injected_mcp();
12017
12736
  GLOBAL_WORKSPACE_ID,
12018
12737
  HARNESS_ENV_KEYS,
12019
12738
  MAX_ANTHROPIC_CACHE_CONTROL_BLOCKS,
12739
+ ORCHESTRATOR_AGENT_KINDS,
12020
12740
  Orchestrator,
12021
12741
  PASTE_ATTACH_MIN_CHARS,
12022
12742
  PASTE_ATTACH_MIN_LINES,
@@ -12041,6 +12761,7 @@ init_injected_mcp();
12041
12761
  applyAppEnvironment,
12042
12762
  applyCompaction,
12043
12763
  applyThreadIntoMain,
12764
+ assertOrchestratorCapableAgent,
12044
12765
  attachmentFromAbsolutePath,
12045
12766
  attachmentsFromBuffers,
12046
12767
  attachmentsFromWorktreePaths,
@@ -12073,6 +12794,7 @@ init_injected_mcp();
12073
12794
  cleanupOrphanWorktrees,
12074
12795
  cloneRepoIntoSideboard,
12075
12796
  codexAdapter,
12797
+ coerceOrchestratorAgent,
12076
12798
  collectTakenTeamSlugs,
12077
12799
  commitAll,
12078
12800
  conductorDbPath,
@@ -12177,9 +12899,11 @@ init_injected_mcp();
12177
12899
  isGlobalThread,
12178
12900
  isImageFilePath,
12179
12901
  isLinearConnected,
12902
+ isOrchestratorCapableAgent,
12180
12903
  isOrchestratorThread,
12181
12904
  isPidAlive,
12182
12905
  isPlaceholderBranch,
12906
+ isSessionQuotaLimit,
12183
12907
  isThinkingEffort,
12184
12908
  listAgentSetupInfo,
12185
12909
  listBranchCommits,
@@ -12195,6 +12919,7 @@ init_injected_mcp();
12195
12919
  listIssues,
12196
12920
  listLinearIssues,
12197
12921
  listLinearIssuesDirect,
12922
+ listModelsForAgent,
12198
12923
  listOpencodeModels,
12199
12924
  listPrs,
12200
12925
  listRunScripts,
@@ -12226,6 +12951,8 @@ init_injected_mcp();
12226
12951
  normalizeWorktreePath,
12227
12952
  openInSystemTerminal,
12228
12953
  opencodeAdapter,
12954
+ orchestrationQuotaFallbackAgent,
12955
+ orchestrationQuotaOnLimit,
12229
12956
  orchestrationTitleNeedsSoccerNickname,
12230
12957
  orchestratorSessionPoisonedByBuiltins,
12231
12958
  originGhRepoEnv,
@@ -12233,6 +12960,7 @@ init_injected_mcp();
12233
12960
  parseForceStopMessage,
12234
12961
  parseGithubSlugFromRemoteUrl,
12235
12962
  parseMcpList,
12963
+ parseSessionQuotaResetAt,
12236
12964
  partsToAssistantText,
12237
12965
  pastedTextStats,
12238
12966
  permissionMode,
@@ -12259,6 +12987,7 @@ init_injected_mcp();
12259
12987
  resolveFilesToCopy,
12260
12988
  resolveGithubRepoSlug,
12261
12989
  resolvePrSelector,
12990
+ resolveQuotaFallbackAgent,
12262
12991
  resolveRepoRoot,
12263
12992
  resolveThreadDefaults,
12264
12993
  resolveThreadEffort,