copilot-hub 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +3 -2
  2. package/apps/agent-engine/dist/config.js +58 -0
  3. package/apps/agent-engine/dist/index.js +90 -16
  4. package/apps/control-plane/dist/channels/codex-quota-cache.js +16 -0
  5. package/apps/control-plane/dist/channels/hub-model-utils.js +244 -24
  6. package/apps/control-plane/dist/channels/hub-ops-commands.js +631 -279
  7. package/apps/control-plane/dist/channels/telegram-channel.js +5 -7
  8. package/apps/control-plane/dist/config.js +58 -0
  9. package/apps/control-plane/dist/index.js +16 -0
  10. package/apps/control-plane/dist/test/hub-model-utils.test.js +110 -13
  11. package/package.json +3 -2
  12. package/packages/core/dist/agent-supervisor.d.ts +5 -0
  13. package/packages/core/dist/agent-supervisor.js +11 -0
  14. package/packages/core/dist/agent-supervisor.js.map +1 -1
  15. package/packages/core/dist/bot-manager.js +17 -1
  16. package/packages/core/dist/bot-manager.js.map +1 -1
  17. package/packages/core/dist/bot-runtime.d.ts +4 -0
  18. package/packages/core/dist/bot-runtime.js +5 -1
  19. package/packages/core/dist/bot-runtime.js.map +1 -1
  20. package/packages/core/dist/codex-app-client.d.ts +13 -2
  21. package/packages/core/dist/codex-app-client.js +51 -13
  22. package/packages/core/dist/codex-app-client.js.map +1 -1
  23. package/packages/core/dist/codex-app-utils.d.ts +6 -0
  24. package/packages/core/dist/codex-app-utils.js +49 -0
  25. package/packages/core/dist/codex-app-utils.js.map +1 -1
  26. package/packages/core/dist/codex-provider.d.ts +3 -1
  27. package/packages/core/dist/codex-provider.js +3 -1
  28. package/packages/core/dist/codex-provider.js.map +1 -1
  29. package/packages/core/dist/kernel-control-plane.d.ts +1 -0
  30. package/packages/core/dist/kernel-control-plane.js +132 -13
  31. package/packages/core/dist/kernel-control-plane.js.map +1 -1
  32. package/packages/core/dist/provider-factory.d.ts +2 -0
  33. package/packages/core/dist/provider-factory.js +3 -0
  34. package/packages/core/dist/provider-factory.js.map +1 -1
  35. package/packages/core/dist/provider-options.js +24 -17
  36. package/packages/core/dist/provider-options.js.map +1 -1
  37. package/packages/core/dist/state-store.d.ts +1 -0
  38. package/packages/core/dist/state-store.js +28 -2
  39. package/packages/core/dist/state-store.js.map +1 -1
  40. package/packages/core/dist/telegram-channel.d.ts +1 -0
  41. package/packages/core/dist/telegram-channel.js +3 -0
  42. package/packages/core/dist/telegram-channel.js.map +1 -1
  43. package/scripts/dist/cli.mjs +132 -203
  44. package/scripts/dist/codex-runtime.mjs +352 -0
  45. package/scripts/dist/codex-version.mjs +91 -0
  46. package/scripts/dist/configure.mjs +26 -49
  47. package/scripts/dist/daemon.mjs +58 -0
  48. package/scripts/src/cli.mts +166 -233
  49. package/scripts/src/codex-runtime.mts +499 -0
  50. package/scripts/src/codex-version.mts +114 -0
  51. package/scripts/src/configure.mts +30 -65
  52. package/scripts/src/daemon.mts +69 -0
  53. package/scripts/test/codex-version.test.mjs +21 -0
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { applyBotModelPolicy, applyModelPolicyToBots, applyRuntimeModelPolicy, buildSessionModelOptions, fetchCodexModelOptions, formatModelButtonText, formatModelLabel, getBotPolicyState, getRuntimeModel, parseSetModelAllCommand, parseSetModelCommand, resolveModelSelectionFromAction, resolveSharedModel, } from "./hub-model-utils.js";
2
+ import { invalidateCodexQuotaUsageCache } from "./codex-quota-cache.js";
3
+ import { applyBotProviderPolicy, applyProviderPolicyToBots, applyRuntimeProviderPolicy, buildReasoningOptionsForModel, buildSessionModelOptions, fetchCodexModelOptions, formatFastModeLabel, formatModelButtonText, formatModelLabel, formatReasoningLabel, getBotPolicyState, getBotProviderSelection, getRuntimeProviderSelection, parseSetModelAllCommand, parseSetModelCommand, resolveModelSelectionFromAction, resolveReasoningSelectionFromAction, resolveSharedModel, resolveSharedReasoningEffort, resolveSharedServiceTier, } from "./hub-model-utils.js";
3
4
  const MENU_TTL_MS = 15 * 60 * 1000;
4
5
  const FLOW_TTL_MS = 10 * 60 * 1000;
5
6
  const TELEGRAM_VERIFY_TIMEOUT_MS = 10_000;
@@ -179,14 +180,15 @@ export async function maybeHandleHubOpsCommand({ ctx, runtime, channelId, }) {
179
180
  .split(/\s+/)
180
181
  .filter(Boolean).length;
181
182
  if (tokenCount <= 1) {
182
- await renderSetModelAgentMenu(ctx, { editMessage: false });
183
+ await renderGlobalModelMenu(ctx, { editMessage: false, runtime: runtime ?? null });
183
184
  return true;
184
185
  }
185
186
  const parsed = parseSetModelCommand(text, BOT_ID_PATTERN);
186
187
  if (!parsed.ok) {
187
- await renderSetModelAgentMenu(ctx, {
188
+ await renderGlobalModelMenu(ctx, {
188
189
  editMessage: false,
189
- notice: "Choose the agent from the list below.",
190
+ runtime: runtime ?? null,
191
+ notice: "Choose a model first, then the target.",
190
192
  });
191
193
  return true;
192
194
  }
@@ -195,15 +197,19 @@ export async function maybeHandleHubOpsCommand({ ctx, runtime, channelId, }) {
195
197
  await ctx.reply(`Agent '${parsed.botId}' not found.`);
196
198
  return true;
197
199
  }
198
- await applyBotModelPolicy({
200
+ await applyBotProviderPolicy({
199
201
  apiPost,
200
202
  botId: parsed.botId,
201
203
  botState,
202
- model: parsed.model,
204
+ patch: {
205
+ model: parsed.model,
206
+ reasoningEffort: null,
207
+ },
203
208
  });
204
209
  const modelLabel = parsed.model ? parsed.model : "auto (workspace default)";
205
210
  await ctx.reply([
206
211
  `Model updated for '${parsed.botId}': ${modelLabel}`,
212
+ "Reasoning reset to the model default.",
207
213
  "Change applies on next message while preserving conversation history.",
208
214
  ].join("\n"));
209
215
  return true;
@@ -223,9 +229,12 @@ export async function maybeHandleHubOpsCommand({ ctx, runtime, channelId, }) {
223
229
  return true;
224
230
  }
225
231
  const bots = await fetchBots();
226
- const result = await applyGlobalModelSelection({
232
+ const result = await applyGlobalProviderSelection({
227
233
  bots,
228
- model: parsed.model,
234
+ patch: {
235
+ model: parsed.model,
236
+ reasoningEffort: null,
237
+ },
229
238
  runtime: runtime ?? null,
230
239
  });
231
240
  if (result.totalTargets === 0) {
@@ -233,7 +242,11 @@ export async function maybeHandleHubOpsCommand({ ctx, runtime, channelId, }) {
233
242
  return true;
234
243
  }
235
244
  const modelLabel = parsed.model ? parsed.model : "auto (workspace default)";
236
- const lines = buildGlobalModelUpdateLines({ modelLabel, result });
245
+ const lines = buildGlobalModelUpdateLines({
246
+ modelLabel,
247
+ reasoningLabel: "Default",
248
+ result,
249
+ });
237
250
  lines.push("Change applies on next message while preserving conversation history.");
238
251
  await ctx.reply(lines.join("\n"));
239
252
  return true;
@@ -272,7 +285,13 @@ export async function maybeHandleHubOpsFollowUp({ ctx, runtime, channelId, }) {
272
285
  const flowKey = buildFlowKey(runtime?.runtimeId, channelId, chatId);
273
286
  const codexFlow = codexSwitchFlows.get(flowKey);
274
287
  if (codexFlow) {
275
- const handled = await handleCodexSwitchFlow({ ctx, flowKey, flow: codexFlow, text });
288
+ const handled = await handleCodexSwitchFlow({
289
+ ctx,
290
+ flowKey,
291
+ flow: codexFlow,
292
+ text,
293
+ runtime: runtime ?? null,
294
+ });
276
295
  if (handled) {
277
296
  return true;
278
297
  }
@@ -362,7 +381,7 @@ export async function maybeHandleHubOpsFollowUp({ ctx, runtime, channelId, }) {
362
381
  await ctx.reply("Flow reset. Use /create_agent to start again.");
363
382
  return true;
364
383
  }
365
- async function handleCodexSwitchFlow({ ctx, flowKey, flow, text, }) {
384
+ async function handleCodexSwitchFlow({ ctx, flowKey, flow, text, runtime, }) {
366
385
  if (flow.step !== "api_key") {
367
386
  codexSwitchFlows.delete(flowKey);
368
387
  await ctx.reply("Flow reset. Use /codex_switch_key to start again.");
@@ -379,9 +398,10 @@ async function handleCodexSwitchFlow({ ctx, flowKey, flow, text, }) {
379
398
  apiKey,
380
399
  });
381
400
  codexSwitchFlows.delete(flowKey);
401
+ const refreshMessage = await refreshHubProviderAfterCodexLogin(runtime);
382
402
  const refreshedBots = readRefreshedBotIds(result);
383
403
  const refreshFailures = readRefreshFailures(result);
384
- const lines = ["Codex account switched successfully."];
404
+ const lines = ["Codex account switched successfully.", refreshMessage];
385
405
  if (result?.detail) {
386
406
  lines.push(`status: ${String(result.detail)}`);
387
407
  }
@@ -424,24 +444,25 @@ export async function maybeHandleHubOpsCallback({ ctx, runtime, }) {
424
444
  await answerCallbackQuerySafe(ctx, "Updated");
425
445
  return true;
426
446
  }
427
- if (action.type === "model_home") {
428
- await renderSetModelAgentMenu(ctx, {
447
+ if (action.type === "agents_home") {
448
+ await renderAgentsMenu(ctx, {
429
449
  sessionId: action.sessionId,
430
450
  editMessage: true,
431
451
  });
432
452
  await answerCallbackQuerySafe(ctx);
433
453
  return true;
434
454
  }
435
- if (action.type === "model_open") {
436
- await renderBotModelMenu(ctx, {
455
+ if (action.type === "global_model_open") {
456
+ await renderGlobalModelMenu(ctx, {
437
457
  sessionId: action.sessionId,
438
- index: action.index,
458
+ editMessage: true,
459
+ runtime: runtime ?? null,
439
460
  });
440
461
  await answerCallbackQuerySafe(ctx);
441
462
  return true;
442
463
  }
443
- if (action.type === "global_model_open") {
444
- await renderGlobalModelMenu(ctx, {
464
+ if (action.type === "global_fast_open") {
465
+ await renderGlobalFastMenu(ctx, {
445
466
  sessionId: action.sessionId,
446
467
  editMessage: true,
447
468
  runtime: runtime ?? null,
@@ -449,6 +470,56 @@ export async function maybeHandleHubOpsCallback({ ctx, runtime, }) {
449
470
  await answerCallbackQuerySafe(ctx);
450
471
  return true;
451
472
  }
473
+ if (action.type === "target_choice") {
474
+ const session = getMenuSession(action.sessionId, ctx);
475
+ if (!session || !session.pendingProviderPatch || !session.pendingFlow) {
476
+ await renderBotsMenu(ctx, {
477
+ editMessage: true,
478
+ notice: "Selection expired. Open the menu again.",
479
+ });
480
+ await answerCallbackQuerySafe(ctx, "Selection expired");
481
+ return true;
482
+ }
483
+ if (action.profileId === "single") {
484
+ await renderProviderTargetAgentMenu(ctx, {
485
+ sessionId: action.sessionId,
486
+ editMessage: true,
487
+ });
488
+ await answerCallbackQuerySafe(ctx);
489
+ return true;
490
+ }
491
+ if (action.profileId === "all" || action.profileId === "all_hub") {
492
+ const bots = await fetchBots();
493
+ const result = await applyGlobalProviderSelection({
494
+ bots,
495
+ patch: session.pendingProviderPatch,
496
+ runtime: action.profileId === "all_hub" ? (runtime ?? null) : null,
497
+ });
498
+ const lines = session.pendingFlow === "speed"
499
+ ? buildGlobalFastUpdateLines({
500
+ fastLabel: session.pendingSummary.speedLabel ?? "Standard",
501
+ result,
502
+ })
503
+ : buildGlobalModelUpdateLines({
504
+ modelLabel: session.pendingSummary.modelLabel ?? "auto (workspace default)",
505
+ reasoningLabel: session.pendingSummary.reasoningLabel ?? "Default",
506
+ result,
507
+ });
508
+ lines.push(session.pendingFlow === "speed"
509
+ ? "Speed changes apply on next message."
510
+ : "Change applies on next message while preserving conversation history.");
511
+ clearPendingProviderSelection(session);
512
+ menuSessions.set(action.sessionId, session);
513
+ await renderBotsMenu(ctx, {
514
+ editMessage: true,
515
+ notice: lines.join("\n"),
516
+ });
517
+ await answerCallbackQuerySafe(ctx, "Updated");
518
+ return true;
519
+ }
520
+ await answerCallbackQuerySafe(ctx, "Invalid target");
521
+ return true;
522
+ }
452
523
  if (action.type === "back") {
453
524
  await renderBotsMenu(ctx, { editMessage: true });
454
525
  await answerCallbackQuerySafe(ctx);
@@ -470,39 +541,103 @@ export async function maybeHandleHubOpsCallback({ ctx, runtime, }) {
470
541
  sessionId: action.sessionId,
471
542
  editMessage: true,
472
543
  runtime: runtime ?? null,
473
- notice: "Model options expired. Open /set_model_all again.",
544
+ notice: "Model menu expired. Open Model & Reasoning again.",
474
545
  });
475
546
  await answerCallbackQuerySafe(ctx, "Model menu expired");
476
547
  return true;
477
548
  }
478
- const bots = await fetchBots();
479
- const result = await applyGlobalModelSelection({
480
- bots,
481
- model: selection.model,
549
+ if (selection.model) {
550
+ await renderGlobalReasoningMenu(ctx, {
551
+ sessionId: action.sessionId,
552
+ runtime: runtime ?? null,
553
+ modelSelection: selection,
554
+ });
555
+ await answerCallbackQuerySafe(ctx);
556
+ return true;
557
+ }
558
+ setPendingProviderSelection(session, {
559
+ patch: {
560
+ model: null,
561
+ reasoningEffort: null,
562
+ },
563
+ flow: "model_reasoning",
564
+ modelLabel: selection.label,
565
+ reasoningLabel: "Default",
566
+ });
567
+ menuSessions.set(action.sessionId, session);
568
+ await renderProviderTargetMenu(ctx, {
569
+ sessionId: action.sessionId,
570
+ editMessage: true,
482
571
  runtime: runtime ?? null,
483
572
  });
484
- if (result.totalTargets === 0) {
573
+ await answerCallbackQuerySafe(ctx);
574
+ return true;
575
+ }
576
+ if (action.type === "global_reasoning_apply") {
577
+ const modelSelection = resolveModelSelectionFromAction({
578
+ session,
579
+ profileId: action.modelProfileId,
580
+ });
581
+ if (!modelSelection.ok || !modelSelection.model) {
485
582
  await renderGlobalModelMenu(ctx, {
486
583
  sessionId: action.sessionId,
487
584
  editMessage: true,
488
585
  runtime: runtime ?? null,
489
- notice: "No agents found and hub model control is unavailable.",
586
+ notice: "Model menu expired. Open Model & Reasoning again.",
490
587
  });
491
- await answerCallbackQuerySafe(ctx, "No targets");
588
+ await answerCallbackQuerySafe(ctx, "Model menu expired");
492
589
  return true;
493
590
  }
494
- const lines = buildGlobalModelUpdateLines({
495
- modelLabel: selection.label,
496
- result,
591
+ const reasoningOptions = buildReasoningOptionsForModel({
592
+ modelSelection,
497
593
  });
498
- lines.push("Change applies on next message while preserving conversation history.");
499
- await renderGlobalModelMenu(ctx, {
594
+ const reasoningSelection = resolveReasoningSelectionFromAction({
595
+ options: reasoningOptions,
596
+ profileId: action.profileId,
597
+ });
598
+ if (!reasoningSelection.ok) {
599
+ await renderGlobalReasoningMenu(ctx, {
600
+ sessionId: action.sessionId,
601
+ runtime: runtime ?? null,
602
+ modelSelection,
603
+ notice: "Reasoning options expired. Choose the model again.",
604
+ });
605
+ await answerCallbackQuerySafe(ctx, "Reasoning menu expired");
606
+ return true;
607
+ }
608
+ setPendingProviderSelection(session, {
609
+ patch: {
610
+ model: modelSelection.model,
611
+ reasoningEffort: reasoningSelection.reasoningEffort,
612
+ },
613
+ flow: "model_reasoning",
614
+ modelLabel: modelSelection.label,
615
+ reasoningLabel: reasoningSelection.label,
616
+ });
617
+ menuSessions.set(action.sessionId, session);
618
+ await renderProviderTargetMenu(ctx, {
619
+ sessionId: action.sessionId,
620
+ editMessage: true,
621
+ runtime: runtime ?? null,
622
+ });
623
+ await answerCallbackQuerySafe(ctx);
624
+ return true;
625
+ }
626
+ if (action.type === "global_fast_apply") {
627
+ setPendingProviderSelection(session, {
628
+ patch: {
629
+ serviceTier: action.profileId === "fast" ? "fast" : null,
630
+ },
631
+ flow: "speed",
632
+ speedLabel: action.profileId === "fast" ? "Fast" : "Standard",
633
+ });
634
+ menuSessions.set(action.sessionId, session);
635
+ await renderProviderTargetMenu(ctx, {
500
636
  sessionId: action.sessionId,
501
637
  editMessage: true,
502
638
  runtime: runtime ?? null,
503
- notice: lines.join("\n"),
504
639
  });
505
- await answerCallbackQuerySafe(ctx, "Model updated");
640
+ await answerCallbackQuerySafe(ctx);
506
641
  return true;
507
642
  }
508
643
  const botId = getBotIdFromSession(session, action.index);
@@ -511,6 +646,43 @@ export async function maybeHandleHubOpsCallback({ ctx, runtime, }) {
511
646
  await answerCallbackQuerySafe(ctx, "Agent not found. Refreshed.");
512
647
  return true;
513
648
  }
649
+ if (action.type === "target_agent_apply") {
650
+ if (!session.pendingProviderPatch || !session.pendingFlow) {
651
+ await renderBotsMenu(ctx, {
652
+ editMessage: true,
653
+ notice: "Selection expired. Open the menu again.",
654
+ });
655
+ await answerCallbackQuerySafe(ctx, "Selection expired");
656
+ return true;
657
+ }
658
+ const botState = await fetchBotById(botId);
659
+ if (!botState) {
660
+ await renderAgentsMenu(ctx, {
661
+ sessionId: action.sessionId,
662
+ editMessage: true,
663
+ notice: `Agent '${botId}' not found.`,
664
+ });
665
+ await answerCallbackQuerySafe(ctx, "Agent not found");
666
+ return true;
667
+ }
668
+ await applyBotProviderPolicy({
669
+ apiPost,
670
+ botId,
671
+ botState,
672
+ patch: session.pendingProviderPatch,
673
+ });
674
+ const notice = session.pendingFlow === "speed"
675
+ ? `Speed updated for '${botId}': ${session.pendingSummary.speedLabel ?? "Standard"}`
676
+ : `Model updated for '${botId}': ${session.pendingSummary.modelLabel ?? "auto"} / ${session.pendingSummary.reasoningLabel ?? "Default"}`;
677
+ clearPendingProviderSelection(session);
678
+ menuSessions.set(action.sessionId, session);
679
+ await renderBotsMenu(ctx, {
680
+ editMessage: true,
681
+ notice,
682
+ });
683
+ await answerCallbackQuerySafe(ctx, "Updated");
684
+ return true;
685
+ }
514
686
  if (action.type === "open") {
515
687
  await renderBotActions(ctx, {
516
688
  sessionId: action.sessionId,
@@ -541,78 +713,6 @@ export async function maybeHandleHubOpsCallback({ ctx, runtime, }) {
541
713
  await answerCallbackQuerySafe(ctx, "Policy updated");
542
714
  return true;
543
715
  }
544
- if (action.type === "model") {
545
- const botState = await fetchBotById(botId);
546
- if (!botState) {
547
- await renderBotsMenu(ctx, { editMessage: true, notice: `Agent '${botId}' not found.` });
548
- await answerCallbackQuerySafe(ctx, "Agent not found");
549
- return true;
550
- }
551
- const selection = resolveModelSelectionFromAction({
552
- session,
553
- profileId: action.profileId,
554
- });
555
- if (!selection.ok) {
556
- await renderBotActions(ctx, {
557
- sessionId: action.sessionId,
558
- index: action.index,
559
- notice: "Model options expired. Open /bots again.",
560
- });
561
- await answerCallbackQuerySafe(ctx, "Model menu expired");
562
- return true;
563
- }
564
- await applyBotModelPolicy({
565
- apiPost,
566
- botId,
567
- botState,
568
- model: selection.model,
569
- });
570
- await renderBotActions(ctx, {
571
- sessionId: action.sessionId,
572
- index: action.index,
573
- notice: `Model updated: ${selection.label}`,
574
- });
575
- await answerCallbackQuerySafe(ctx, "Model updated");
576
- return true;
577
- }
578
- if (action.type === "model_apply") {
579
- const botState = await fetchBotById(botId);
580
- if (!botState) {
581
- await renderSetModelAgentMenu(ctx, {
582
- sessionId: action.sessionId,
583
- editMessage: true,
584
- notice: `Agent '${botId}' not found.`,
585
- });
586
- await answerCallbackQuerySafe(ctx, "Agent not found");
587
- return true;
588
- }
589
- const selection = resolveModelSelectionFromAction({
590
- session,
591
- profileId: action.profileId,
592
- });
593
- if (!selection.ok) {
594
- await renderBotModelMenu(ctx, {
595
- sessionId: action.sessionId,
596
- index: action.index,
597
- notice: "Model options expired. Open /set_model again.",
598
- });
599
- await answerCallbackQuerySafe(ctx, "Model menu expired");
600
- return true;
601
- }
602
- await applyBotModelPolicy({
603
- apiPost,
604
- botId,
605
- botState,
606
- model: selection.model,
607
- });
608
- await renderBotModelMenu(ctx, {
609
- sessionId: action.sessionId,
610
- index: action.index,
611
- notice: `Model updated: ${selection.label}`,
612
- });
613
- await answerCallbackQuerySafe(ctx, "Model updated");
614
- return true;
615
- }
616
716
  if (action.type === "reset_ask") {
617
717
  await renderResetConfirm(ctx, {
618
718
  sessionId: action.sessionId,
@@ -643,7 +743,8 @@ export async function maybeHandleHubOpsCallback({ ctx, runtime, }) {
643
743
  }
644
744
  if (action.type === "delete_confirm") {
645
745
  await apiPost(`/api/bots/${encodeURIComponent(botId)}/delete`, { deleteMode: "soft" });
646
- await renderBotsMenu(ctx, {
746
+ await renderAgentsMenu(ctx, {
747
+ sessionId: action.sessionId,
647
748
  editMessage: true,
648
749
  notice: `Agent deleted: ${botId}`,
649
750
  });
@@ -657,7 +758,7 @@ export async function maybeHandleHubOpsCallback({ ctx, runtime, }) {
657
758
  await answerCallbackQuerySafe(ctx, "Action failed");
658
759
  await editMessageOrReply(ctx, `Action failed:\n${sanitizeError(error)}`, {
659
760
  reply_markup: {
660
- inline_keyboard: [[{ text: "Back to bots", callback_data: "hub:back" }]],
761
+ inline_keyboard: [[{ text: "Back to menu", callback_data: "hub:back" }]],
661
762
  },
662
763
  });
663
764
  return true;
@@ -668,32 +769,25 @@ function buildHelpText(runtimeName) {
668
769
  `${String(runtimeName ?? "Copilot Hub")}`,
669
770
  "",
670
771
  "Commands:",
671
- "/help",
672
- "/health",
673
772
  "/bots",
773
+ "/health",
674
774
  "/create_agent",
675
775
  "/codex_status",
676
776
  "/codex_login",
677
777
  "/codex_switch_key",
678
778
  "/set_model",
679
- "/set_model_all",
680
779
  "/cancel",
681
780
  "",
682
- "Codex account:",
683
- "/codex_login: switch account with device code flow",
781
+ "/bots: open the main control menu",
782
+ "/set_model: open Model & Reasoning directly",
783
+ "/codex_login: switch account with device code",
684
784
  "/codex_switch_key: switch account with API key",
685
785
  "",
686
- "Policy guide in /bots:",
687
- "Safe: read-only + approval prompts",
688
- "Standard: workspace write + approval prompts",
689
- "Semi Auto: workspace write + ask on failures",
690
- "Full: no approval prompts",
691
- "All agent actions start from that agent workspace.",
692
- "Model changes apply on next message and keep conversation history.",
693
- "/set_model opens a clickable agent->model flow.",
694
- "/set_model_all opens a clickable model list for all agents and the hub.",
695
- "",
696
- "For development tasks, send a normal message to the assistant.",
786
+ "Use /bots for:",
787
+ "Agents",
788
+ "Model & Reasoning",
789
+ "Speed",
790
+ "Create agent",
697
791
  ].join("\n");
698
792
  }
699
793
  function buildFlowKey(runtimeId, channelId, chatId) {
@@ -734,6 +828,47 @@ async function renderBotsMenu(ctx, { editMessage = false, notice = "" } = {}) {
734
828
  if (notice) {
735
829
  lines.push(notice, "");
736
830
  }
831
+ lines.push("Control menu:");
832
+ lines.push(`agents: ${bots.length}`);
833
+ if (bots.length > 0) {
834
+ const runningCount = bots.filter((bot) => bot.running).length;
835
+ lines.push(`running: ${runningCount}/${bots.length}`);
836
+ }
837
+ lines.push("", "Choose a section:");
838
+ const keyboard = buildBotsMenuKeyboard(sessionId, bots);
839
+ if (editMessage) {
840
+ await editMessageOrReply(ctx, lines.join("\n"), {
841
+ reply_markup: {
842
+ inline_keyboard: keyboard,
843
+ },
844
+ });
845
+ return;
846
+ }
847
+ await ctx.reply(lines.join("\n"), {
848
+ reply_markup: {
849
+ inline_keyboard: keyboard,
850
+ },
851
+ });
852
+ }
853
+ async function renderAgentsMenu(ctx, { sessionId = "", editMessage = false, notice = "", } = {}) {
854
+ const bots = await fetchBots();
855
+ const chatId = getChatId(ctx);
856
+ const activeSessionId = sessionId || createMenuSession(chatId, bots);
857
+ const session = getMenuSession(activeSessionId, ctx);
858
+ if (!session) {
859
+ await renderBotsMenu(ctx, {
860
+ editMessage,
861
+ notice: "Menu expired. Open /bots again.",
862
+ });
863
+ return;
864
+ }
865
+ session.botIds = bots.map((entry) => String(entry?.id ?? "").trim()).filter(Boolean);
866
+ clearPendingProviderSelection(session);
867
+ menuSessions.set(activeSessionId, session);
868
+ const lines = [];
869
+ if (notice) {
870
+ lines.push(notice, "");
871
+ }
737
872
  lines.push("Agents:");
738
873
  if (bots.length === 0) {
739
874
  lines.push("No bots registered.");
@@ -743,9 +878,9 @@ async function renderBotsMenu(ctx, { editMessage = false, notice = "" } = {}) {
743
878
  const status = botState.running ? "ON" : "OFF";
744
879
  lines.push(`- ${botState.id} (${status})`);
745
880
  }
746
- lines.push("", "Tap an agent below.");
881
+ lines.push("", "Choose an agent:");
747
882
  }
748
- const keyboard = buildBotsMenuKeyboard(sessionId, bots);
883
+ const keyboard = buildAgentsMenuKeyboard(activeSessionId, bots);
749
884
  if (editMessage) {
750
885
  await editMessageOrReply(ctx, lines.join("\n"), {
751
886
  reply_markup: {
@@ -764,41 +899,36 @@ async function renderBotActions(ctx, { sessionId, index, notice = "" }) {
764
899
  const session = getMenuSession(sessionId, ctx);
765
900
  const botId = getBotIdFromSession(session, index);
766
901
  if (!botId) {
767
- await renderBotsMenu(ctx, { editMessage: true, notice: "Agent not found. Refreshed." });
902
+ await renderAgentsMenu(ctx, {
903
+ sessionId,
904
+ editMessage: true,
905
+ notice: "Agent not found. Refreshed.",
906
+ });
768
907
  return;
769
908
  }
770
909
  const botState = await fetchBotById(botId);
771
910
  if (!botState) {
772
- await renderBotsMenu(ctx, { editMessage: true, notice: `Agent '${botId}' not found.` });
911
+ await renderAgentsMenu(ctx, {
912
+ sessionId,
913
+ editMessage: true,
914
+ notice: `Agent '${botId}' not found.`,
915
+ });
773
916
  return;
774
917
  }
775
- const providerOptions = botState?.provider?.options && typeof botState.provider.options === "object"
776
- ? botState.provider.options
777
- : {};
778
- const currentModel = String(providerOptions.model ?? "").trim();
918
+ const providerSelection = getBotProviderSelection(botState);
779
919
  const botPolicyState = getBotPolicyState(botState);
780
- const modelCatalog = await fetchCodexModelOptions(apiGet);
781
- const modelOptions = buildSessionModelOptions({
782
- catalog: modelCatalog.models,
783
- currentModel,
784
- });
785
920
  if (session) {
786
- session.modelOptions = modelOptions;
921
+ clearPendingProviderSelection(session);
787
922
  menuSessions.set(sessionId, session);
788
923
  }
789
924
  const lines = [];
790
925
  if (notice) {
791
926
  lines.push(notice, "");
792
927
  }
793
- lines.push(`Agent: ${botState.id}`, `running: ${botState.running ? "yes" : "no"}`, `telegram: ${botState.telegramRunning ? "yes" : "no"}`, `sandboxMode: ${botPolicyState.sandboxMode}`, `approvalPolicy: ${botPolicyState.approvalPolicy}`, `model: ${formatModelLabel(providerOptions.model)}`, "", "Policy quick guide:", "Safe = read-only + approval prompts", "Standard = workspace write + approval prompts", "Semi Auto = workspace write + ask on failures", "Full = no approval prompts", "All actions start from this agent workspace.", "Model changes apply on next message and keep conversation history.", modelCatalog.available
794
- ? `Available models: ${modelCatalog.models.length}`
795
- : "Available models: unavailable (you can still use /set_model).", "", "Choose an action:");
928
+ lines.push(`Agent: ${botState.id}`, `running: ${botState.running ? "yes" : "no"}`, `telegram: ${botState.telegramRunning ? "yes" : "no"}`, `sandboxMode: ${botPolicyState.sandboxMode}`, `approvalPolicy: ${botPolicyState.approvalPolicy}`, `model: ${formatModelLabel(providerSelection.model)}`, `reasoning: ${formatReasoningLabel(providerSelection.reasoningEffort)}`, `speed: ${formatFastModeLabel(providerSelection.serviceTier)}`, "", "Policy quick guide:", "Safe = read-only + approval prompts", "Standard = workspace write + approval prompts", "Semi Auto = workspace write + ask on failures", "Full = no approval prompts", "All actions start from this agent workspace.", "Use the main menu for Model & Reasoning and Speed.", "", "Choose an action:");
796
929
  await editMessageOrReply(ctx, lines.join("\n"), {
797
930
  reply_markup: {
798
- inline_keyboard: buildBotActionsKeyboard(sessionId, index, {
799
- modelOptions,
800
- currentModel,
801
- }),
931
+ inline_keyboard: buildBotActionsKeyboard(sessionId, index),
802
932
  },
803
933
  });
804
934
  }
@@ -810,7 +940,7 @@ async function renderResetConfirm(ctx, { sessionId, index, botId }) {
810
940
  { text: "Confirm reset", callback_data: `hub:rc:${sessionId}:${index}` },
811
941
  { text: "Cancel", callback_data: `hub:o:${sessionId}:${index}` },
812
942
  ],
813
- [{ text: "Back to bots", callback_data: "hub:back" }],
943
+ [{ text: "Back to agents", callback_data: `hub:ag:${sessionId}` }],
814
944
  ],
815
945
  },
816
946
  });
@@ -823,12 +953,22 @@ async function renderDeleteConfirm(ctx, { sessionId, index, botId }) {
823
953
  { text: "Confirm delete", callback_data: `hub:dc:${sessionId}:${index}` },
824
954
  { text: "Cancel", callback_data: `hub:o:${sessionId}:${index}` },
825
955
  ],
826
- [{ text: "Back to bots", callback_data: "hub:back" }],
956
+ [{ text: "Back to agents", callback_data: `hub:ag:${sessionId}` }],
827
957
  ],
828
958
  },
829
959
  });
830
960
  }
831
961
  function buildBotsMenuKeyboard(sessionId, bots) {
962
+ const hasBots = bots.length > 0;
963
+ return [
964
+ [{ text: "Agents", callback_data: `hub:ag:${sessionId}` }],
965
+ [{ text: "Model & Reasoning", callback_data: `hub:ga:${sessionId}` }],
966
+ [{ text: "Speed", callback_data: `hub:gf:${sessionId}` }],
967
+ [{ text: "Create agent", callback_data: "hub:create" }],
968
+ ...(hasBots ? [[{ text: "Refresh", callback_data: `hub:r:${sessionId}` }]] : []),
969
+ ];
970
+ }
971
+ function buildAgentsMenuKeyboard(sessionId, bots) {
832
972
  const rows = [];
833
973
  for (let index = 0; index < bots.length; index += 1) {
834
974
  const botState = bots[index];
@@ -840,9 +980,8 @@ function buildBotsMenuKeyboard(sessionId, bots) {
840
980
  { text: `${botState.id} (${status})`, callback_data: `hub:o:${sessionId}:${index}` },
841
981
  ]);
842
982
  }
843
- rows.push([{ text: "Refresh", callback_data: `hub:r:${sessionId}` }]);
844
- rows.push([{ text: "Set model for all", callback_data: `hub:ga:${sessionId}` }]);
845
- rows.push([{ text: "Create agent", callback_data: "hub:create" }]);
983
+ rows.push([{ text: "Refresh", callback_data: `hub:ag:${sessionId}` }]);
984
+ rows.push([{ text: "Back to menu", callback_data: "hub:back" }]);
846
985
  return rows;
847
986
  }
848
987
  async function renderGlobalModelMenu(ctx, { sessionId = "", editMessage = false, runtime = null, notice = "", } = {}) {
@@ -861,7 +1000,7 @@ async function renderGlobalModelMenu(ctx, { sessionId = "", editMessage = false,
861
1000
  if (!session) {
862
1001
  await renderBotsMenu(ctx, {
863
1002
  editMessage,
864
- notice: "Menu expired. Open /set_model_all again.",
1003
+ notice: "Menu expired. Open Model & Reasoning again.",
865
1004
  });
866
1005
  return;
867
1006
  }
@@ -872,22 +1011,18 @@ async function renderGlobalModelMenu(ctx, { sessionId = "", editMessage = false,
872
1011
  catalog: modelCatalog.models,
873
1012
  currentModel,
874
1013
  });
1014
+ clearPendingProviderSelection(session);
875
1015
  session.modelOptions = modelOptions;
1016
+ session.reasoningOptions = [];
876
1017
  menuSessions.set(activeSessionId, session);
877
1018
  const lines = [];
878
1019
  if (notice) {
879
1020
  lines.push(notice, "");
880
1021
  }
881
- lines.push(hubIncluded ? "Global model for all agents and hub:" : "Global model for all agents:");
1022
+ lines.push("Model & Reasoning:");
882
1023
  lines.push(`agents: ${bots.length}`);
883
- if (hubIncluded) {
884
- lines.push("hub: included");
885
- }
886
- lines.push(`current: ${sharedModel.mode === "uniform"
887
- ? formatModelLabel(sharedModel.model)
888
- : hubIncluded
889
- ? "mixed (agents and hub use different models)"
890
- : "mixed (agents use different models)"}`);
1024
+ lines.push(`hub available: ${hubIncluded ? "yes" : "no"}`);
1025
+ lines.push(`current model: ${sharedModel.mode === "uniform" ? formatModelLabel(sharedModel.model) : "mixed"}`);
891
1026
  lines.push(modelCatalog.available
892
1027
  ? `available models: ${modelCatalog.models.length}`
893
1028
  : "available models: unavailable right now");
@@ -911,9 +1046,66 @@ async function renderGlobalModelMenu(ctx, { sessionId = "", editMessage = false,
911
1046
  },
912
1047
  });
913
1048
  }
914
- async function renderSetModelAgentMenu(ctx, { sessionId = "", editMessage = false, notice = "", } = {}) {
1049
+ async function renderGlobalReasoningMenu(ctx, { sessionId, runtime = null, modelSelection, editMessage = true, notice = "", }) {
915
1050
  const bots = await fetchBots();
916
- if (bots.length === 0) {
1051
+ const hubIncluded = isHubModelControlAvailable(runtime);
1052
+ if (bots.length === 0 && !hubIncluded) {
1053
+ await renderBotsMenu(ctx, {
1054
+ editMessage,
1055
+ notice: notice || "No agents found.",
1056
+ });
1057
+ return;
1058
+ }
1059
+ const session = getMenuSession(sessionId, ctx);
1060
+ if (!session) {
1061
+ await renderBotsMenu(ctx, {
1062
+ editMessage,
1063
+ notice: "Menu expired. Open Model & Reasoning again.",
1064
+ });
1065
+ return;
1066
+ }
1067
+ const sharedReasoning = resolveSharedReasoningForGlobalTargets(bots, runtime);
1068
+ const currentModel = resolveSharedModelForGlobalTargets(bots, runtime);
1069
+ const reasoningOptions = buildReasoningOptionsForModel({
1070
+ modelSelection,
1071
+ currentModel: currentModel.mode === "uniform" && currentModel.model === modelSelection.model
1072
+ ? currentModel.model
1073
+ : null,
1074
+ currentReasoningEffort: sharedReasoning.mode === "uniform" ? sharedReasoning.reasoningEffort : null,
1075
+ });
1076
+ session.reasoningOptions = reasoningOptions;
1077
+ menuSessions.set(sessionId, session);
1078
+ const lines = [];
1079
+ if (notice) {
1080
+ lines.push(notice, "");
1081
+ }
1082
+ lines.push("Model & Reasoning:");
1083
+ lines.push(`model: ${modelSelection.label}`);
1084
+ lines.push(`current reasoning: ${sharedReasoning.mode === "uniform"
1085
+ ? formatReasoningLabel(sharedReasoning.reasoningEffort)
1086
+ : "mixed"}`);
1087
+ lines.push("", "Choose the reasoning level:");
1088
+ const keyboard = buildGlobalReasoningKeyboard(sessionId, modelSelection.key, {
1089
+ reasoningOptions,
1090
+ });
1091
+ if (editMessage) {
1092
+ await editMessageOrReply(ctx, lines.join("\n"), {
1093
+ reply_markup: {
1094
+ inline_keyboard: keyboard,
1095
+ },
1096
+ });
1097
+ return;
1098
+ }
1099
+ await ctx.reply(lines.join("\n"), {
1100
+ reply_markup: {
1101
+ inline_keyboard: keyboard,
1102
+ },
1103
+ });
1104
+ }
1105
+ async function renderGlobalFastMenu(ctx, { sessionId = "", editMessage = false, runtime = null, notice = "", } = {}) {
1106
+ const bots = await fetchBots();
1107
+ const hubIncluded = isHubModelControlAvailable(runtime);
1108
+ if (bots.length === 0 && !hubIncluded) {
917
1109
  await renderBotsMenu(ctx, {
918
1110
  editMessage,
919
1111
  notice: notice || "No agents found.",
@@ -926,18 +1118,29 @@ async function renderSetModelAgentMenu(ctx, { sessionId = "", editMessage = fals
926
1118
  if (!session) {
927
1119
  await renderBotsMenu(ctx, {
928
1120
  editMessage,
929
- notice: "Menu expired. Open /set_model again.",
1121
+ notice: "Menu expired. Open /bots again.",
930
1122
  });
931
1123
  return;
932
1124
  }
1125
+ const sharedServiceTier = resolveSharedServiceTierForGlobalTargets(bots, runtime);
1126
+ clearPendingProviderSelection(session);
1127
+ session.reasoningOptions = [];
1128
+ menuSessions.set(activeSessionId, session);
933
1129
  const lines = [];
934
1130
  if (notice) {
935
1131
  lines.push(notice, "");
936
1132
  }
937
- lines.push("Set model: choose an agent.");
1133
+ lines.push("Speed:");
938
1134
  lines.push(`agents: ${bots.length}`);
939
- lines.push("", "Pick one:");
940
- const keyboard = buildSetModelAgentKeyboard(activeSessionId, bots);
1135
+ lines.push(`hub available: ${hubIncluded ? "yes" : "no"}`);
1136
+ lines.push(`current speed: ${sharedServiceTier.mode === "uniform"
1137
+ ? formatFastModeLabel(sharedServiceTier.serviceTier)
1138
+ : "mixed"}`);
1139
+ lines.push("", "Choose the speed mode:");
1140
+ const keyboard = buildGlobalFastKeyboard(activeSessionId, {
1141
+ serviceTier: sharedServiceTier.mode === "uniform" ? sharedServiceTier.serviceTier : null,
1142
+ hasMixedSelection: sharedServiceTier.mode === "mixed",
1143
+ });
941
1144
  if (editMessage) {
942
1145
  await editMessageOrReply(ctx, lines.join("\n"), {
943
1146
  reply_markup: {
@@ -952,52 +1155,80 @@ async function renderSetModelAgentMenu(ctx, { sessionId = "", editMessage = fals
952
1155
  },
953
1156
  });
954
1157
  }
955
- async function renderBotModelMenu(ctx, { sessionId, index, editMessage = true, notice = "", }) {
1158
+ async function renderProviderTargetMenu(ctx, { sessionId, runtime = null, editMessage = true, notice = "", }) {
1159
+ const bots = await fetchBots();
956
1160
  const session = getMenuSession(sessionId, ctx);
957
- const botId = getBotIdFromSession(session, index);
958
- if (!botId) {
959
- await renderSetModelAgentMenu(ctx, {
960
- sessionId,
961
- editMessage: true,
962
- notice: "Agent not found. Refreshed.",
1161
+ if (!session || !session.pendingProviderPatch || !session.pendingFlow) {
1162
+ await renderBotsMenu(ctx, {
1163
+ editMessage,
1164
+ notice: "Selection expired. Open the menu again.",
963
1165
  });
964
1166
  return;
965
1167
  }
966
- const botState = await fetchBotById(botId);
967
- if (!botState) {
968
- await renderSetModelAgentMenu(ctx, {
969
- sessionId,
970
- editMessage: true,
971
- notice: `Agent '${botId}' not found.`,
1168
+ session.botIds = bots.map((entry) => String(entry?.id ?? "").trim()).filter(Boolean);
1169
+ menuSessions.set(sessionId, session);
1170
+ const hubIncluded = isHubModelControlAvailable(runtime);
1171
+ const lines = [];
1172
+ if (notice) {
1173
+ lines.push(notice, "");
1174
+ }
1175
+ lines.push(session.pendingFlow === "speed" ? "Speed" : "Model & Reasoning");
1176
+ if (session.pendingSummary.modelLabel) {
1177
+ lines.push(`model: ${session.pendingSummary.modelLabel}`);
1178
+ }
1179
+ if (session.pendingSummary.reasoningLabel) {
1180
+ lines.push(`reasoning: ${session.pendingSummary.reasoningLabel}`);
1181
+ }
1182
+ if (session.pendingSummary.speedLabel) {
1183
+ lines.push(`speed: ${session.pendingSummary.speedLabel}`);
1184
+ }
1185
+ lines.push("", "Choose where to apply it:");
1186
+ const keyboard = buildProviderTargetKeyboard(sessionId, {
1187
+ hubIncluded,
1188
+ backCallbackData: session.pendingFlow === "speed" ? `hub:gf:${sessionId}` : `hub:ga:${sessionId}`,
1189
+ });
1190
+ if (editMessage) {
1191
+ await editMessageOrReply(ctx, lines.join("\n"), {
1192
+ reply_markup: {
1193
+ inline_keyboard: keyboard,
1194
+ },
972
1195
  });
973
1196
  return;
974
1197
  }
975
- const providerOptions = botState?.provider?.options && typeof botState.provider.options === "object"
976
- ? botState.provider.options
977
- : {};
978
- const currentModel = String(providerOptions.model ?? "").trim();
979
- const modelCatalog = await fetchCodexModelOptions(apiGet);
980
- const modelOptions = buildSessionModelOptions({
981
- catalog: modelCatalog.models,
982
- currentModel,
1198
+ await ctx.reply(lines.join("\n"), {
1199
+ reply_markup: {
1200
+ inline_keyboard: keyboard,
1201
+ },
983
1202
  });
984
- if (session) {
985
- session.modelOptions = modelOptions;
986
- menuSessions.set(sessionId, session);
1203
+ }
1204
+ async function renderProviderTargetAgentMenu(ctx, { sessionId, editMessage = true, notice = "", }) {
1205
+ const bots = await fetchBots();
1206
+ const session = getMenuSession(sessionId, ctx);
1207
+ if (!session || !session.pendingProviderPatch || !session.pendingFlow) {
1208
+ await renderBotsMenu(ctx, {
1209
+ editMessage,
1210
+ notice: "Selection expired. Open the menu again.",
1211
+ });
1212
+ return;
987
1213
  }
1214
+ session.botIds = bots.map((entry) => String(entry?.id ?? "").trim()).filter(Boolean);
1215
+ menuSessions.set(sessionId, session);
988
1216
  const lines = [];
989
1217
  if (notice) {
990
1218
  lines.push(notice, "");
991
1219
  }
992
- lines.push(`Agent '${botId}' model:`);
993
- lines.push(`current: ${formatModelLabel(currentModel)}`);
994
- lines.push(modelCatalog.available
995
- ? `available models: ${modelCatalog.models.length}`
996
- : "available models: unavailable right now");
997
- lines.push("", "Choose a model:");
998
- const keyboard = buildBotModelKeyboard(sessionId, index, {
999
- modelOptions,
1000
- currentModel,
1220
+ lines.push("Choose one agent:");
1221
+ if (session.pendingSummary.modelLabel) {
1222
+ lines.push(`model: ${session.pendingSummary.modelLabel}`);
1223
+ }
1224
+ if (session.pendingSummary.reasoningLabel) {
1225
+ lines.push(`reasoning: ${session.pendingSummary.reasoningLabel}`);
1226
+ }
1227
+ if (session.pendingSummary.speedLabel) {
1228
+ lines.push(`speed: ${session.pendingSummary.speedLabel}`);
1229
+ }
1230
+ const keyboard = buildProviderTargetAgentKeyboard(sessionId, bots, {
1231
+ backCallbackData: `hub:tt:${sessionId}:single`,
1001
1232
  });
1002
1233
  if (editMessage) {
1003
1234
  await editMessageOrReply(ctx, lines.join("\n"), {
@@ -1013,8 +1244,26 @@ async function renderBotModelMenu(ctx, { sessionId, index, editMessage = true, n
1013
1244
  },
1014
1245
  });
1015
1246
  }
1016
- function buildBotActionsKeyboard(sessionId, index, { modelOptions = [], currentModel = "", } = {}) {
1017
- const rows = [
1247
+ function setPendingProviderSelection(session, { patch, flow, modelLabel = null, reasoningLabel = null, speedLabel = null, }) {
1248
+ session.pendingProviderPatch = { ...patch };
1249
+ session.pendingFlow = flow;
1250
+ session.pendingSummary = {
1251
+ modelLabel,
1252
+ reasoningLabel,
1253
+ speedLabel,
1254
+ };
1255
+ }
1256
+ function clearPendingProviderSelection(session) {
1257
+ session.pendingProviderPatch = null;
1258
+ session.pendingFlow = null;
1259
+ session.pendingSummary = {
1260
+ modelLabel: null,
1261
+ reasoningLabel: null,
1262
+ speedLabel: null,
1263
+ };
1264
+ }
1265
+ function buildBotActionsKeyboard(sessionId, index) {
1266
+ return [
1018
1267
  [
1019
1268
  { text: "Safe (read-only)", callback_data: `hub:p:${sessionId}:${index}:safe` },
1020
1269
  { text: "Standard (ask)", callback_data: `hub:p:${sessionId}:${index}:standard` },
@@ -1023,101 +1272,98 @@ function buildBotActionsKeyboard(sessionId, index, { modelOptions = [], currentM
1023
1272
  { text: "Semi (fail ask)", callback_data: `hub:p:${sessionId}:${index}:semi_auto` },
1024
1273
  { text: "Full (no prompts)", callback_data: `hub:p:${sessionId}:${index}:full_auto` },
1025
1274
  ],
1275
+ [{ text: "Reset Context", callback_data: `hub:ra:${sessionId}:${index}` }],
1276
+ [{ text: "Delete Agent", callback_data: `hub:da:${sessionId}:${index}` }],
1277
+ [{ text: "Back to agents", callback_data: `hub:ag:${sessionId}` }],
1278
+ ];
1279
+ }
1280
+ function buildGlobalModelKeyboard(sessionId, { modelOptions = [], currentModel = "", hasMixedSelection = false, } = {}) {
1281
+ const rows = [
1026
1282
  [
1027
1283
  {
1028
- text: formatModelButtonText("Auto", currentModel === ""),
1029
- callback_data: `hub:m:${sessionId}:${index}:auto`,
1284
+ text: formatModelButtonText("Auto", !hasMixedSelection && currentModel === ""),
1285
+ callback_data: `hub:gm:${sessionId}:auto`,
1030
1286
  },
1031
1287
  ],
1032
1288
  ];
1033
1289
  for (const option of modelOptions) {
1034
- const isCurrent = String(currentModel ?? "")
1035
- .trim()
1036
- .toLowerCase() ===
1037
- String(option.model ?? "")
1290
+ const isCurrent = !hasMixedSelection &&
1291
+ String(currentModel ?? "")
1038
1292
  .trim()
1039
- .toLowerCase();
1293
+ .toLowerCase() ===
1294
+ String(option.model ?? "")
1295
+ .trim()
1296
+ .toLowerCase();
1040
1297
  rows.push([
1041
1298
  {
1042
1299
  text: formatModelButtonText(option.label, isCurrent),
1043
- callback_data: `hub:m:${sessionId}:${index}:${option.key}`,
1300
+ callback_data: `hub:gm:${sessionId}:${option.key}`,
1044
1301
  },
1045
1302
  ]);
1046
1303
  }
1047
- rows.push([{ text: "Reset Context", callback_data: `hub:ra:${sessionId}:${index}` }], [{ text: "Delete Agent", callback_data: `hub:da:${sessionId}:${index}` }], [{ text: "Back to bots", callback_data: "hub:back" }]);
1304
+ rows.push([{ text: "Back to menu", callback_data: "hub:back" }]);
1048
1305
  return rows;
1049
1306
  }
1050
- function buildSetModelAgentKeyboard(sessionId, bots) {
1307
+ function buildGlobalReasoningKeyboard(sessionId, modelProfileId, { reasoningOptions = [] } = {}) {
1051
1308
  const rows = [];
1052
- for (let index = 0; index < bots.length; index += 1) {
1053
- const botState = bots[index];
1054
- if (!botState) {
1055
- continue;
1056
- }
1057
- const status = botState.running ? "ON" : "OFF";
1309
+ for (const option of reasoningOptions) {
1058
1310
  rows.push([
1059
1311
  {
1060
- text: `${botState.id} (${status})`,
1061
- callback_data: `hub:mo:${sessionId}:${index}`,
1312
+ text: formatModelButtonText(option.label, option.selected === true),
1313
+ callback_data: `hub:gr:${sessionId}:${modelProfileId}:${option.key}`,
1062
1314
  },
1063
1315
  ]);
1064
1316
  }
1065
- rows.push([{ text: "Refresh", callback_data: `hub:sm:${sessionId}` }]);
1066
- rows.push([{ text: "Back to bots", callback_data: "hub:back" }]);
1317
+ rows.push([{ text: "Back to model", callback_data: `hub:ga:${sessionId}` }]);
1318
+ rows.push([{ text: "Back to menu", callback_data: "hub:back" }]);
1067
1319
  return rows;
1068
1320
  }
1069
- function buildBotModelKeyboard(sessionId, index, { modelOptions = [], currentModel = "", } = {}) {
1070
- const rows = [
1321
+ function buildGlobalFastKeyboard(sessionId, { serviceTier = null, hasMixedSelection = false, } = {}) {
1322
+ return [
1071
1323
  [
1072
1324
  {
1073
- text: formatModelButtonText("Auto", currentModel === ""),
1074
- callback_data: `hub:mm:${sessionId}:${index}:auto`,
1325
+ text: formatModelButtonText("Standard", !hasMixedSelection && serviceTier !== "fast"),
1326
+ callback_data: `hub:gft:${sessionId}:standard`,
1075
1327
  },
1076
1328
  ],
1077
- ];
1078
- for (const option of modelOptions) {
1079
- const isCurrent = String(currentModel ?? "")
1080
- .trim()
1081
- .toLowerCase() ===
1082
- String(option.model ?? "")
1083
- .trim()
1084
- .toLowerCase();
1085
- rows.push([
1086
- {
1087
- text: formatModelButtonText(option.label, isCurrent),
1088
- callback_data: `hub:mm:${sessionId}:${index}:${option.key}`,
1089
- },
1090
- ]);
1091
- }
1092
- rows.push([{ text: "Back to agents", callback_data: `hub:sm:${sessionId}` }]);
1093
- rows.push([{ text: "Back to bots", callback_data: "hub:back" }]);
1094
- return rows;
1095
- }
1096
- function buildGlobalModelKeyboard(sessionId, { modelOptions = [], currentModel = "", hasMixedSelection = false, } = {}) {
1097
- const rows = [
1098
1329
  [
1099
1330
  {
1100
- text: formatModelButtonText("Auto", !hasMixedSelection && currentModel === ""),
1101
- callback_data: `hub:gm:${sessionId}:auto`,
1331
+ text: formatModelButtonText("Fast", !hasMixedSelection && serviceTier === "fast"),
1332
+ callback_data: `hub:gft:${sessionId}:fast`,
1102
1333
  },
1103
1334
  ],
1335
+ [{ text: "Back to menu", callback_data: "hub:back" }],
1104
1336
  ];
1105
- for (const option of modelOptions) {
1106
- const isCurrent = !hasMixedSelection &&
1107
- String(currentModel ?? "")
1108
- .trim()
1109
- .toLowerCase() ===
1110
- String(option.model ?? "")
1111
- .trim()
1112
- .toLowerCase();
1337
+ }
1338
+ function buildProviderTargetKeyboard(sessionId, { hubIncluded, backCallbackData, }) {
1339
+ const rows = [
1340
+ [{ text: "One agent", callback_data: `hub:tt:${sessionId}:single` }],
1341
+ [{ text: "All agents", callback_data: `hub:tt:${sessionId}:all` }],
1342
+ ];
1343
+ if (hubIncluded) {
1344
+ rows.push([{ text: "All agents + hub", callback_data: `hub:tt:${sessionId}:all_hub` }]);
1345
+ }
1346
+ rows.push([{ text: "Back", callback_data: backCallbackData }]);
1347
+ rows.push([{ text: "Back to menu", callback_data: "hub:back" }]);
1348
+ return rows;
1349
+ }
1350
+ function buildProviderTargetAgentKeyboard(sessionId, bots, { backCallbackData, }) {
1351
+ const rows = [];
1352
+ for (let index = 0; index < bots.length; index += 1) {
1353
+ const botState = bots[index];
1354
+ if (!botState) {
1355
+ continue;
1356
+ }
1357
+ const status = botState.running ? "ON" : "OFF";
1113
1358
  rows.push([
1114
1359
  {
1115
- text: formatModelButtonText(option.label, isCurrent),
1116
- callback_data: `hub:gm:${sessionId}:${option.key}`,
1360
+ text: `${botState.id} (${status})`,
1361
+ callback_data: `hub:ta:${sessionId}:${index}`,
1117
1362
  },
1118
1363
  ]);
1119
1364
  }
1120
- rows.push([{ text: "Back to bots", callback_data: "hub:back" }]);
1365
+ rows.push([{ text: "Back", callback_data: backCallbackData }]);
1366
+ rows.push([{ text: "Back to menu", callback_data: "hub:back" }]);
1121
1367
  return rows;
1122
1368
  }
1123
1369
  function createMenuSession(chatId, bots) {
@@ -1137,6 +1383,14 @@ function createMenuSession(chatId, bots) {
1137
1383
  createdAt: Date.now(),
1138
1384
  botIds: bots.map((entry) => String(entry?.id ?? "").trim()).filter(Boolean),
1139
1385
  modelOptions: [],
1386
+ reasoningOptions: [],
1387
+ pendingProviderPatch: null,
1388
+ pendingFlow: null,
1389
+ pendingSummary: {
1390
+ modelLabel: null,
1391
+ reasoningLabel: null,
1392
+ speedLabel: null,
1393
+ },
1140
1394
  });
1141
1395
  return sessionId;
1142
1396
  }
@@ -1200,22 +1454,27 @@ function parseMenuAction(rawData) {
1200
1454
  sessionId,
1201
1455
  };
1202
1456
  }
1203
- if (kind === "sm" && parts.length === 3) {
1457
+ if (kind === "gf" && parts.length === 3) {
1458
+ const sessionId = parts[2];
1459
+ if (!sessionId) {
1460
+ return null;
1461
+ }
1462
+ return {
1463
+ type: "global_fast_open",
1464
+ sessionId,
1465
+ };
1466
+ }
1467
+ if (kind === "ag" && parts.length === 3) {
1204
1468
  const sessionId = parts[2];
1205
1469
  if (!sessionId) {
1206
1470
  return null;
1207
1471
  }
1208
1472
  return {
1209
- type: "model_home",
1473
+ type: "agents_home",
1210
1474
  sessionId,
1211
1475
  };
1212
1476
  }
1213
- if ((kind === "o" ||
1214
- kind === "mo" ||
1215
- kind === "ra" ||
1216
- kind === "rc" ||
1217
- kind === "da" ||
1218
- kind === "dc") &&
1477
+ if ((kind === "o" || kind === "ra" || kind === "rc" || kind === "da" || kind === "dc") &&
1219
1478
  parts.length === 4) {
1220
1479
  const sessionId = parts[2];
1221
1480
  if (!sessionId) {
@@ -1227,7 +1486,6 @@ function parseMenuAction(rawData) {
1227
1486
  }
1228
1487
  const mapping = {
1229
1488
  o: "open",
1230
- mo: "model_open",
1231
1489
  ra: "reset_ask",
1232
1490
  rc: "reset_confirm",
1233
1491
  da: "delete_ask",
@@ -1243,7 +1501,7 @@ function parseMenuAction(rawData) {
1243
1501
  index,
1244
1502
  };
1245
1503
  }
1246
- if ((kind === "p" || kind === "m" || kind === "mm") && parts.length === 5) {
1504
+ if (kind === "p" && parts.length === 5) {
1247
1505
  const index = parseMenuIndex(parts[3]);
1248
1506
  const profileId = String(parts[4] ?? "")
1249
1507
  .trim()
@@ -1256,7 +1514,7 @@ function parseMenuAction(rawData) {
1256
1514
  return null;
1257
1515
  }
1258
1516
  return {
1259
- type: kind === "p" ? "policy" : kind === "m" ? "model" : "model_apply",
1517
+ type: "policy",
1260
1518
  sessionId,
1261
1519
  index,
1262
1520
  profileId,
@@ -1276,6 +1534,64 @@ function parseMenuAction(rawData) {
1276
1534
  profileId,
1277
1535
  };
1278
1536
  }
1537
+ if (kind === "gft" && parts.length === 4) {
1538
+ const sessionId = parts[2];
1539
+ const profileId = String(parts[3] ?? "")
1540
+ .trim()
1541
+ .toLowerCase();
1542
+ if (!sessionId || !profileId) {
1543
+ return null;
1544
+ }
1545
+ return {
1546
+ type: "global_fast_apply",
1547
+ sessionId,
1548
+ profileId,
1549
+ };
1550
+ }
1551
+ if (kind === "tt" && parts.length === 4) {
1552
+ const sessionId = parts[2];
1553
+ const profileId = String(parts[3] ?? "")
1554
+ .trim()
1555
+ .toLowerCase();
1556
+ if (!sessionId || !profileId) {
1557
+ return null;
1558
+ }
1559
+ return {
1560
+ type: "target_choice",
1561
+ sessionId,
1562
+ profileId,
1563
+ };
1564
+ }
1565
+ if (kind === "ta" && parts.length === 4) {
1566
+ const sessionId = parts[2];
1567
+ const index = parseMenuIndex(parts[3]);
1568
+ if (!sessionId || index === null) {
1569
+ return null;
1570
+ }
1571
+ return {
1572
+ type: "target_agent_apply",
1573
+ sessionId,
1574
+ index,
1575
+ };
1576
+ }
1577
+ if (kind === "gr" && parts.length === 5) {
1578
+ const sessionId = parts[2];
1579
+ const modelProfileId = String(parts[3] ?? "")
1580
+ .trim()
1581
+ .toLowerCase();
1582
+ const profileId = String(parts[4] ?? "")
1583
+ .trim()
1584
+ .toLowerCase();
1585
+ if (!sessionId || !modelProfileId || !profileId) {
1586
+ return null;
1587
+ }
1588
+ return {
1589
+ type: "global_reasoning_apply",
1590
+ sessionId,
1591
+ modelProfileId,
1592
+ profileId,
1593
+ };
1594
+ }
1279
1595
  return null;
1280
1596
  }
1281
1597
  function isHubModelControlAvailable(runtime) {
@@ -1284,24 +1600,38 @@ function isHubModelControlAvailable(runtime) {
1284
1600
  function resolveSharedModelForGlobalTargets(bots, runtime) {
1285
1601
  const models = bots.map((bot) => bot?.provider?.options?.model);
1286
1602
  if (runtime && typeof runtime.getProviderOptions === "function") {
1287
- models.push(getRuntimeModel(runtime));
1603
+ models.push(getRuntimeProviderSelection(runtime).model);
1288
1604
  }
1289
1605
  return resolveSharedModel(models);
1290
1606
  }
1291
- async function applyGlobalModelSelection({ bots, model, runtime, }) {
1607
+ function resolveSharedReasoningForGlobalTargets(bots, runtime) {
1608
+ const values = bots.map((bot) => bot?.provider?.options?.reasoningEffort);
1609
+ if (runtime && typeof runtime.getProviderOptions === "function") {
1610
+ values.push(getRuntimeProviderSelection(runtime).reasoningEffort);
1611
+ }
1612
+ return resolveSharedReasoningEffort(values);
1613
+ }
1614
+ function resolveSharedServiceTierForGlobalTargets(bots, runtime) {
1615
+ const values = bots.map((bot) => bot?.provider?.options?.serviceTier);
1616
+ if (runtime && typeof runtime.getProviderOptions === "function") {
1617
+ values.push(getRuntimeProviderSelection(runtime).serviceTier);
1618
+ }
1619
+ return resolveSharedServiceTier(values);
1620
+ }
1621
+ async function applyGlobalProviderSelection({ bots, patch, runtime, }) {
1292
1622
  const hubIncluded = isHubModelControlAvailable(runtime);
1293
- const botResult = await applyModelPolicyToBots({
1623
+ const botResult = await applyProviderPolicyToBots({
1294
1624
  apiPost,
1295
1625
  bots,
1296
- model,
1626
+ patch,
1297
1627
  });
1298
1628
  let hubError = null;
1299
1629
  let hubUpdated = false;
1300
1630
  if (hubIncluded) {
1301
1631
  try {
1302
- await applyRuntimeModelPolicy({
1632
+ await applyRuntimeProviderPolicy({
1303
1633
  runtime,
1304
- model,
1634
+ patch,
1305
1635
  });
1306
1636
  hubUpdated = true;
1307
1637
  }
@@ -1317,11 +1647,32 @@ async function applyGlobalModelSelection({ bots, model, runtime, }) {
1317
1647
  hubError,
1318
1648
  };
1319
1649
  }
1320
- function buildGlobalModelUpdateLines({ modelLabel, result, }) {
1650
+ function buildGlobalModelUpdateLines({ modelLabel, reasoningLabel, result, }) {
1651
+ const lines = [
1652
+ result.hubIncluded
1653
+ ? `Model updated for all agents and hub: ${modelLabel} / ${reasoningLabel}`
1654
+ : `Model updated for all agents: ${modelLabel} / ${reasoningLabel}`,
1655
+ `Updated: ${result.updatedCount}/${result.totalTargets}`,
1656
+ ];
1657
+ if (result.botFailures.length > 0 || result.hubError) {
1658
+ lines.push(`Warnings: ${result.botFailures.length + (result.hubError ? 1 : 0)}`);
1659
+ }
1660
+ if (result.botFailures.length > 0) {
1661
+ const failedIds = result.botFailures.map((entry) => entry.botId).filter(Boolean);
1662
+ if (failedIds.length > 0) {
1663
+ lines.push(`Failed agents: ${failedIds.join(", ")}`);
1664
+ }
1665
+ }
1666
+ if (result.hubError) {
1667
+ lines.push(`Hub warning: ${result.hubError}`);
1668
+ }
1669
+ return lines;
1670
+ }
1671
+ function buildGlobalFastUpdateLines({ fastLabel, result, }) {
1321
1672
  const lines = [
1322
1673
  result.hubIncluded
1323
- ? `Model updated for all agents and hub: ${modelLabel}`
1324
- : `Model updated for all agents: ${modelLabel}`,
1674
+ ? `Speed updated for all agents and hub: ${fastLabel}`
1675
+ : `Speed updated for all agents: ${fastLabel}`,
1325
1676
  `Updated: ${result.updatedCount}/${result.totalTargets}`,
1326
1677
  ];
1327
1678
  if (result.botFailures.length > 0 || result.hubError) {
@@ -1644,6 +1995,7 @@ async function watchCodexLoginCompletion({ ctx, runtime, flowKey, watcherToken,
1644
1995
  await ctx.reply("Codex login is still pending. Run /codex_status. Once succeeded, new turns use the new account quota.");
1645
1996
  }
1646
1997
  async function refreshHubProviderAfterCodexLogin(runtime) {
1998
+ invalidateCodexQuotaUsageCache();
1647
1999
  if (!runtime || typeof runtime.refreshProviderSession !== "function") {
1648
2000
  return "Account updated. Hub refresh is not available on this runtime.";
1649
2001
  }