@sideboard-ai/core 0.1.44 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)) {
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)) {
772
+ advanced.orchestrationQuotaFallbackAgent = patch.orchestrationQuotaFallbackAgent;
773
+ }
760
774
  return saveAppSettings({ ...current, advanced });
761
775
  }
762
776
  function autoRenameBranchEnabled(settings = loadAppSettings()) {
@@ -777,6 +791,13 @@ 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
+ return preferred && DEFAULT_AGENTS.has(preferred) ? preferred : "cursor";
800
+ }
780
801
  function maxConcurrentAgents(settings = loadAppSettings()) {
781
802
  const n = settings.advanced.maxConcurrent;
782
803
  if (typeof n === "number" && Number.isFinite(n)) {
@@ -893,7 +914,9 @@ function normalizeThread(raw) {
893
914
  attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
894
915
  prTitle: raw.prTitle ?? null,
895
916
  userSetTitle: Boolean(raw.userSetTitle),
896
- activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : []
917
+ activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
918
+ quotaResumeAt: raw.quotaResumeAt ?? null,
919
+ quotaContinuedFromId: raw.quotaContinuedFromId ?? null
897
920
  };
898
921
  }
899
922
  function createEmptyThread(partial) {
@@ -3184,11 +3207,14 @@ var init_coordinator_prompt = __esm({
3184
3207
  "Discover:",
3185
3208
  "- list_workspaces \u2014 registered repos (path + github slug when known)",
3186
3209
  "- list_branches / list_prs / list_issues \u2014 pass repoPath from list_workspaces (issues: Linear API or GitHub Issues)",
3210
+ "- list_models \u2014 only when you need a specific model (rare); otherwise leave model unset = Auto",
3187
3211
  "- list_threads / get_thread \u2014 fleet status (what is going on)",
3188
3212
  "Workspaces:",
3189
3213
  "- add_workspace / remove_workspace \u2014 register or unregister a git repo",
3190
3214
  "Worktree threads (chats):",
3191
3215
  "- create_thread \u2014 create a worktree + chat from branch | pr | ticket; pass repoPath + parentThreadId",
3216
+ "- 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.",
3217
+ "- 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
3218
  "- 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
3219
  "- wait_for_turn / get_turn_result \u2014 wait for and read the agent reply",
3194
3220
  "- stop_thread \u2014 force-stop: kill in-flight turn AND clear queued prompts (do not leave stale queue after an interrupt)",
@@ -3941,11 +3967,15 @@ function humanizeAgentFailDetail(detail) {
3941
3967
  }
3942
3968
  function formatTurnExitError(exitCode, stderrSummary) {
3943
3969
  const code = exitCode ?? 1;
3944
- const detail = humanizeAgentFailDetail(stderrSummary);
3970
+ const raw = stderrSummary.trim();
3971
+ if (/^exit\s*\d+$/i.test(raw)) {
3972
+ return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
3973
+ }
3974
+ const detail = humanizeAgentFailDetail(raw);
3945
3975
  if (!detail) {
3946
3976
  return `exit ${code}: agent exited without details (credits, auth, rate limits, or a CLI error)`;
3947
3977
  }
3948
- if (looksLikeAgentFailureMessage(stderrSummary)) return detail;
3978
+ if (looksLikeAgentFailureMessage(raw)) return detail;
3949
3979
  return `exit ${code}: ${detail}`;
3950
3980
  }
3951
3981
  var NODE_VERSION_FOOTER;
@@ -5709,6 +5739,199 @@ var init_opencode = __esm({
5709
5739
  }
5710
5740
  });
5711
5741
 
5742
+ // src/agents/list-models.ts
5743
+ async function listBrightsyModels() {
5744
+ try {
5745
+ const targets = await listBrightsyChatTargets();
5746
+ const accountId = targets.activeAccountId;
5747
+ const models = (targets.models ?? []).map((m) => ({
5748
+ id: encodeBrightsyTarget("model", m.id, accountId),
5749
+ displayName: m.name || m.id,
5750
+ description: m.description ?? void 0
5751
+ }));
5752
+ const agents = (targets.agents ?? []).map((a) => ({
5753
+ id: encodeBrightsyTarget("agent", a.id, accountId),
5754
+ displayName: a.name || a.id,
5755
+ description: a.description ?? "Brightsy agent target"
5756
+ }));
5757
+ return [...models, ...agents].slice(0, 80);
5758
+ } catch {
5759
+ return [];
5760
+ }
5761
+ }
5762
+ async function listModelsForAgent(agent) {
5763
+ const kinds = agent ? [agent] : ["claude", "codex", "opencode", "cursor", "brightsy"];
5764
+ const out = [];
5765
+ for (const kind of kinds) {
5766
+ if (kind === "claude") {
5767
+ out.push({
5768
+ agent: kind,
5769
+ auto: true,
5770
+ models: CLAUDE_MODEL_CATALOG,
5771
+ note: "Default Auto \u2014 only pass a model id when you have a reason."
5772
+ });
5773
+ continue;
5774
+ }
5775
+ if (kind === "codex") {
5776
+ out.push({
5777
+ agent: kind,
5778
+ auto: true,
5779
+ models: await listCodexModels(),
5780
+ note: "Default Auto \u2014 only pass a model slug when you have a reason."
5781
+ });
5782
+ continue;
5783
+ }
5784
+ if (kind === "opencode") {
5785
+ out.push({
5786
+ agent: kind,
5787
+ auto: true,
5788
+ models: await listOpencodeModels(),
5789
+ note: "Default Auto \u2014 only pass a provider/model id when you have a reason."
5790
+ });
5791
+ continue;
5792
+ }
5793
+ if (kind === "cursor") {
5794
+ out.push({
5795
+ agent: kind,
5796
+ auto: true,
5797
+ models: await listCursorModels(),
5798
+ note: 'Default Auto \u2014 only pass a model id when you have a reason (or use "default").'
5799
+ });
5800
+ continue;
5801
+ }
5802
+ if (kind === "brightsy") {
5803
+ const models = await listBrightsyModels();
5804
+ out.push({
5805
+ agent: kind,
5806
+ auto: true,
5807
+ models,
5808
+ 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."
5809
+ });
5810
+ }
5811
+ }
5812
+ return out;
5813
+ }
5814
+ var CLAUDE_MODEL_CATALOG;
5815
+ var init_list_models = __esm({
5816
+ "src/agents/list-models.ts"() {
5817
+ "use strict";
5818
+ init_brightsy();
5819
+ init_brightsy_targets();
5820
+ init_codex();
5821
+ init_cursor();
5822
+ init_opencode();
5823
+ CLAUDE_MODEL_CATALOG = [
5824
+ { id: "fable", displayName: "Fable" },
5825
+ { id: "opus", displayName: "Opus" },
5826
+ { id: "sonnet", displayName: "Sonnet" },
5827
+ { id: "haiku", displayName: "Haiku" }
5828
+ ];
5829
+ }
5830
+ });
5831
+
5832
+ // src/agents/session-quota.ts
5833
+ function isSessionQuotaLimit(text) {
5834
+ const lower = text.trim().toLowerCase();
5835
+ if (!lower) return false;
5836
+ if (/credit balance is too low|out of credits|insufficient.?quota|billing/.test(lower)) {
5837
+ return false;
5838
+ }
5839
+ if (/prompt is too long|context.*(too long|exceed)|conversation too long/.test(lower)) {
5840
+ return false;
5841
+ }
5842
+ 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);
5843
+ }
5844
+ function parseSessionQuotaResetAt(text, now = /* @__PURE__ */ new Date()) {
5845
+ const absolute = text.match(
5846
+ /resets\s+(?:at\s+)?(\d{1,2}):(\d{2})\s*(am|pm)(?:\s*\(([^)]+)\))?/i
5847
+ );
5848
+ if (absolute) {
5849
+ const hour12 = Number(absolute[1]);
5850
+ const minute = Number(absolute[2]);
5851
+ const ampm = absolute[3].toLowerCase();
5852
+ const timeZone = absolute[4]?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone;
5853
+ let hour = hour12 % 12;
5854
+ if (ampm === "pm") hour += 12;
5855
+ const at = zonedWallTimeToUtc(now, hour, minute, timeZone);
5856
+ if (!at) return null;
5857
+ if (at.getTime() <= now.getTime() + 3e4) {
5858
+ const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1e3);
5859
+ return zonedWallTimeToUtc(tomorrow, hour, minute, timeZone);
5860
+ }
5861
+ return at;
5862
+ }
5863
+ const relative = text.match(
5864
+ /resets\s+in\s+(\d+)\s*(minutes?|hours?|days?)/i
5865
+ );
5866
+ if (relative) {
5867
+ const n = Number(relative[1]);
5868
+ const unit = relative[2].toLowerCase();
5869
+ const ms = unit.startsWith("day") ? n * 24 * 60 * 60 * 1e3 : unit.startsWith("hour") ? n * 60 * 60 * 1e3 : n * 60 * 1e3;
5870
+ return new Date(now.getTime() + ms);
5871
+ }
5872
+ return null;
5873
+ }
5874
+ function zonedWallTimeToUtc(day, hour, minute, timeZone) {
5875
+ try {
5876
+ const cal = new Intl.DateTimeFormat("en-US", {
5877
+ timeZone,
5878
+ year: "numeric",
5879
+ month: "2-digit",
5880
+ day: "2-digit"
5881
+ });
5882
+ const parts = Object.fromEntries(
5883
+ cal.formatToParts(day).filter((p) => p.type !== "literal").map((p) => [p.type, p.value])
5884
+ );
5885
+ const year = Number(parts.year);
5886
+ const month = Number(parts.month);
5887
+ const date = Number(parts.day);
5888
+ if (![year, month, date].every((n) => Number.isFinite(n))) return null;
5889
+ const utcGuess = Date.UTC(year, month - 1, date, hour, minute, 0);
5890
+ const dtf = new Intl.DateTimeFormat("en-US", {
5891
+ timeZone,
5892
+ year: "numeric",
5893
+ month: "2-digit",
5894
+ day: "2-digit",
5895
+ hour: "2-digit",
5896
+ minute: "2-digit",
5897
+ second: "2-digit",
5898
+ hourCycle: "h23"
5899
+ });
5900
+ const asParts = Object.fromEntries(
5901
+ dtf.formatToParts(new Date(utcGuess)).filter((p) => p.type !== "literal").map((p) => [p.type, p.value])
5902
+ );
5903
+ const asUtc = Date.UTC(
5904
+ Number(asParts.year),
5905
+ Number(asParts.month) - 1,
5906
+ Number(asParts.day),
5907
+ Number(asParts.hour),
5908
+ Number(asParts.minute),
5909
+ Number(asParts.second || "0")
5910
+ );
5911
+ const offset = asUtc - utcGuess;
5912
+ return new Date(utcGuess - offset);
5913
+ } catch {
5914
+ return null;
5915
+ }
5916
+ }
5917
+ function resolveQuotaFallbackAgent(current, preferred) {
5918
+ const ordered = preferred ? [preferred, ...FALLBACK_ORDER.filter((a) => a !== preferred)] : FALLBACK_ORDER;
5919
+ return ordered.find((a) => a !== current) ?? (current === "cursor" ? "codex" : "cursor");
5920
+ }
5921
+ var FALLBACK_ORDER;
5922
+ var init_session_quota = __esm({
5923
+ "src/agents/session-quota.ts"() {
5924
+ "use strict";
5925
+ FALLBACK_ORDER = [
5926
+ "cursor",
5927
+ "codex",
5928
+ "opencode",
5929
+ "brightsy",
5930
+ "claude"
5931
+ ];
5932
+ }
5933
+ });
5934
+
5712
5935
  // src/agents/install.ts
5713
5936
  function getAgentSetupInfo(agent) {
5714
5937
  return SETUP[agent];
@@ -5889,6 +6112,7 @@ var init_install = __esm({
5889
6112
  // src/agents/index.ts
5890
6113
  var agents_exports = {};
5891
6114
  __export(agents_exports, {
6115
+ CLAUDE_MODEL_CATALOG: () => CLAUDE_MODEL_CATALOG,
5892
6116
  PLAN_MODE_INSTRUCTION: () => PLAN_MODE_INSTRUCTION,
5893
6117
  allAdapters: () => allAdapters,
5894
6118
  brightsyAdapter: () => brightsyAdapter,
@@ -5903,17 +6127,21 @@ __export(agents_exports, {
5903
6127
  getAgentSetupInfo: () => getAgentSetupInfo,
5904
6128
  installAgent: () => installAgent,
5905
6129
  isCursorAutoModel: () => isCursorAutoModel,
6130
+ isSessionQuotaLimit: () => isSessionQuotaLimit,
5906
6131
  listAgentSetupInfo: () => listAgentSetupInfo,
5907
6132
  listBrightsyChatTargets: () => listBrightsyChatTargets,
5908
6133
  listCodexModels: () => listCodexModels,
5909
6134
  listCursorModels: () => listCursorModels,
6135
+ listModelsForAgent: () => listModelsForAgent,
5910
6136
  listOpencodeModels: () => listOpencodeModels,
5911
6137
  loginAgent: () => loginAgent,
5912
6138
  openInSystemTerminal: () => openInSystemTerminal,
5913
6139
  opencodeAdapter: () => opencodeAdapter,
5914
6140
  parseCursorRunnerLine: () => parseCursorRunnerLine,
6141
+ parseSessionQuotaResetAt: () => parseSessionQuotaResetAt,
5915
6142
  permissionMode: () => permissionMode,
5916
- resolveCursorModelId: () => resolveCursorModelId
6143
+ resolveCursorModelId: () => resolveCursorModelId,
6144
+ resolveQuotaFallbackAgent: () => resolveQuotaFallbackAgent
5917
6145
  });
5918
6146
  function getAdapter(kind) {
5919
6147
  return adapters[kind];
@@ -5939,6 +6167,8 @@ var init_agents = __esm({
5939
6167
  init_cursor_events();
5940
6168
  init_cursor();
5941
6169
  init_opencode();
6170
+ init_list_models();
6171
+ init_session_quota();
5942
6172
  init_path();
5943
6173
  init_install();
5944
6174
  adapters = {
@@ -6034,6 +6264,7 @@ var index_exports = {};
6034
6264
  __export(index_exports, {
6035
6265
  BRIGHTSY_MCP_ALLOWED_TOOLS: () => BRIGHTSY_MCP_ALLOWED_TOOLS,
6036
6266
  BrightsySideboardApi: () => BrightsySideboardApi,
6267
+ CLAUDE_MODEL_CATALOG: () => CLAUDE_MODEL_CATALOG,
6037
6268
  CLOUD_COORDINATOR_BUSY_REPLY: () => CLOUD_COORDINATOR_BUSY_REPLY,
6038
6269
  CLOUD_COORDINATOR_STOPPED_REPLY: () => CLOUD_COORDINATOR_STOPPED_REPLY,
6039
6270
  CLOUD_COORDINATOR_TIMEOUT_REPLY: () => CLOUD_COORDINATOR_TIMEOUT_REPLY,
@@ -6210,6 +6441,7 @@ __export(index_exports, {
6210
6441
  isOrchestratorThread: () => isOrchestratorThread,
6211
6442
  isPidAlive: () => isPidAlive,
6212
6443
  isPlaceholderBranch: () => isPlaceholderBranch,
6444
+ isSessionQuotaLimit: () => isSessionQuotaLimit,
6213
6445
  isThinkingEffort: () => isThinkingEffort,
6214
6446
  listAgentSetupInfo: () => listAgentSetupInfo,
6215
6447
  listBranchCommits: () => listBranchCommits,
@@ -6225,6 +6457,7 @@ __export(index_exports, {
6225
6457
  listIssues: () => listIssues,
6226
6458
  listLinearIssues: () => listLinearIssues,
6227
6459
  listLinearIssuesDirect: () => listLinearIssuesDirect,
6460
+ listModelsForAgent: () => listModelsForAgent,
6228
6461
  listOpencodeModels: () => listOpencodeModels,
6229
6462
  listPrs: () => listPrs,
6230
6463
  listRunScripts: () => listRunScripts,
@@ -6256,6 +6489,8 @@ __export(index_exports, {
6256
6489
  normalizeWorktreePath: () => normalizeWorktreePath,
6257
6490
  openInSystemTerminal: () => openInSystemTerminal,
6258
6491
  opencodeAdapter: () => opencodeAdapter,
6492
+ orchestrationQuotaFallbackAgent: () => orchestrationQuotaFallbackAgent,
6493
+ orchestrationQuotaOnLimit: () => orchestrationQuotaOnLimit,
6259
6494
  orchestrationTitleNeedsSoccerNickname: () => orchestrationTitleNeedsSoccerNickname,
6260
6495
  orchestratorSessionPoisonedByBuiltins: () => orchestratorSessionPoisonedByBuiltins,
6261
6496
  originGhRepoEnv: () => originGhRepoEnv,
@@ -6263,6 +6498,7 @@ __export(index_exports, {
6263
6498
  parseForceStopMessage: () => parseForceStopMessage,
6264
6499
  parseGithubSlugFromRemoteUrl: () => parseGithubSlugFromRemoteUrl,
6265
6500
  parseMcpList: () => parseMcpList,
6501
+ parseSessionQuotaResetAt: () => parseSessionQuotaResetAt,
6266
6502
  partsToAssistantText: () => partsToAssistantText,
6267
6503
  pastedTextStats: () => pastedTextStats,
6268
6504
  permissionMode: () => permissionMode,
@@ -6289,6 +6525,7 @@ __export(index_exports, {
6289
6525
  resolveFilesToCopy: () => resolveFilesToCopy,
6290
6526
  resolveGithubRepoSlug: () => resolveGithubRepoSlug,
6291
6527
  resolvePrSelector: () => resolvePrSelector,
6528
+ resolveQuotaFallbackAgent: () => resolveQuotaFallbackAgent,
6292
6529
  resolveRepoRoot: () => resolveRepoRoot,
6293
6530
  resolveThreadDefaults: () => resolveThreadDefaults,
6294
6531
  resolveThreadEffort: () => resolveThreadEffort,
@@ -9175,16 +9412,24 @@ function forkChatTab(input) {
9175
9412
  const from = requireThread(input.threadId);
9176
9413
  const slice = forkMessageSlice(from, input.throughIndex);
9177
9414
  const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
9178
- return createChatTab({
9415
+ const tab = createChatTab({
9179
9416
  fromThreadId: input.threadId,
9180
9417
  agent: input.agent ?? from.agent,
9418
+ model: input.model,
9181
9419
  title: input.title?.trim() || void 0,
9182
9420
  attachments: [attachment]
9183
9421
  });
9422
+ if (isOrchestratorThread(from) && tab.parentThreadId !== from.id) {
9423
+ const next = { ...tab, parentThreadId: from.id };
9424
+ writeThread(next);
9425
+ return next;
9426
+ }
9427
+ return tab;
9184
9428
  }
9185
9429
 
9186
9430
  // src/threads/fork-worktree.ts
9187
9431
  init_thread_store();
9432
+ init_global_workspace();
9188
9433
  function requireThread2(idOrRef) {
9189
9434
  const thread = findThreadByRef(idOrRef) ?? null;
9190
9435
  if (!thread) throw new Error(`Thread not found: ${idOrRef}`);
@@ -9192,16 +9437,28 @@ function requireThread2(idOrRef) {
9192
9437
  }
9193
9438
  async function forkThreadWorktree(input, onSetupLine) {
9194
9439
  const from = requireThread2(input.threadId);
9440
+ if (isOrchestratorThread(from)) {
9441
+ throw new Error(
9442
+ "fork_worktree targets a worktree agent thread (not the orchestrator). Pass a child/worktree thread ref."
9443
+ );
9444
+ }
9445
+ if (!from.branchName?.trim() || !from.repoPath?.trim()) {
9446
+ throw new Error(
9447
+ `Cannot fork worktree: thread ${from.id} has no branch/repo (need a real worktree chat).`
9448
+ );
9449
+ }
9195
9450
  const slice = forkMessageSlice(from, input.throughIndex);
9196
9451
  const attachment = buildForkTranscriptAttachment(from.title || "Chat", slice);
9452
+ const nextAgent = input.agent ?? from.agent;
9453
+ const nextModel = input.model !== void 0 ? input.model : input.agent && input.agent !== from.agent ? null : from.model;
9197
9454
  const thread = await createThread(
9198
9455
  {
9199
9456
  sourceType: "branch",
9200
9457
  sourceRef: from.branchName,
9201
9458
  repoPath: from.repoPath,
9202
- agent: input.agent ?? from.agent,
9459
+ agent: nextAgent,
9203
9460
  autonomy: from.autonomy,
9204
- model: from.model,
9461
+ model: nextModel,
9205
9462
  effort: from.effort,
9206
9463
  fast: from.fast,
9207
9464
  planMode: from.planMode,
@@ -9795,6 +10052,115 @@ async function requestReview(threadRef, send) {
9795
10052
  return { tab: started, from };
9796
10053
  }
9797
10054
 
10055
+ // src/orchestrator/quota-failover.ts
10056
+ var import_node_crypto6 = require("crypto");
10057
+ init_session_quota();
10058
+ init_app_settings();
10059
+ init_global_workspace();
10060
+ init_thread_store();
10061
+ function planOrchestrationQuotaFailover(thread, limitText, opts) {
10062
+ if (!isOrchestratorThread(thread)) return null;
10063
+ if (!isSessionQuotaLimit(limitText)) return null;
10064
+ const onLimit = opts?.onLimit ?? orchestrationQuotaOnLimit();
10065
+ const resumeAt = parseSessionQuotaResetAt(limitText, opts?.now);
10066
+ if (thread.quotaContinuedFromId) {
10067
+ if (resumeAt) {
10068
+ return {
10069
+ action: "wait_reset",
10070
+ reason: "Already continued once; waiting for quota reset instead.",
10071
+ limitText,
10072
+ resumeAt
10073
+ };
10074
+ }
10075
+ return {
10076
+ action: "none",
10077
+ reason: "Already continued once; no parseable reset time.",
10078
+ limitText
10079
+ };
10080
+ }
10081
+ if (onLimit === "wait_reset") {
10082
+ if (!resumeAt) {
10083
+ return {
10084
+ action: "none",
10085
+ reason: "wait_reset configured but reset time could not be parsed.",
10086
+ limitText
10087
+ };
10088
+ }
10089
+ return {
10090
+ action: "wait_reset",
10091
+ reason: "Settings: wait for quota reset.",
10092
+ limitText,
10093
+ resumeAt
10094
+ };
10095
+ }
10096
+ const preferred = opts?.fallbackAgent ?? orchestrationQuotaFallbackAgent();
10097
+ const fallbackAgent = resolveQuotaFallbackAgent(thread.agent, preferred);
10098
+ return {
10099
+ action: "switch_agent",
10100
+ reason: `Continue on ${fallbackAgent} (Auto) after ${thread.agent} session limit.`,
10101
+ limitText,
10102
+ fallbackAgent
10103
+ };
10104
+ }
10105
+ function buildQuotaHandoffAttachment(from, limitText, fallbackAgent) {
10106
+ const children = listThreads({ includeArchived: false }).filter((t) => t.parentThreadId === from.id && t.status !== "archived").slice(0, 40).map(
10107
+ (t) => `- ${t.title} \xB7 ${t.status} \xB7 ${t.agent} \xB7 sideboard://thread/${t.id}`
10108
+ );
10109
+ const recent = from.messages.slice(-8).map((m) => {
10110
+ const role = m.role === "user" ? "User" : m.role === "agent" ? "Agent" : "Summary";
10111
+ const text = m.text.trim().replace(/\s+/g, " ").slice(0, 280);
10112
+ return text ? `- ${role}: ${text}` : null;
10113
+ }).filter(Boolean);
10114
+ const body = [
10115
+ `# Orchestration handoff`,
10116
+ "",
10117
+ `Previous chat: ${from.title} (\`${from.id}\`) on **${from.agent}** hit a session/usage limit.`,
10118
+ `Limit: ${limitText.trim()}`,
10119
+ `Continuing on **${fallbackAgent}** with Auto model.`,
10120
+ "",
10121
+ `## Goal`,
10122
+ from.sourceRef?.trim() || "(none)",
10123
+ "",
10124
+ `## Child threads`,
10125
+ children.length ? children.join("\n") : "(none listed \u2014 call list_threads)",
10126
+ "",
10127
+ `## Recent turns (truncated)`,
10128
+ recent.length ? recent.join("\n") : "(none)",
10129
+ "",
10130
+ `## Instructions`,
10131
+ `- Continue fleet orchestration from this handoff.`,
10132
+ `- Prefer Sideboard MCP (list_threads, get_thread, send_to_thread, \u2026) for live status.`,
10133
+ `- Leave model Auto unless there is a specific reason to pin one.`,
10134
+ `- Do not wait on the limited ${from.agent} account; keep going on ${fallbackAgent}.`
10135
+ ].join("\n");
10136
+ return {
10137
+ id: (0, import_node_crypto6.randomUUID)(),
10138
+ name: "Orchestration quota handoff.md",
10139
+ kind: "transcript",
10140
+ content: body
10141
+ };
10142
+ }
10143
+ var QUOTA_CONTINUE_PROMPT = (fromAgent, fallback) => [
10144
+ `${fromAgent} hit a session/usage limit. Continue this orchestration on ${fallback} using the attached handoff.`,
10145
+ "Call list_threads for live fleet status, then proceed with the goal. Leave model Auto unless needed."
10146
+ ].join(" ");
10147
+ 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.";
10148
+ function createQuotaFailoverChat(from, fallbackAgent, limitText) {
10149
+ const handoff = buildQuotaHandoffAttachment(from, limitText, fallbackAgent);
10150
+ const tab = createChatTab({
10151
+ fromThreadId: from.id,
10152
+ agent: fallbackAgent,
10153
+ model: null,
10154
+ attachments: [handoff]
10155
+ });
10156
+ return updateThread(tab.id, {
10157
+ parentThreadId: from.id,
10158
+ quotaContinuedFromId: from.id,
10159
+ sourceRef: from.sourceRef,
10160
+ sourceType: "orchestration"
10161
+ });
10162
+ }
10163
+
9798
10164
  // src/orchestrator/orchestrator.ts
9799
10165
  init_types();
9800
10166
  init_settings();
@@ -9865,6 +10231,8 @@ var Orchestrator = class {
9865
10231
  haltDrain = /* @__PURE__ */ new Set();
9866
10232
  /** WIP snapshot SHA at the start of the latest agent turn (per thread). */
9867
10233
  turnBaselines = /* @__PURE__ */ new Map();
10234
+ /** Timers for orchestration session-quota auto-resume. */
10235
+ quotaResumeTimers = /* @__PURE__ */ new Map();
9868
10236
  maxConcurrent;
9869
10237
  runningCount = 0;
9870
10238
  constructor(opts) {
@@ -9944,6 +10312,113 @@ var Orchestrator = class {
9944
10312
  void this.drainQueue(thread.id);
9945
10313
  }
9946
10314
  }
10315
+ this.schedulePendingQuotaResumes();
10316
+ }
10317
+ clearQuotaResumeTimer(threadId) {
10318
+ const timer = this.quotaResumeTimers.get(threadId);
10319
+ if (timer) clearTimeout(timer);
10320
+ this.quotaResumeTimers.delete(threadId);
10321
+ }
10322
+ /** Schedule (or fire) auto-retry after a provider session/usage limit reset. */
10323
+ scheduleQuotaResume(threadId, resumeAt) {
10324
+ this.clearQuotaResumeTimer(threadId);
10325
+ updateThread(threadId, { quotaResumeAt: resumeAt.toISOString() });
10326
+ const delay = Math.max(5e3, resumeAt.getTime() - Date.now());
10327
+ const capped = Math.min(delay, 2147483647);
10328
+ const timer = setTimeout(() => {
10329
+ this.quotaResumeTimers.delete(threadId);
10330
+ void this.resumeAfterQuotaWait(threadId);
10331
+ }, capped);
10332
+ this.quotaResumeTimers.set(threadId, timer);
10333
+ }
10334
+ schedulePendingQuotaResumes() {
10335
+ for (const thread of listThreads({ includeArchived: false })) {
10336
+ if (!thread.quotaResumeAt) continue;
10337
+ const at = new Date(thread.quotaResumeAt);
10338
+ if (Number.isNaN(at.getTime())) continue;
10339
+ if (at.getTime() <= Date.now()) {
10340
+ void this.resumeAfterQuotaWait(thread.id);
10341
+ } else if (!this.quotaResumeTimers.has(thread.id)) {
10342
+ this.scheduleQuotaResume(thread.id, at);
10343
+ }
10344
+ }
10345
+ }
10346
+ async resumeAfterQuotaWait(threadId) {
10347
+ const thread = findThreadByRef(threadId);
10348
+ if (!thread || thread.status === "archived") return;
10349
+ this.clearQuotaResumeTimer(threadId);
10350
+ try {
10351
+ updateThread(threadId, { quotaResumeAt: null });
10352
+ } catch {
10353
+ return;
10354
+ }
10355
+ if (thread.status === "running" || this.activeTurns.has(threadId) || this.startingTurns.has(threadId)) {
10356
+ return;
10357
+ }
10358
+ await this.send(threadId, QUOTA_RESUME_PROMPT);
10359
+ }
10360
+ /**
10361
+ * Host-side continue when an orchestration chat hits a provider session/usage
10362
+ * limit (not context size): switch agent (Auto) or wait until reset.
10363
+ */
10364
+ async maybeHandleOrchestrationQuotaFailover(threadId, limitText) {
10365
+ const thread = findThreadByRef(threadId);
10366
+ if (!thread) return;
10367
+ const plan = planOrchestrationQuotaFailover(thread, limitText);
10368
+ if (!plan || plan.action === "none") return;
10369
+ if (plan.action === "wait_reset" && plan.resumeAt) {
10370
+ this.haltDrain.add(threadId);
10371
+ this.scheduleQuotaResume(threadId, plan.resumeAt);
10372
+ setStatus(threadId, "idle", null);
10373
+ appendMessage(threadId, {
10374
+ role: "agent",
10375
+ text: `Sideboard will auto-retry this orchestration around ${plan.resumeAt.toLocaleString()} when the session limit resets.`,
10376
+ ts: (/* @__PURE__ */ new Date()).toISOString()
10377
+ });
10378
+ this.emit({
10379
+ type: "quota_failover",
10380
+ threadId,
10381
+ action: "wait_reset",
10382
+ message: plan.reason,
10383
+ resumeAt: plan.resumeAt.toISOString()
10384
+ });
10385
+ this.emit({ type: "status_changed", threadId, status: "idle" });
10386
+ return;
10387
+ }
10388
+ if (plan.action === "switch_agent" && plan.fallbackAgent) {
10389
+ this.haltDrain.add(threadId);
10390
+ const next = createQuotaFailoverChat(
10391
+ thread,
10392
+ plan.fallbackAgent,
10393
+ plan.limitText
10394
+ );
10395
+ this.clearQuotaResumeTimer(threadId);
10396
+ try {
10397
+ updateThread(threadId, { quotaResumeAt: null });
10398
+ } catch {
10399
+ }
10400
+ appendMessage(threadId, {
10401
+ role: "agent",
10402
+ text: `Session limit on ${thread.agent}. Sideboard continued on ${plan.fallbackAgent} (Auto) in [${next.title}](sideboard://thread/${next.id}).`,
10403
+ ts: (/* @__PURE__ */ new Date()).toISOString()
10404
+ });
10405
+ this.emit({
10406
+ type: "quota_failover",
10407
+ threadId,
10408
+ action: "switch_agent",
10409
+ toThreadId: next.id,
10410
+ message: plan.reason
10411
+ });
10412
+ this.emit({
10413
+ type: "status_changed",
10414
+ threadId: next.id,
10415
+ status: next.status
10416
+ });
10417
+ await this.send(
10418
+ next.id,
10419
+ QUOTA_CONTINUE_PROMPT(thread.agent, plan.fallbackAgent)
10420
+ );
10421
+ }
9947
10422
  }
9948
10423
  getThreads(includeArchived = false) {
9949
10424
  return listThreads({ includeArchived });
@@ -10302,11 +10777,16 @@ var Orchestrator = class {
10302
10777
  }
10303
10778
  }
10304
10779
  }
10305
- const failureOnlyMessage = exitCode !== 0 && looksLikeAgentFailureMessage(assistantText) && !parts.some((p) => p.type === "tool" || p.type === "thinking");
10306
- if (!failureOnlyMessage && (assistantText || parts.length > 0)) {
10780
+ const lastStderr = summarizeTurnStderr(stderrTail);
10781
+ const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
10782
+ let chatText = assistantText;
10783
+ if (exitCode !== 0 && !chatText && looksLikeAgentFailureMessage(detail)) {
10784
+ chatText = humanizeAgentFailDetail(detail);
10785
+ }
10786
+ if (chatText || parts.length > 0) {
10307
10787
  appendMessage(threadId, {
10308
10788
  role: "agent",
10309
- text: assistantText,
10789
+ text: chatText,
10310
10790
  parts: parts.length > 0 ? parts : void 0,
10311
10791
  durationMs: Math.max(0, Date.now() - turnStartedAt),
10312
10792
  usage,
@@ -10325,13 +10805,12 @@ var Orchestrator = class {
10325
10805
  this.emit({ type: "status_changed", threadId, status: "stopped" });
10326
10806
  this.emit({ type: "turn_finished", threadId, exitCode });
10327
10807
  } else {
10328
- const lastStderr = summarizeTurnStderr(stderrTail);
10329
- const detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
10330
10808
  const failDetail = formatTurnExitError(exitCode, detail);
10809
+ const explainedInChat = exitCode !== 0 && Boolean(chatText) && (looksLikeAgentFailureMessage(chatText) || failDetail && chatText.includes(failDetail.replace(/^exit\s*\d+:\s*/i, "").trim()));
10331
10810
  setStatus(
10332
10811
  threadId,
10333
10812
  exitCode === 0 ? "idle" : "error",
10334
- exitCode === 0 ? null : failDetail
10813
+ exitCode === 0 || explainedInChat ? null : failDetail
10335
10814
  );
10336
10815
  this.emit({
10337
10816
  type: "status_changed",
@@ -10339,6 +10818,10 @@ var Orchestrator = class {
10339
10818
  status: exitCode === 0 ? "idle" : "error"
10340
10819
  });
10341
10820
  this.emit({ type: "turn_finished", threadId, exitCode });
10821
+ if (exitCode !== 0) {
10822
+ const blob = [chatText, detail].filter(Boolean).join("\n");
10823
+ void this.maybeHandleOrchestrationQuotaFailover(threadId, blob);
10824
+ }
10342
10825
  }
10343
10826
  } catch (err) {
10344
10827
  const message = err instanceof Error ? err.message : String(err);
@@ -10352,6 +10835,7 @@ var Orchestrator = class {
10352
10835
  this.emit({ type: "error", threadId, message });
10353
10836
  this.emit({ type: "status_changed", threadId, status: "error" });
10354
10837
  this.emit({ type: "turn_finished", threadId, exitCode: 1 });
10838
+ void this.maybeHandleOrchestrationQuotaFailover(threadId, message);
10355
10839
  }
10356
10840
  } finally {
10357
10841
  this.startingTurns.delete(threadId);
@@ -11062,6 +11546,7 @@ var import_zod = require("zod");
11062
11546
  var import_node_path25 = require("path");
11063
11547
  init_worktree();
11064
11548
  init_global_workspace();
11549
+ init_list_models();
11065
11550
 
11066
11551
  // src/mcp/archive-guard.ts
11067
11552
  init_global_workspace();
@@ -11478,6 +11963,120 @@ async function startMcpServer() {
11478
11963
  }
11479
11964
  }
11480
11965
  );
11966
+ const agentEnum = import_zod.z.enum(["claude", "codex", "opencode", "brightsy", "cursor"]);
11967
+ server.tool(
11968
+ "list_models",
11969
+ "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.",
11970
+ {
11971
+ agent: agentEnum.optional().describe("Limit to one agent; omit for all")
11972
+ },
11973
+ async ({ agent }) => {
11974
+ try {
11975
+ const catalogs = await listModelsForAgent(agent);
11976
+ return {
11977
+ content: [{ type: "text", text: JSON.stringify(catalogs, null, 2) }]
11978
+ };
11979
+ } catch (err) {
11980
+ const message = err instanceof Error ? err.message : String(err);
11981
+ return { content: [{ type: "text", text: message }], isError: true };
11982
+ }
11983
+ }
11984
+ );
11985
+ server.tool(
11986
+ "fork_worktree",
11987
+ "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.",
11988
+ {
11989
+ ref: import_zod.z.string().describe("Worktree thread id/ref to fork"),
11990
+ through_index: import_zod.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
11991
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
11992
+ model: import_zod.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
11993
+ title: import_zod.z.string().optional()
11994
+ },
11995
+ async ({ ref, through_index, agent, model, title }) => {
11996
+ try {
11997
+ const source = orch.getThread(ref);
11998
+ if (source) await orch.reconcile(source.repoPath);
11999
+ const thread = await orch.forkThreadWorktree({
12000
+ threadId: ref,
12001
+ throughIndex: through_index,
12002
+ agent,
12003
+ model,
12004
+ title
12005
+ });
12006
+ return {
12007
+ content: [
12008
+ {
12009
+ type: "text",
12010
+ text: JSON.stringify({
12011
+ id: thread.id,
12012
+ title: thread.title,
12013
+ status: thread.status,
12014
+ agent: thread.agent,
12015
+ model: thread.model,
12016
+ branchName: thread.branchName,
12017
+ worktreePath: thread.worktreePath,
12018
+ fromThreadId: source?.id ?? ref,
12019
+ link: `sideboard://thread/${thread.id}`
12020
+ })
12021
+ }
12022
+ ]
12023
+ };
12024
+ } catch (err) {
12025
+ const message = err instanceof Error ? err.message : String(err);
12026
+ return { content: [{ type: "text", text: message }], isError: true };
12027
+ }
12028
+ }
12029
+ );
12030
+ server.tool(
12031
+ "fork_chat",
12032
+ "Fork a chat into a NEW tab on the SAME workspace: worktree agent \u2192 same worktree tab; Global orchestration chat \u2192 new orchestration chat (same synthetic home). Seeds a transcript; optional agent override. Leave model unset for Auto unless you have a reason. Remote coordinators use this to continue an orchestration chat on another agent after session limits. Then send_to_thread / wait_for_turn on the returned id. Use fork_worktree only for worktree agents that need a new git worktree.",
12033
+ {
12034
+ ref: import_zod.z.string().describe("Thread id/ref to fork (worktree agent or orchestration chat)"),
12035
+ through_index: import_zod.z.number().optional().describe("Inclusive message index to include in the transcript (default: all)"),
12036
+ agent: agentEnum.optional().describe("Agent for the forked chat (default: same as source)"),
12037
+ model: import_zod.z.string().nullable().optional().describe("Usually omit (Auto). Only set from list_models when you need a specific model"),
12038
+ title: import_zod.z.string().optional()
12039
+ },
12040
+ async ({ ref, through_index, agent, model, title }) => {
12041
+ try {
12042
+ const source = orch.getThread(ref);
12043
+ if (!source) {
12044
+ return {
12045
+ content: [{ type: "text", text: `Thread not found: ${ref}` }],
12046
+ isError: true
12047
+ };
12048
+ }
12049
+ const tab = orch.forkChatTab({
12050
+ threadId: source.id,
12051
+ throughIndex: through_index,
12052
+ agent,
12053
+ model,
12054
+ title
12055
+ });
12056
+ return {
12057
+ content: [
12058
+ {
12059
+ type: "text",
12060
+ text: JSON.stringify({
12061
+ id: tab.id,
12062
+ title: tab.title,
12063
+ status: tab.status,
12064
+ agent: tab.agent,
12065
+ model: tab.model,
12066
+ sourceType: tab.sourceType,
12067
+ worktreePath: tab.worktreePath,
12068
+ fromThreadId: source.id,
12069
+ link: `sideboard://thread/${tab.id}`
12070
+ })
12071
+ }
12072
+ ]
12073
+ };
12074
+ } catch (err) {
12075
+ const message = err instanceof Error ? err.message : String(err);
12076
+ return { content: [{ type: "text", text: message }], isError: true };
12077
+ }
12078
+ }
12079
+ );
11481
12080
  server.tool(
11482
12081
  "run_dev_script",
11483
12082
  "Start a .sideboard/.conductor run script for a thread (default script if name omitted); returns port",
@@ -12004,6 +12603,7 @@ init_injected_mcp();
12004
12603
  0 && (module.exports = {
12005
12604
  BRIGHTSY_MCP_ALLOWED_TOOLS,
12006
12605
  BrightsySideboardApi,
12606
+ CLAUDE_MODEL_CATALOG,
12007
12607
  CLOUD_COORDINATOR_BUSY_REPLY,
12008
12608
  CLOUD_COORDINATOR_STOPPED_REPLY,
12009
12609
  CLOUD_COORDINATOR_TIMEOUT_REPLY,
@@ -12180,6 +12780,7 @@ init_injected_mcp();
12180
12780
  isOrchestratorThread,
12181
12781
  isPidAlive,
12182
12782
  isPlaceholderBranch,
12783
+ isSessionQuotaLimit,
12183
12784
  isThinkingEffort,
12184
12785
  listAgentSetupInfo,
12185
12786
  listBranchCommits,
@@ -12195,6 +12796,7 @@ init_injected_mcp();
12195
12796
  listIssues,
12196
12797
  listLinearIssues,
12197
12798
  listLinearIssuesDirect,
12799
+ listModelsForAgent,
12198
12800
  listOpencodeModels,
12199
12801
  listPrs,
12200
12802
  listRunScripts,
@@ -12226,6 +12828,8 @@ init_injected_mcp();
12226
12828
  normalizeWorktreePath,
12227
12829
  openInSystemTerminal,
12228
12830
  opencodeAdapter,
12831
+ orchestrationQuotaFallbackAgent,
12832
+ orchestrationQuotaOnLimit,
12229
12833
  orchestrationTitleNeedsSoccerNickname,
12230
12834
  orchestratorSessionPoisonedByBuiltins,
12231
12835
  originGhRepoEnv,
@@ -12233,6 +12837,7 @@ init_injected_mcp();
12233
12837
  parseForceStopMessage,
12234
12838
  parseGithubSlugFromRemoteUrl,
12235
12839
  parseMcpList,
12840
+ parseSessionQuotaResetAt,
12236
12841
  partsToAssistantText,
12237
12842
  pastedTextStats,
12238
12843
  permissionMode,
@@ -12259,6 +12864,7 @@ init_injected_mcp();
12259
12864
  resolveFilesToCopy,
12260
12865
  resolveGithubRepoSlug,
12261
12866
  resolvePrSelector,
12867
+ resolveQuotaFallbackAgent,
12262
12868
  resolveRepoRoot,
12263
12869
  resolveThreadDefaults,
12264
12870
  resolveThreadEffort,