openmausbot 0.1.97 → 0.1.98

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 (144) hide show
  1. package/dist/assets/ActivityPanel-H6D3SD39.js +1 -0
  2. package/dist/assets/BotSettingsDialog-Dqz_AiBZ.js +7 -0
  3. package/dist/assets/CloudBackendPicker-iezpJFpF.js +1 -0
  4. package/dist/assets/ComputerPanel-DZPj-IWu.js +1 -0
  5. package/dist/assets/DesktopViewer-utONms1p.js +5 -0
  6. package/dist/assets/InspectorPanel-OVaBVrzC.js +5 -0
  7. package/dist/assets/KeyboardShortcutsModal-gk9mvHj_.js +1 -0
  8. package/dist/assets/LocalVmWorkspace-DLIvd2kC.js +1 -0
  9. package/dist/assets/MacLocalControl-bJv8yiPB.js +1 -0
  10. package/dist/assets/NewBotDialog-1qGOAdUM.js +2 -0
  11. package/dist/assets/RemoteAgentSettingsPanel-DqKr0HkO.js +1 -0
  12. package/dist/assets/RoutinesPage-BM2D4qGi.js +1 -0
  13. package/dist/assets/RoutinesSection-Dny9ra-c.js +1 -0
  14. package/dist/assets/SettingsModal-gC6XJJmt.js +11 -0
  15. package/dist/assets/TeamMapPage-B6fuu4r_.js +8 -0
  16. package/dist/assets/TriggersPanel-j0qTP2CO.js +1 -0
  17. package/dist/assets/VisibilitySection-DT0FqPvu.js +12 -0
  18. package/dist/assets/WebhooksPanel--YQyCjKO.js +3 -0
  19. package/dist/assets/{abnfDiagram-O67JEVCF-k1UCNXb7.js → abnfDiagram-O67JEVCF-BsjcFX0-.js} +1 -1
  20. package/dist/assets/activity-BxOkayvD.js +1 -0
  21. package/dist/assets/{arc-BUWGa9UU.js → arc-CJNA94c6.js} +1 -1
  22. package/dist/assets/architectureDiagram-NJMV4G6O-dz2ud7lP.js +36 -0
  23. package/dist/assets/{blockDiagram-BEXU5L5S-Ag9nLW4H.js → blockDiagram-BEXU5L5S-ChBEALoR.js} +1 -1
  24. package/dist/assets/brain-CFZ7PEXc.js +1 -0
  25. package/dist/assets/{c4Diagram-YGBWAQC7-Bh0sjgUa.js → c4Diagram-YGBWAQC7-KZNRKDQe.js} +1 -1
  26. package/dist/assets/channel-WhFyujhZ.js +1 -0
  27. package/dist/assets/{chunk-2Q5K7J3B-BFBcIIqu.js → chunk-2Q5K7J3B-BLLwmCET.js} +1 -1
  28. package/dist/assets/{chunk-3FUC2YCW-Dvfpex-f.js → chunk-3FUC2YCW-CQxyreBZ.js} +2 -2
  29. package/dist/assets/{chunk-5VM5RSS4-CMjP2YZW.js → chunk-5VM5RSS4-D6sFMW0u.js} +1 -1
  30. package/dist/assets/{chunk-GTNCS2PH-BryoeTc7.js → chunk-GTNCS2PH-BaZ1YgLF.js} +1 -1
  31. package/dist/assets/{chunk-GWA4HPMP-BhfvcchO.js → chunk-GWA4HPMP-CcalphmC.js} +1 -1
  32. package/dist/assets/{chunk-JWPE2WC7-bcCVp8rp.js → chunk-JWPE2WC7-CSk8R3ve.js} +1 -1
  33. package/dist/assets/{chunk-NETBCI7D-yKIjIlkQ.js → chunk-NETBCI7D-CTmLjA57.js} +1 -1
  34. package/dist/assets/{chunk-WEXAMYUT-D3Zqbmup.js → chunk-WEXAMYUT-CM9Z77Ne.js} +1 -1
  35. package/dist/assets/{chunk-XXDRQBXY-CjVoIMG6.js → chunk-XXDRQBXY-B1fxpMS2.js} +1 -1
  36. package/dist/assets/{classDiagram-v2-NBCMYWYE-mUCWdVyH.js → classDiagram-v2-NBCMYWYE-DCCyoLpj.js} +1 -1
  37. package/dist/assets/computer-control-NLov352p.js +1 -0
  38. package/dist/assets/cose-bilkent-JH36ORCC-KyImBKvE.js +1 -0
  39. package/dist/assets/{cynefin-OW5HDTMX-BpRrxoO4.js → cynefin-OW5HDTMX-fsZc6MGa.js} +1 -1
  40. package/dist/assets/{cynefinDiagram-VND7K2PF-D3VBWCPK.js → cynefinDiagram-VND7K2PF-CGmHsShB.js} +1 -1
  41. package/dist/assets/{dagre-6A5THRUB-DxEIACOO.js → dagre-6A5THRUB-Dah1h-CH.js} +1 -1
  42. package/dist/assets/{diagram-22UHCM2B-BANgZEOS.js → diagram-22UHCM2B-BIu71OyV.js} +1 -1
  43. package/dist/assets/{diagram-3UASUU5V-Cnq7nJir.js → diagram-3UASUU5V-gxhUB_Mu.js} +1 -1
  44. package/dist/assets/{diagram-ATOU4E4O-4TBDIWnz.js → diagram-ATOU4E4O-CBhusUBn.js} +1 -1
  45. package/dist/assets/{diagram-CDSNMT55-CD_B39D4.js → diagram-CDSNMT55-BdPrc86x.js} +1 -1
  46. package/dist/assets/{diagram-MLGK6HIB-CzvnFhEC.js → diagram-MLGK6HIB-Clp110R_.js} +1 -1
  47. package/dist/assets/{diagram-MPIPVDR6-C4w2SNml.js → diagram-MPIPVDR6-DJKfbugw.js} +1 -1
  48. package/dist/assets/{ebnfDiagram-ZINNZB2B-gkUEBm2s.js → ebnfDiagram-ZINNZB2B-DnsltoBu.js} +1 -1
  49. package/dist/assets/{elk-276RUBZZ-B_56X6mw.js → elk-276RUBZZ-jWdq6gqH.js} +1 -1
  50. package/dist/assets/{erDiagram-OPXOYQCR-C_oByoDS.js → erDiagram-OPXOYQCR-DArPVf8u.js} +1 -1
  51. package/dist/assets/{flowDiagram-KWPJA3E3-CR9xXJkC.js → flowDiagram-KWPJA3E3-D2HZ-2f7.js} +1 -1
  52. package/dist/assets/ganttDiagram-FUAMR5RP--o0KIxEo.js +292 -0
  53. package/dist/assets/{gitGraphDiagram-X574FWY7-VJ6-Tp_p.js → gitGraphDiagram-X574FWY7-CsoXr2D7.js} +1 -1
  54. package/dist/assets/image-plus-BbVjGHin.js +1 -0
  55. package/dist/assets/index-BrPW5HDO.css +1 -0
  56. package/dist/assets/index-C3NqfdVU.js +585 -0
  57. package/dist/assets/{index-Cp4S4NOL.js → index-CJTp9JEp.js} +1 -1
  58. package/dist/assets/{infoDiagram-VRGFBTTK-2Lu9te4D.js → infoDiagram-VRGFBTTK-g-6lNp7r.js} +1 -1
  59. package/dist/assets/{ishikawaDiagram-OU5B5YK6-DR49XY-e.js → ishikawaDiagram-OU5B5YK6-B1d6qVTV.js} +1 -1
  60. package/dist/assets/{journeyDiagram-ZHPQQLJL-ZoR0M305.js → journeyDiagram-ZHPQQLJL-BTPuiU6T.js} +1 -1
  61. package/dist/assets/{kanban-definition-PNTS6WVX-DLKHxrO-.js → kanban-definition-PNTS6WVX-bDDuRJvP.js} +1 -1
  62. package/dist/assets/{linear-BASsfWly.js → linear-BVVKbEMy.js} +1 -1
  63. package/dist/assets/{mermaid.core-BVidiPkk.js → mermaid.core-JJdoKyKB.js} +5 -5
  64. package/dist/assets/{mindmap-definition-NLK3R4M7-g_Qx0U5C.js → mindmap-definition-NLK3R4M7-B4aE3T0q.js} +1 -1
  65. package/dist/assets/module-DCUsy2BQ.js +3 -0
  66. package/dist/assets/page-visible-BBo_Zm8g.js +1 -0
  67. package/dist/assets/{pegDiagram-GJSIUBJH-DI9Cjs8w.js → pegDiagram-GJSIUBJH-DFKQWpR7.js} +1 -1
  68. package/dist/assets/{pieDiagram-5QR66LMP-sRI0Uhr5.js → pieDiagram-5QR66LMP-Iv7OKVJi.js} +1 -1
  69. package/dist/assets/{quadrantDiagram-O4NWA36T-CamKTby3.js → quadrantDiagram-O4NWA36T-U2YnfwBk.js} +1 -1
  70. package/dist/assets/{railroadDiagram-XR7U4H2S-DOLlkbf3.js → railroadDiagram-XR7U4H2S-CuPy6RqM.js} +1 -1
  71. package/dist/assets/remote-desktop-panel-BNEj7tUx.js +1 -0
  72. package/dist/assets/{requirementDiagram-PLB6GJNP-DrRQ5r97.js → requirementDiagram-PLB6GJNP-bMI5AK7T.js} +1 -1
  73. package/dist/assets/{sankeyDiagram-IPEJSGJF-DGS_lXcX.js → sankeyDiagram-IPEJSGJF-BKcFE2WQ.js} +1 -1
  74. package/dist/assets/{sequenceDiagram-PO4LG4MO-CkjWqwtf.js → sequenceDiagram-PO4LG4MO-Bn9q88kl.js} +1 -1
  75. package/dist/assets/{sizeCapture-INFHLROL-CepRgful.js → sizeCapture-INFHLROL-Cvmdq9HD.js} +1 -1
  76. package/dist/assets/{stateDiagram-v2-GCMORJYK-hykr-9RS.js → stateDiagram-v2-GCMORJYK-JJQYw3eQ.js} +1 -1
  77. package/dist/assets/{swimlanes-2SLR337P-CKOHEZbI.js → swimlanes-2SLR337P-BkSCBEwp.js} +1 -1
  78. package/dist/assets/swimlanesDiagram-TC7HE7FX-CmTirbVT.js +8 -0
  79. package/dist/assets/table-file.worker-KkMx-SNx.js +10 -0
  80. package/dist/assets/{timeline-definition-EJHVYXUP-CB8pJ6AC.js → timeline-definition-EJHVYXUP-DsaM4GHr.js} +1 -1
  81. package/dist/assets/{usecaseDiagram-POWQR4AR-DrclJHdl.js → usecaseDiagram-POWQR4AR-3FOwLq7B.js} +1 -1
  82. package/dist/assets/user-DcZBnAuv.js +1 -0
  83. package/dist/assets/{vennDiagram-UO4OBE2U-zS23qjv8.js → vennDiagram-UO4OBE2U-DyNjJsKt.js} +1 -1
  84. package/dist/assets/{wardleyDiagram-VNRHLVJA-52jgeKxz.js → wardleyDiagram-VNRHLVJA-Y4ZMcnBC.js} +1 -1
  85. package/dist/assets/{xychartDiagram-PMCCYNJV-DSdl9rQ8.js → xychartDiagram-PMCCYNJV-BXZpg955.js} +1 -1
  86. package/dist/index.html +2 -2
  87. package/dist-server/cloud-home-start.js +19 -8
  88. package/dist-server/container-mcp.js +8 -0
  89. package/dist-server/desktop-entry.mjs +54 -0
  90. package/dist-server/drivers/agents-proxy.js +21 -4
  91. package/dist-server/index.js +1412 -821
  92. package/dist-server/openmausbot.js +837 -571
  93. package/dist-server/pair-cli.js +837 -571
  94. package/dist-server/permission-proxy.js +2 -0
  95. package/dist-server/server/atomic.js +70 -2
  96. package/dist-server/server/cloud-home-start.js +3 -3
  97. package/dist-server/server/config.js +22 -1
  98. package/dist-server/server/connector-card-text.js +15 -0
  99. package/dist-server/server/default-model-selection.js +7 -4
  100. package/dist-server/server/drivers/acp/core.js +10 -0
  101. package/dist-server/server/drivers/acp/grok.js +47 -5
  102. package/dist-server/server/drivers/agents-catalog.js +24 -4
  103. package/dist-server/server/drivers/agents-proxy.js +1 -0
  104. package/dist-server/server/drivers/chat-mcp-tools.js +5 -3
  105. package/dist-server/server/drivers/claude-login-auth.js +1 -1
  106. package/dist-server/server/drivers/claude.js +35 -11
  107. package/dist-server/server/drivers/codex-device-auth.js +27 -319
  108. package/dist-server/server/drivers/device-auth.js +342 -0
  109. package/dist-server/server/drivers/openai-chat.js +12 -4
  110. package/dist-server/server/fleet-agent.js +18 -5
  111. package/dist-server/server/harness/http.js +5 -2
  112. package/dist-server/server/harness/registry.js +19 -0
  113. package/dist-server/server/harness-capabilities.js +7 -4
  114. package/dist-server/server/hosted-models.js +3 -1
  115. package/dist-server/server/index.js +268 -54
  116. package/dist-server/server/key-rejections.js +57 -0
  117. package/dist-server/server/message-file.js +7 -4
  118. package/dist-server/server/permission-proxy.js +4 -0
  119. package/dist-server/server/provider-key-check.js +5 -1
  120. package/dist-server/server/request-auth.js +1 -1
  121. package/dist-server/server/room-turn-timeout.js +28 -0
  122. package/dist-server/server/routes/thread-models.js +36 -0
  123. package/dist-server/server/routines.js +2 -2
  124. package/dist-server/server/skills.js +3 -2
  125. package/dist-server/server/store.js +148 -60
  126. package/dist-server/server/team-backup.js +19 -4
  127. package/dist-server/server/thread-model.js +25 -0
  128. package/dist-server/server/turn-continuation.js +8 -2
  129. package/dist-server/server/webhook-ingress.js +8 -3
  130. package/dist-server/server/webhooks.js +40 -5
  131. package/dist-server/shared/device-sign-in.js +57 -0
  132. package/dist-server/shared/team-backup.js +2 -0
  133. package/dist-server/shared/thread-model.js +13 -0
  134. package/dist-server/vps-container-mcp.js +8 -0
  135. package/dist-server/workspace-backup.worker.js +41 -22
  136. package/package.json +1 -1
  137. package/dist/assets/DesktopViewer-CPidMCtt.js +0 -5
  138. package/dist/assets/architectureDiagram-NJMV4G6O-DiPor3UT.js +0 -36
  139. package/dist/assets/channel-DR8eD6QW.js +0 -1
  140. package/dist/assets/cose-bilkent-JH36ORCC-BBmR1bbX.js +0 -1
  141. package/dist/assets/ganttDiagram-FUAMR5RP-gtYBTu0B.js +0 -292
  142. package/dist/assets/index-CpaIXRLp.js +0 -614
  143. package/dist/assets/index-Z81SMsgR.css +0 -1
  144. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +0 -8
@@ -52,7 +52,7 @@ import { messageFileDisposition, messageFileDownloadName, messageAttachmentName,
52
52
  import { avatarGenerationRequestSchema, avatarGenerationStateMatches, generateAvatarImage, avatarImageStatus, snapshotAvatarGenerationState, } from "./avatar-image.js";
53
53
  import { fitsOnOneLine, parseBotProfilePatch } from "./bot-profile.js";
54
54
  import { groupTurnCwd } from "./room-cwd.js";
55
- import { RoomTurnDeadline, RoomTurnStallRegistry, roomTurnTimeoutMessage } from "./room-turn-timeout.js";
55
+ import { RoomTurnDeadline, RoomTurnStallRegistry, effectiveRoomTurnTimeoutMinutes, parseConversationTurnTimeout, roomTurnTimeoutMessage } from "./room-turn-timeout.js";
56
56
  import { roomTurnEnd } from "./room-turn-end.js";
57
57
  import { pickComputer } from "./computer-selection.js";
58
58
  import * as boat from "./boat.js";
@@ -67,6 +67,7 @@ import { liveCardKind, liveDecisionRefusal } from "../shared/live-approval.js";
67
67
  import { boatAccountResourceChangeError, cloudBackendChangeError, vpsAliasResourceChangeError, } from "./cloud-backend.js";
68
68
  import * as composio from "./composio.js";
69
69
  import { connectorCallFromFrame, connectorRefusalText, connectorUnrecognizedText, evaluateConnectorTools } from "./connector-verdict.js";
70
+ import { connectorCardText } from "./connector-card-text.js";
70
71
  import { chiefOfStaffSystemPrompt } from "./chief-of-staff.js";
71
72
  import { buildRecall } from "./recall.js";
72
73
  import { createMemoryUpkeep, upkeepEnabled } from "./memory-upkeep.js";
@@ -74,7 +75,7 @@ import { appendAboutMe, commitLearned, planLearned } from "./profile-learned.js"
74
75
  import { canAccessTeam, canReachPeer, coordinatorSupervises, livePeerRoster, livePeerRosterBlock, peerAllowed, peerName, peerRosterSystemPrompt, peerStatus, peerStatusWords, reachablePeers, resolveTeammate, roomPeerRosterSystemPrompt, roomRosterLine, PEER_ACCESS_HELP } from "./peer-roster.js";
75
76
  import { containerComputerAction, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, containerExec, containerRuntimeStatus, localVmWakeAction, localVmWorkspaceExists, perBotLocalVmTarget, poolLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, VM_WORKSPACE_GUEST, } from "./container-computer.js";
76
77
  import { attachForTurn, saveBotAttachment } from "./bot-attachment.js";
77
- import { cacheUntilConfigChanges, ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmIdleTimeoutMinutes, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, threadEventLogMaxBytes, maxConcurrentBotThreads, threadEventLogRetentionDays, saveConfig, showToolCallsEnabled, routinesInConversationEnabled, claudeUserMcpEnabled, skillAuthoringEnabled, autoRecallEnabled, captureQuietMs, tidyHour, sharedComputersEnabled, builtInBrowserEnabled, llmThreadTitlesEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, stripWorkspaceCredentialEnv, PROVIDER_CREDENTIAL_ENV, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, browserEngineAttachCdpUrl, DATA_DIR, skillsLibraryEnabled, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, CLAUDE_API_INSTANCE, liveSettingsFor, mergeOpenCodeProviderKeys, openCodeProviderKeys, } from "./config.js";
78
+ import { cacheUntilConfigChanges, ensureDirs, instanceConfigs, loadConfig, providerReloadKeys, localVmIdleTimeoutMinutes, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, mcpCallTimeoutMinutes, threadEventLogMaxBytes, maxConcurrentBotThreads, threadEventLogRetentionDays, saveConfig, showToolCallsEnabled, routinesInConversationEnabled, claudeUserMcpEnabled, skillAuthoringEnabled, autoRecallEnabled, captureQuietMs, tidyHour, sharedComputersEnabled, builtInBrowserEnabled, llmThreadTitlesEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, stripWorkspaceCredentialEnv, PROVIDER_CREDENTIAL_ENV, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, browserEngineAttachCdpUrl, DATA_DIR, skillsLibraryEnabled, EVENTS_DIR, NATIVE_DIR, customMcpServers, roomHandoffLimits, onConfigSaved, CLAUDE_API_INSTANCE, liveSettingsFor, mergeOpenCodeProviderKeys, openCodeProviderKeys, } from "./config.js";
78
79
  import { sweepThreadEventLogs } from "./thread-retention.js";
79
80
  import { ComputerControl } from "./computer-control.js";
80
81
  import { augmentedPath, findCliCandidates, resetPathCache } from "./env-path.js";
@@ -83,7 +84,8 @@ import { appendUsage, parseUsageRange, readUsage, summarizeUsage, usageCsv, USAG
83
84
  import { loadPlanUsage, planAccountsFromInstances } from "./plan-usage.js";
84
85
  import { GroupUsageReader } from "./group-thread-usage.js";
85
86
  import { ledgerCost } from "./model-prices.js";
86
- import { checkProviderKey, PROVIDER_KEY_KINDS } from "./provider-key-check.js";
87
+ import { checkProviderKey, PROVIDER_KEY_KINDS, providerBaseUrl } from "./provider-key-check.js";
88
+ import { forgetKey, noteKeyAccepted, noteKeyRejected, onKeyRejectionChange } from "./key-rejections.js";
87
89
  import { assertWithinBudget, noteSpend, spendAlertText, spendState, takeSpendAlert } from "./spend.js";
88
90
  import { fleetAvailable, fleetRequest, fleetSocketPath } from "./fleet-client.js";
89
91
  import { entitled } from "./enterprise.js";
@@ -126,12 +128,13 @@ import { createCloudMoveRoutes, workspaceShared } from "./cloud-move-http.js";
126
128
  import { RESTART_EXIT_CODE } from "./restart.js";
127
129
  import { holdIncludedServices } from "./included-services.js";
128
130
  import { selectDefaultModelSelection, withNewBotEffort } from "./default-model-selection.js";
131
+ import { threadModelFallback } from "./thread-model.js";
129
132
  import { computerEngineMoveText, removedComputerInstanceIds, writeComputerEngineMoveLines } from "./computer-engine-removal.js";
130
133
  import { cancelPeerApprovalsFor, cancelPeerApprovalsForThread, dismissStalePeerCards, peerApprovalFailure, requestPeerApproval, resolvePeerComms } from "./peer-approval.js";
131
134
  import { peerDeliveryReceipt } from "./peer-delivery.js";
132
135
  import { peerProvenanceNote, withPeerProvenance } from "./peer-provenance.js";
133
136
  import { decideRoomPost, emptyRoomPostBudget } from "./room-post-budget.js";
134
- import { isProjectEmoji, mentionedBots, parseConnectorTools, roomResponders, sectionKey, Store, titleFromLlm, toWireTask, } from "./store.js";
137
+ import { isProjectEmoji, mentionedBots, parseConnectorTools, roomResponders, sectionKey, Store, threadTitleFrom, titleFromLlm, toWireTask, } from "./store.js";
135
138
  import * as tts from "./tts/index.js";
136
139
  import { createDecider, deciderIncludedHere, deciderReady, deciderSavePatch, describeDecider } from "./decider/index.js";
137
140
  import { decideRoomResponder } from "./decider/room-routing.js";
@@ -244,6 +247,7 @@ import { createHostedSlackRoutes } from "./routes/hosted-slack.js";
244
247
  import { createBotPresetRoutes } from "./routes/bot-presets.js";
245
248
  import { createBotMemoryRoutes } from "./routes/bot-memory.js";
246
249
  import { createDeciderRoutes } from "./routes/decider.js";
250
+ import { createThreadModelRoutes } from "./routes/thread-models.js";
247
251
  import { createDesktopViewer, desktopViewerUrl } from "./routes/desktop-viewer.js";
248
252
  import { localDesktopTarget, localVmViewerStatus, viewerTargetId } from "./desktop-viewer-targets.js";
249
253
  import { createAntigravityLeftoverRoutes } from "./routes/antigravity-leftovers.js";
@@ -835,12 +839,13 @@ function openerFrom(fromThreadId) {
835
839
  return person;
836
840
  return person && !cloudOwnerPerson(person) ? person : CLOUD_NOBODY_KEY;
837
841
  }
838
- /** The model a thread a bot opens from one of its own threads starts on:
839
- * that thread's, not the bot default the person may have moved away from
840
- * (#1950). Only the bot's own thread counts — a room or a teammate's thread
841
- * is not one of its tasks, and another bot's model belongs to another engine.
842
- * Approval stays the bot's, except that a level the inherited engine would
843
- * have to confirm on a model switch starts the new thread in Ask. */
842
+ /** The model a thread a bot opens from one of its own threads starts on
843
+ * (#1950): the model a person picked in that thread, kept as the new thread's
844
+ * own. A thread that follows the bot opens one that follows it too. Only the
845
+ * bot's own thread counts — a room or a teammate's thread is not one of its
846
+ * tasks, and another bot's model belongs to another engine. Approval stays
847
+ * the bot's, except that a level the inherited engine would have to confirm
848
+ * on a model switch starts the new thread in Ask. */
844
849
  function parentThreadModel(botId, parentThreadId) {
845
850
  // The stored profile, never a per-thread projection: the comparison is
846
851
  // against the defaults the new thread would otherwise get.
@@ -1668,6 +1673,9 @@ const managedPolicy = new ManagedDesktopPolicy({ onChange: () => {
1668
1673
  broadcast({ kind: "config", ...configStatus() });
1669
1674
  resyncOpenCodeProviderKeys();
1670
1675
  } });
1676
+ // A key the provider refused, or accepted again, changes what Model
1677
+ // providers and the picker show; the config event refreshes /api/instances.
1678
+ onKeyRejectionChange(() => broadcast({ kind: "config", ...configStatus() }));
1671
1679
  /** A computer kind this server will not use, refused before anything is
1672
1680
  * prepared: a Cloud home never offers this computer or a Local VM
1673
1681
  * (cloud-home.ts), and an enrolled organisation may disallow any kind. */
@@ -2055,6 +2063,9 @@ function agentsIntegration(botId, threadId, depth, skillAuthoring, generation, r
2055
2063
  const speaking = botForThread(botId, threadId) ?? store.bot(botId);
2056
2064
  return tts.voiceReady(cfg, speaking?.voice) && speaking?.voiceNotes !== false ? "1" : "0";
2057
2065
  })(),
2066
+ // And for a role: team setup, bot creation and deletion, rooms and
2067
+ // retries are shown only to a Chief of Staff, whom their routes require.
2068
+ OMB_CHIEF_OF_STAFF: store.bot(botId)?.chiefOfStaff === true ? "1" : "0",
2058
2069
  },
2059
2070
  };
2060
2071
  }
@@ -3005,6 +3016,62 @@ function checkedTaskModelSwitch(current, raw, updateBotDefault, resetApprovalToA
3005
3016
  }
3006
3017
  return checked;
3007
3018
  }
3019
+ /** An engine as a turn's start sees it, without waiting on its CLI: the live
3020
+ * instance and what its last read said (registry.lastSnapshot). Before the
3021
+ * first read it counts as available, so only what is known stops a pick. */
3022
+ function threadEngine(instanceId) {
3023
+ const live = registry.get(instanceId);
3024
+ if (!live)
3025
+ return undefined;
3026
+ return {
3027
+ instanceId, driverKind: live.driverKind, enabled: live.enabled, models: live.models,
3028
+ capabilities: { modelVariants: live.adapter.capabilities.modelVariants === true },
3029
+ snapshot: registry.lastSnapshot(instanceId) ?? { state: "available" },
3030
+ access: providerConfigs()[instanceId]?.access ?? registry.access(instanceId),
3031
+ };
3032
+ }
3033
+ /** This thread's own model, when it gives way to its bot's now
3034
+ * (threadModelFallback); null when the thread follows its bot or its own
3035
+ * model runs as picked. */
3036
+ function threadModelGivingWay(botId, threadId) {
3037
+ const bot = store.bot(botId);
3038
+ const own = store.taskByThread(botId, threadId)?.modelSelection;
3039
+ if (!bot || !own)
3040
+ return null;
3041
+ return threadModelFallback(own, bot.modelSelection, threadEngine, {
3042
+ refusal: policyModelRefusal,
3043
+ ...(hostedModels ? { allows: (selection) => hostedModels.allows(selection) } : {}),
3044
+ }) ? own : null;
3045
+ }
3046
+ /** The model a turn in this thread would run on now (healThreadModel). */
3047
+ function turnSelection(botId, threadId) {
3048
+ return threadModelGivingWay(botId, threadId) ? store.bot(botId)?.modelSelection : store.projectBotForTask(botId, threadId)?.modelSelection;
3049
+ }
3050
+ /** A model as a person reads it: its catalog label, after its engine's name
3051
+ * while that engine is here. */
3052
+ function modelName(selection) {
3053
+ const instance = registry.get(selection.instanceId);
3054
+ const label = instance?.models.options.find((option) => option.id === selection.model)?.label ?? selection.model;
3055
+ return instance ? `${instance.displayName || instance.driverKind} · ${label}` : label;
3056
+ }
3057
+ /** A thread's own model that can't run here gives way to its bot's: the turn
3058
+ * about to start runs on the bot's model, the thread follows the bot from
3059
+ * then on, and one plain line in it says so. Every turn starts through
3060
+ * startTurn (a person's message, a routine, a delegation, a webhook, a
3061
+ * queued follow-up), so this is the one place it happens; a room turn
3062
+ * already runs on each member's own model. */
3063
+ function healThreadModel(botId, threadId) {
3064
+ const bot = store.bot(botId);
3065
+ const own = threadModelGivingWay(botId, threadId);
3066
+ if (!bot || !own || bot.approvalGrant || threadBusy(botId, threadId))
3067
+ return;
3068
+ if (!store.followBotModel(botId, [threadId], (from, to) => hostedModels?.resetTask(from, to) ?? {}).length)
3069
+ return;
3070
+ store.appendMessage(threadId, { role: "bot", kind: "activity", tool: {
3071
+ name: `notice: This thread's model (${modelName(own)}) isn't available, so it now uses ${bot.name}'s model (${modelName(bot.modelSelection)}).`,
3072
+ ok: true,
3073
+ } });
3074
+ }
3008
3075
  function checkedExportSkillNames(value, bots) {
3009
3076
  // Sharing instructions is explicit: ordinary package exports include none.
3010
3077
  if (value === undefined)
@@ -3201,15 +3268,27 @@ function checkedMemberIds(value) {
3201
3268
  return { ok: true, memberIds };
3202
3269
  }
3203
3270
  let bootSelection = { instanceId: "", model: "" };
3271
+ /** The engines read at start, while it runs (below). It is where the server
3272
+ * learns what each engine supports (a Claude CLI's version and flags), so a
3273
+ * turn waits for it, as every turn did when the server listened only after
3274
+ * that read. */
3275
+ let enginesRead = null;
3204
3276
  /** Preset bots for New bot, from shared files and the organization (presets.ts). */
3205
3277
  const presetStore = createPresetStore();
3206
- const store = new Store(() => bootSelection, (selection) => withNewBotEffort(selection, cfg.newBots?.effort, registry.get(selection.instanceId)?.adapter.capabilities.effortLevels));
3278
+ const store = new Store(() => bootSelection, (selection) => withNewBotEffort(selection, cfg.newBots?.effort, registry.get(selection.instanceId)?.adapter.capabilities.effortLevels), (instanceId) => registry.cliTarget(instanceId)?.driverKind);
3207
3279
  const teamComputers = new TeamComputers(join(DATA_DIR, "team-computers.json"), ENVIRONMENT_ID);
3208
3280
  let followupsReady = false;
3209
3281
  const sendSequencer = new SendSequencer();
3210
3282
  // Before the new-bot default is read: a saved default may name the engine.
3211
3283
  await retryComputerEngineMove();
3212
- bootSelection = await defaultSelection();
3284
+ // Reading the new-bot default probes every engine CLI (each up to its
3285
+ // timeout), and at start only the first-run seed needs the answer. A later
3286
+ // start listens without waiting: the same read runs behind it, and turns
3287
+ // wait for it (enginesRead).
3288
+ if (store.needsSeed())
3289
+ bootSelection = await defaultSelection();
3290
+ else
3291
+ enginesRead = defaultSelection().then((selection) => { bootSelection = selection; }, (error) => console.warn(`[engines] reading the engines at start failed: ${error instanceof Error ? error.message : String(error)}`)).finally(() => { enginesRead = null; });
3213
3292
  store.seedIfEmpty();
3214
3293
  hostedModels?.reconcile(store);
3215
3294
  // Skills library boot sweep (features.skillsLibrary): migrate per-bot
@@ -3263,11 +3342,13 @@ if (browserCleanupReferencesReconciled)
3263
3342
  * than the desktop window did. Stripped here rather than at each call site
3264
3343
  * so a new broadcast cannot forget. */
3265
3344
  let activeCoordinationForThread = (_threadId) => false;
3266
- const wireTask = (task) => {
3345
+ /** One thread as a client sees it. `botModel` is its bot's model: a thread
3346
+ * without a model of its own runs on it (toWireTask). */
3347
+ const wireTask = (task, botModel) => {
3267
3348
  // Time-based snoozes heal on read against the server clock — no client
3268
3349
  // timer, no device skew. The 0 sentinel ("until new activity") is not a
3269
3350
  // time and survives reads; only a wake event in the store clears it.
3270
- const { snoozedUntil, ...base } = toWireTask(task);
3351
+ const { snoozedUntil, ...base } = toWireTask(task, botModel);
3271
3352
  const coordinated = { ...base, waitingForTeammates: activeCoordinationForThread(task.threadId) && !task.busy };
3272
3353
  const asleep = snoozedUntil === 0 || (snoozedUntil !== undefined && snoozedUntil > Date.now());
3273
3354
  return asleep ? { ...coordinated, snoozedUntil } : coordinated;
@@ -3281,7 +3362,7 @@ const wireBot = (bot) => {
3281
3362
  ? { ...rest, approvalMode: "ask", autoApprove: false }
3282
3363
  : rest;
3283
3364
  return { ...visible, waitingForTeammates: activeCoordinationForThread(bot.threadId) && !threadBusy(bot.id, bot.threadId),
3284
- avatarUrl: visible.avatarUrl ?? null, ...(tasks ? { tasks: tasks.map(wireTask) } : {}) };
3365
+ avatarUrl: visible.avatarUrl ?? null, ...(tasks ? { tasks: tasks.map((task) => wireTask(task, bot.modelSelection)) } : {}) };
3285
3366
  };
3286
3367
  function toolScopeForTurn(botId) {
3287
3368
  const bot = store.bot(botId);
@@ -3296,7 +3377,7 @@ function toolScopeForTurn(botId) {
3296
3377
  * can validate it before sending the confirmation that makes it effective. */
3297
3378
  const wireTrustedApprovalBot = (bot) => {
3298
3379
  const { resumeCursors: _resumeCursors, tasks, approvalGrant: _approvalGrant, lastProfileRequestId: _lastProfileRequestId, lastTeamSetupReceipt: _lastTeamSetupReceipt, packageBase: _packageBase, assignedSkills: _assignedSkills, ...rest } = bot;
3299
- return { ...rest, approvalMode: approvalModeFor(rest), avatarUrl: rest.avatarUrl ?? null, ...(tasks ? { tasks: tasks.map(wireTask) } : {}) };
3380
+ return { ...rest, approvalMode: approvalModeFor(rest), avatarUrl: rest.avatarUrl ?? null, ...(tasks ? { tasks: tasks.map((task) => wireTask(task, bot.modelSelection)) } : {}) };
3300
3381
  };
3301
3382
  /** A settings-based preview, not a receipt of a dispatched turn. No
3302
3383
  * provisioning or credentials are needed to inspect it. The selected engine
@@ -3950,7 +4031,7 @@ const storedAvatarExists = (avatarUrl) => attachmentExists(avatarUrl.slice("/api
3950
4031
  const publicBot = (bot) => ({
3951
4032
  ...wireBot(bot),
3952
4033
  ...messagePage(bot.threadId, DEFAULT_PAGE),
3953
- tasks: store.tasks(bot.id).map(wireTask),
4034
+ tasks: store.tasks(bot.id).map((task) => wireTask(task, bot.modelSelection)),
3954
4035
  });
3955
4036
  const publicBotQueuedMessages = () => queuedSteerSnapshot((botId, threadId) => Boolean(store.taskByThread(botId, threadId)));
3956
4037
  // busyBotId names only the speaker that currently owns the provider process.
@@ -4142,6 +4223,15 @@ function updateChannel(groupId, value) {
4142
4223
  throw Object.assign(new Error("invalid default responder"), { status: 400 });
4143
4224
  patch.defaultResponder = responder;
4144
4225
  }
4226
+ if (Object.prototype.hasOwnProperty.call(body, "turnTimeoutMinutes")) {
4227
+ if (!existing.dm) {
4228
+ throw Object.assign(new Error("set the turn limit on that conversation"), { status: 400 });
4229
+ }
4230
+ const parsed = parseConversationTurnTimeout(body.turnTimeoutMinutes);
4231
+ if (!parsed.ok)
4232
+ throw Object.assign(new Error(parsed.error), { status: 400 });
4233
+ patch.turnTimeoutMinutes = parsed.minutes;
4234
+ }
4145
4235
  if (body.cwd !== undefined) {
4146
4236
  if (existing.dm)
4147
4237
  throw Object.assign(new Error("direct-message channels cannot have a working folder"), { status: 400 });
@@ -4462,7 +4552,14 @@ function publicGroupState(record) {
4462
4552
  usage = groupUsageReader.forThread(group.threadId);
4463
4553
  }
4464
4554
  catch { /* accounting must not block chat */ }
4465
- return { ...group, usage: usage ?? null, working: groupIsWorking(group) || [...roomHandoffs.nodes.values()].some(n => n.groupId === group.id && !["completed", "failed", "cancelled"].includes(n.status)) };
4555
+ return {
4556
+ ...group,
4557
+ // Always present so a client can tell "back to the group default"
4558
+ // apart from a partial patch that did not mention the field.
4559
+ turnTimeoutMinutes: record.turnTimeoutMinutes ?? null,
4560
+ usage: usage ?? null,
4561
+ working: groupIsWorking(group) || [...roomHandoffs.nodes.values()].some(n => n.groupId === group.id && !["completed", "failed", "cancelled"].includes(n.status)),
4562
+ };
4466
4563
  }
4467
4564
  function beginGroupTurnOperation(groupId, threadId, botIds = []) {
4468
4565
  const operation = {
@@ -8847,6 +8944,8 @@ async function startTurn(botId, text, opts) {
8847
8944
  },
8848
8945
  };
8849
8946
  }
8947
+ // A thread whose own model can't run runs on its bot's from now on.
8948
+ healThreadModel(botId, threadId);
8850
8949
  const bot = store.projectBotForTask(botId, threadId);
8851
8950
  if (!bot)
8852
8951
  throw Object.assign(new Error("no such task"), { status: 404 });
@@ -9088,6 +9187,11 @@ async function startTurn(botId, text, opts) {
9088
9187
  turnContext.delete(threadId);
9089
9188
  void (async () => {
9090
9189
  try {
9190
+ if (enginesRead) {
9191
+ await enginesRead;
9192
+ if (!directTurnClaimIsCurrent(bot.id, dispatchClaimId, threadId))
9193
+ throw new DirectTurnSetupCancelled("turn stopped while the engines were read");
9194
+ }
9091
9195
  // Readiness can wait on the network. Admit the task first so its busy
9092
9196
  // state, Stop action and watchdog own that wait just like VM setup.
9093
9197
  if (opts?.runOn === "cloud") {
@@ -10042,6 +10146,7 @@ async function startTurn(botId, text, opts) {
10042
10146
  mentionTurn: tagged.length > 0,
10043
10147
  integrations,
10044
10148
  mcpFromUserConfig: claudeUserMcpEnabled(cfg) && !managedPolicy.restrictsMcp(),
10149
+ mcpCallTimeoutMs: mcpCallTimeoutMinutes(cfg) * 60_000,
10045
10150
  cwd,
10046
10151
  }), () => !directTurnClaimExists(bot.id, dispatchClaimId, threadId), async () => {
10047
10152
  await instance.adapter.interruptTurn(threadId).catch(() => { });
@@ -10279,6 +10384,9 @@ function routineSourceOwner(run) {
10279
10384
  ? { bot, group: undefined, threadId: task.threadId }
10280
10385
  : null;
10281
10386
  }
10387
+ // Without a results snapshot (a run from before every routine reported to
10388
+ // its bot's main thread, or a bot with no visible thread) the chat that
10389
+ // made the routine is the destination.
10282
10390
  const threadId = run.sourceThreadId?.trim();
10283
10391
  if (!threadId)
10284
10392
  return null;
@@ -10299,6 +10407,21 @@ function routineSourceOwner(run) {
10299
10407
  const group = store.groupByThread(threadId);
10300
10408
  return group?.memberIds.includes(bot.id) ? { bot, group, threadId } : null;
10301
10409
  }
10410
+ /** There is no durable "main thread" (bot.threadId is only the UI
10411
+ * selection), so a routine reports into the bot's oldest open conversation:
10412
+ * the one it was created with until that is deleted or archived. Deleting
10413
+ * the last one already gives the bot a fresh conversation (Store.deleteTask). */
10414
+ function botMainThread(botId) {
10415
+ const visible = store.tasks(botId).filter((task) => !task.routineRunId);
10416
+ const open = visible.filter((task) => task.archivedAt === undefined);
10417
+ return (open.length ? open : visible).reduce((oldest, task) => (!oldest || task.createdAt < oldest.createdAt ? task : oldest), undefined)?.threadId;
10418
+ }
10419
+ /** Routines without a chosen thread used to get an automatic "<name> ·
10420
+ * Results" thread. One nobody renamed is that automatic default, not a
10421
+ * choice: new runs move to the main thread, and the old thread stays. */
10422
+ function legacyResultsThread(routineName, task) {
10423
+ return task.title.endsWith(" · Results") || task.title === threadTitleFrom(`${routineName} · Results`);
10424
+ }
10302
10425
  function routineSourceThread(run) {
10303
10426
  return routineSourceOwner(run)?.threadId ?? null;
10304
10427
  }
@@ -10532,21 +10655,32 @@ routines = new RoutineManager({
10532
10655
  return Boolean(bot && !bot.hidden && task && !task.routineRunId);
10533
10656
  },
10534
10657
  resolveResultsThread: (routine, forceNew) => {
10535
- // A trusted chat source is already snapshotted as sourceThreadId on each
10536
- // run. Keep it distinct: it can belong to a teammate or room, whereas an
10537
- // explicit resultsThreadId must be a visible task owned by the running bot.
10538
- if (!forceNew && routineSourceOwner(routine))
10539
- return routine.resultsThreadId;
10658
+ // Every run reports into the routine's bot's main thread, wherever the
10659
+ // routine was made (chat, editor, API, or a teammate's request). A run
10660
+ // still executes in its own hidden task, which Run logs and Open run use.
10661
+ // A thread the person chose in "Post results to" keeps winning.
10662
+ const bot = store.bot(routine.botId);
10663
+ const chosen = !forceNew && routine.resultsThreadId && bot && !bot.hidden
10664
+ ? store.taskByThread(bot.id, routine.resultsThreadId) : undefined;
10665
+ // On a Cloud home a routine reports only where its writer may write: a
10666
+ // guest's (or nobody's) keeps a results conversation opened as theirs.
10667
+ const opener = CLOUD_HOME ? routineOpener(routine.id) : undefined;
10668
+ const main = bot && !bot.hidden ? botMainThread(bot.id) : undefined;
10669
+ const mainAllowed = main && (!CLOUD_HOME || (opener === CLOUD_OWNER_KEY && !cloudGuestOpened(main)));
10670
+ const keepChosen = chosen && !chosen.routineRunId && !(mainAllowed && legacyResultsThread(routine.name, chosen));
10671
+ if (keepChosen)
10672
+ return chosen.threadId;
10673
+ if (mainAllowed || !CLOUD_HOME)
10674
+ return main;
10540
10675
  const threadId = store.createTask(routine.botId, `${routine.name} · Results`, false)?.threadId;
10541
- // On a Cloud home the results conversation is opened by whoever wrote the
10542
- // routine: it carries their name for it (cloudGuestOpened).
10543
- if (threadId && CLOUD_HOME)
10544
- threadStarters.set(threadId, routineOpener(routine.id));
10676
+ if (threadId)
10677
+ threadStarters.set(threadId, opener);
10545
10678
  return threadId;
10546
10679
  },
10547
10680
  discardResultsThread: (botId, threadId) => {
10548
10681
  const task = store.taskByThread(botId, threadId);
10549
- if (task && !task.busy && !task.routineRunId && store.messagesFor(threadId).length === 0) {
10682
+ // The main thread is reused, never allocated, so a failed write keeps it.
10683
+ if (task && threadId !== botMainThread(botId) && !task.busy && !task.routineRunId && store.messagesFor(threadId).length === 0) {
10550
10684
  store.deleteTask(botId, threadId);
10551
10685
  handoffs.forget(threadId);
10552
10686
  }
@@ -10718,7 +10852,11 @@ if (recoveryOwners.length > 0) {
10718
10852
  // after the user confirms a durable card. Keeping this beside the scheduler
10719
10853
  // makes the card resolvable after an app restart without involving the model.
10720
10854
  async function cloudRoutineReadiness(botId, threadId) {
10721
- const bot = threadId ? store.projectBotForTask(botId, threadId) : store.bot(botId);
10855
+ const projected = threadId ? store.projectBotForTask(botId, threadId) : store.bot(botId);
10856
+ // The model its turn will run on: a thread's own model that can't run
10857
+ // gives way to the bot's when the turn starts (healThreadModel).
10858
+ const selection = threadId ? turnSelection(botId, threadId) : projected?.modelSelection;
10859
+ const bot = projected && selection ? { ...projected, modelSelection: selection } : projected;
10722
10860
  if (!bot || bot.hidden)
10723
10861
  return { ready: false, reason: "The routine's target bot no longer exists." };
10724
10862
  if (!boat.boatConfigured(cfg)) {
@@ -11437,10 +11575,9 @@ const webhooks = new WebhookManager({
11437
11575
  return;
11438
11576
  store.appendMessage(threadId, { role: "bot", kind: "text", text, webhookPost: true });
11439
11577
  },
11440
- // Mirrors resolveResultsThread's routines wiring a few hundred lines up
11441
- // in this same file: create-on-first-use, never activated (so it never
11442
- // steals the bot's live selection the way POST /api/bots/:id/tasks
11443
- // does), reused forever after via trigger.resultsThreadId.
11578
+ // Create-on-first-use, never activated (so it never steals the bot's
11579
+ // live selection the way POST /api/bots/:id/tasks does), reused forever
11580
+ // after via trigger.resultsThreadId.
11444
11581
  resolvePostThread: (trigger, forceNew) => {
11445
11582
  if (!forceNew && trigger.resultsThreadId)
11446
11583
  return trigger.resultsThreadId;
@@ -12207,6 +12344,10 @@ setupRetry = 0) {
12207
12344
  { id: "skill-instructions", label: "Skill instructions", text: renderSkillInstructions(selectedSkills, { includeRoot: Boolean(workspace) }) },
12208
12345
  { id: "playbooks", label: "Playbooks", text: installedPlaybookInstructions(text, bot.playbooks) },
12209
12346
  ]);
12347
+ // The engines read at start is part of setup (it learns what this engine
12348
+ // supports): wait for it before the claim below.
12349
+ if (enginesRead)
12350
+ await enginesRead;
12210
12351
  // run the turn and wait for it to settle, folding the reply text so a
12211
12352
  // chained @mention can be routed afterwards
12212
12353
  // Claim only after setup succeeded. An unavailable, busy, unsupported, or
@@ -12246,7 +12387,10 @@ setupRetry = 0) {
12246
12387
  else
12247
12388
  markCancelledProviderHandshake(threadId, retirementOwner);
12248
12389
  };
12249
- const timeoutMinutes = roomTurnTimeoutMinutes(cfg);
12390
+ const liveGroup = store.group(groupId) ?? group;
12391
+ const timeoutMinutes = effectiveRoomTurnTimeoutMinutes(roomTurnTimeoutMinutes(cfg), liveGroup.dm
12392
+ ? liveGroup.turnTimeoutMinutes
12393
+ : store.groupTaskByThread(liveGroup.id, threadId)?.turnTimeoutMinutes);
12250
12394
  const outcome = await new Promise((resolve) => {
12251
12395
  let done = false;
12252
12396
  let unsub = () => { };
@@ -12334,6 +12478,7 @@ setupRetry = 0) {
12334
12478
  cwd,
12335
12479
  integrations,
12336
12480
  mcpFromUserConfig: claudeUserMcpEnabled(cfg) && !managedPolicy.restrictsMcp(),
12481
+ mcpCallTimeoutMs: mcpCallTimeoutMinutes(cfg) * 60_000,
12337
12482
  ...(instance.instanceId === readyBot.modelSelection.instanceId
12338
12483
  ? memberTurnSelection(readyBot.modelSelection)
12339
12484
  : { model: instance.models.default }),
@@ -14262,6 +14407,7 @@ function configStatus() {
14262
14407
  // not a secret — the settings picker shows it; "" = follow the system
14263
14408
  language: cfg.language ?? "",
14264
14409
  rooms: { turnTimeoutMinutes: roomTurnTimeoutMinutes(cfg) },
14410
+ mcp: { callTimeoutMinutes: mcpCallTimeoutMinutes(cfg) },
14265
14411
  automaticRecovery: cfg.automaticRecovery ?? { enabled: false },
14266
14412
  // absent effort = no level is sent, so clients can tell it from any level
14267
14413
  newBots: cfg.newBots?.effort ? { effort: cfg.newBots.effort } : {},
@@ -14617,15 +14763,22 @@ function serveStatic(res, path) {
14617
14763
  try {
14618
14764
  const data = readFileSync(file);
14619
14765
  const type = MIME[extname(file)] ?? "application/octet-stream";
14620
- res.writeHead(200, { "content-type": type, ...(type === "text/html" ? PAGE_HEADERS : {}) });
14766
+ // Vite puts a content hash in the name of every file it writes under
14767
+ // /assets/, so a URL there never changes: the browser keeps it instead of
14768
+ // downloading the bundle on every load. That only holds while public/ has
14769
+ // no assets/ dir (index.test.ts checks). Pages are revalidated, so a new
14770
+ // build shows up on the next load.
14771
+ const cache = type === "text/html" ? "no-cache" : safe.startsWith("/assets/") ? "public, max-age=31536000, immutable" : null;
14772
+ res.writeHead(200, { "content-type": type, ...(type === "text/html" ? PAGE_HEADERS : {}), ...(cache ? { "cache-control": cache } : {}) });
14621
14773
  res.end(data);
14622
14774
  return true;
14623
14775
  }
14624
14776
  catch {
14625
- // SPA fallback
14777
+ // SPA fallback. Never immutable: a missing chunk answered with the page
14778
+ // must not stay cached under the chunk's URL.
14626
14779
  try {
14627
14780
  const data = readFileSync(join(STATIC_DIR, "index.html"));
14628
- res.writeHead(200, { "content-type": "text/html", ...PAGE_HEADERS });
14781
+ res.writeHead(200, { "content-type": "text/html", "cache-control": "no-cache", ...PAGE_HEADERS });
14629
14782
  res.end(data);
14630
14783
  return true;
14631
14784
  }
@@ -14740,6 +14893,15 @@ ROUTES.push(createBotMemoryRoutes({
14740
14893
  },
14741
14894
  }));
14742
14895
  ROUTES.push(createDeciderRoutes({ decider }));
14896
+ // "Switch them too": a bot's threads on a model of their own follow its model again.
14897
+ ROUTES.push(createThreadModelRoutes({
14898
+ bot: (id) => store.bot(id),
14899
+ busy: (botId, threadId) => threadBusy(botId, threadId),
14900
+ follow: (botId, threadIds) => store.followBotModel(botId, threadIds, (from, to) => hostedModels?.resetTask(from, to) ?? {}),
14901
+ // Like updateBotDefault on a thread: on a Cloud home, the owner's alone.
14902
+ mayChangeModel: (auth) => !CLOUD_HOME || cloudOwnerSession(auth),
14903
+ reply: (botId) => publicBot(store.bot(botId)),
14904
+ }));
14743
14905
  ROUTES.push(createAntigravityLeftoverRoutes({
14744
14906
  hosted: Boolean(hostedModels),
14745
14907
  isAntigravity: (instanceId) => registry.get(instanceId)?.driverKind === "antigravityAgent",
@@ -18048,6 +18210,9 @@ const handleRequest = async (req, res) => {
18048
18210
  const message = store.appendMessage(threadId, {
18049
18211
  role: "bot",
18050
18212
  kind: "connector",
18213
+ // Read only by clients that predate the card (older phones draw
18214
+ // an unknown kind by its text); see connector-card-text.ts.
18215
+ text: connectorCardText(toolkit.label, item.alias),
18051
18216
  ...(owner.group ? { from: { botId: owner.bot.id, name: owner.bot.name, color: owner.bot.color } } : {}),
18052
18217
  connector: {
18053
18218
  slug: item.slug,
@@ -18460,7 +18625,7 @@ const handleRequest = async (req, res) => {
18460
18625
  return json(res, 200, {
18461
18626
  bots: shownBots.map((bot) => memberBot({
18462
18627
  ...wireBot(bot),
18463
- tasks: store.tasks(bot.id).map(wireTask),
18628
+ tasks: store.tasks(bot.id).map((task) => wireTask(task, bot.modelSelection)),
18464
18629
  ...messagePage(bot.threadId, limit),
18465
18630
  }, visible)),
18466
18631
  botQueuedMessages: visible.everything ? queued : Object.fromEntries(Object.entries(queued).filter(([threadId]) => visible.thread(threadId))),
@@ -19424,7 +19589,7 @@ const handleRequest = async (req, res) => {
19424
19589
  if (!body || typeof body !== "object" || Array.isArray(body)) {
19425
19590
  return json(res, 400, { error: "body must be a JSON object" });
19426
19591
  }
19427
- const allowed = new Set(["title", "pinned"]);
19592
+ const allowed = new Set(["title", "pinned", "turnTimeoutMinutes"]);
19428
19593
  if (Object.keys(body).some((key) => !allowed.has(key)))
19429
19594
  return json(res, 400, { error: "unsupported channel thread setting" });
19430
19595
  const existing = store.groupTaskByThread(group.id, m[2]);
@@ -19432,12 +19597,16 @@ const handleRequest = async (req, res) => {
19432
19597
  return json(res, 404, { error: "no such channel task" });
19433
19598
  const pinning = body.pinned !== undefined;
19434
19599
  const renaming = body.title !== undefined;
19435
- if (!pinning && !renaming)
19600
+ const limiting = Object.prototype.hasOwnProperty.call(body, "turnTimeoutMinutes");
19601
+ if (!pinning && !renaming && !limiting)
19436
19602
  return json(res, 400, { error: "unsupported channel thread setting" });
19437
19603
  if (pinning && typeof body.pinned !== "boolean")
19438
19604
  return json(res, 400, { error: "pinned must be a boolean" });
19439
19605
  if (renaming && typeof body.title !== "string")
19440
19606
  return json(res, 400, { error: "title must be a string" });
19607
+ const parsedLimit = limiting ? parseConversationTurnTimeout(body.turnTimeoutMinutes) : null;
19608
+ if (parsedLimit && !parsedLimit.ok)
19609
+ return json(res, 400, { error: parsedLimit.error });
19441
19610
  // Echoing the current title lets a newer client pin on an older server
19442
19611
  // without the old handler turning a missing title into "Untitled".
19443
19612
  // That echo is not a rename. A real rename stays blocked while working.
@@ -19445,10 +19614,19 @@ const handleRequest = async (req, res) => {
19445
19614
  const notYours = titleChange ? cloudThreadRefusal(auth, m[2]) : null;
19446
19615
  if (notYours)
19447
19616
  return json(res, 403, { error: notYours });
19448
- if (channelTaskBlocked(group) && !(pinning && !titleChange)) {
19617
+ // A longer limit applies to the next turn, so it can be saved while
19618
+ // this one is still running. A rename still waits.
19619
+ const settingsOnly = !titleChange && (pinning || limiting);
19620
+ if (channelTaskBlocked(group) && !settingsOnly) {
19449
19621
  return json(res, 409, { error: "this channel is working or waiting on you — finish that turn first" });
19450
19622
  }
19451
19623
  let task = existing;
19624
+ if (limiting && parsedLimit?.ok) {
19625
+ const limited = store.setGroupTaskTurnTimeout(group.id, m[2], parsedLimit.minutes);
19626
+ if (!limited)
19627
+ return json(res, 404, { error: "no such channel task" });
19628
+ task = limited;
19629
+ }
19452
19630
  if (pinning) {
19453
19631
  const pinned = store.setGroupTaskPinned(group.id, m[2], body.pinned === true);
19454
19632
  if (!pinned)
@@ -20282,10 +20460,12 @@ const handleRequest = async (req, res) => {
20282
20460
  }
20283
20461
  // patchBot persists first and emits the canonical bot change, which the
20284
20462
  // store listener above turns into the slim wire-format SSE broadcast.
20463
+ // Every thread that follows the bot moves with it; the selected thread
20464
+ // follows it from now on too, as if picked there.
20285
20465
  const bot = store.patchBot(existing.id, { modelSelection: checked.selection });
20286
20466
  if (!bot)
20287
20467
  return json(res, 404, { error: "no such bot" });
20288
- store.patchTask(bot.id, selected.threadId, { modelSelection: checked.selection,
20468
+ store.patchTask(bot.id, selected.threadId, { modelSelection: undefined,
20289
20469
  ...hostedModels?.resetTask(selected.modelSelection, checked.selection) });
20290
20470
  return json(res, 200, { bot: wireBot(bot) });
20291
20471
  }
@@ -20971,8 +21151,10 @@ const handleRequest = async (req, res) => {
20971
21151
  // and pins that already match stay; returning to Auto sweeps nothing.
20972
21152
  if (computerSpecified && requestedComputer !== undefined)
20973
21153
  store.clearAutoSurfacePins(m[1], requestedComputer);
21154
+ // A new bot model moves every thread that follows the bot; the selected
21155
+ // thread follows it from now on too.
20974
21156
  if (normalizedSelection && selectedTask)
20975
- store.patchTask(bot.id, selectedTask.threadId, { modelSelection: normalizedSelection,
21157
+ store.patchTask(bot.id, selectedTask.threadId, { modelSelection: undefined,
20976
21158
  ...hostedModels?.resetTask(selectedTask.modelSelection, normalizedSelection) });
20977
21159
  if (existingBot && (bot.browserProfile !== beforeBrowserProfile || bot.browser !== beforeBrowserEnabled)) {
20978
21160
  browserLive.closeForBot(bot.id);
@@ -21769,12 +21951,15 @@ const handleRequest = async (req, res) => {
21769
21951
  if (phoneSecretSubmissions.hasThread(bot.threadId)) {
21770
21952
  return json(res, 409, { error: "this task is securely saving a credential — try again when it finishes" });
21771
21953
  }
21772
- if (!registry.get(bot.modelSelection.instanceId)) {
21954
+ // A thread's own model that can't run gives way to the bot's when the
21955
+ // rerun starts (healThreadModel), so check the model it will run on.
21956
+ const rerunSelection = turnSelection(bot.id, bot.threadId) ?? bot.modelSelection;
21957
+ if (!registry.get(rerunSelection.instanceId)) {
21773
21958
  return json(res, 409, {
21774
- error: `provider instance "${bot.modelSelection.instanceId}" is unavailable — pick another model in settings`,
21959
+ error: `provider instance "${rerunSelection.instanceId}" is unavailable — pick another model in settings`,
21775
21960
  });
21776
21961
  }
21777
- const rerunInstance = registry.get(bot.modelSelection.instanceId);
21962
+ const rerunInstance = registry.get(rerunSelection.instanceId);
21778
21963
  const rerunRefusal = policyModelRefusal(rerunInstance);
21779
21964
  if (rerunRefusal)
21780
21965
  return json(res, 409, { error: rerunRefusal });
@@ -22205,7 +22390,7 @@ const handleRequest = async (req, res) => {
22205
22390
  // Who opened it decides who may answer its cards on a shared workspace.
22206
22391
  if (auth.kind === "session")
22207
22392
  threadStarters.set(task.threadId, actorKey(auth));
22208
- return json(res, 201, { bot: publicBot(store.bot(bot.id)), task: wireTask(task) });
22393
+ return json(res, 201, { bot: publicBot(store.bot(bot.id)), task: wireTask(task, store.bot(bot.id).modelSelection) });
22209
22394
  }
22210
22395
  m = path.match(/^\/api\/bots\/([\w-]+)\/tasks\/([\w-]+)$/);
22211
22396
  if (m && method === "POST") {
@@ -22223,7 +22408,7 @@ const handleRequest = async (req, res) => {
22223
22408
  const switched = store.switchTask(bot.id, m[2]);
22224
22409
  if (!switched)
22225
22410
  return json(res, 404, { error: "no such task" });
22226
- const switchedSettings = { ...wireBot(switched), tasks: store.tasks(switched.id).map(wireTask) };
22411
+ const switchedSettings = { ...wireBot(switched), tasks: store.tasks(switched.id).map((task) => wireTask(task, switched.modelSelection)) };
22227
22412
  // "0" is settings only, a positive page is a bounded transcript, and no
22228
22413
  // parameter is the whole thread — the one branch that materialises it.
22229
22414
  // Search results land through that branch (src/lib/focus-message.ts):
@@ -22305,7 +22490,7 @@ const handleRequest = async (req, res) => {
22305
22490
  const task = store.refreshTaskPermissions(profile.id, m[2]);
22306
22491
  if (!task)
22307
22492
  return json(res, 404, { error: "no such task" });
22308
- return json(res, 200, { task: wireTask(task), bot: publicBot(store.bot(profile.id)) });
22493
+ return json(res, 200, { task: wireTask(task, store.bot(profile.id).modelSelection), bot: publicBot(store.bot(profile.id)) });
22309
22494
  }
22310
22495
  const patch = {};
22311
22496
  if (body.projectId !== undefined) {
@@ -22411,7 +22596,7 @@ const handleRequest = async (req, res) => {
22411
22596
  const task = patch.modelSelection
22412
22597
  ? store.switchTaskModel(m[1], m[2], patch.modelSelection, body.updateBotDefault === true, body.resetApprovalToAsk === true, { ...patch, ...hostedModels?.resetTask(current.modelSelection, patch.modelSelection) })
22413
22598
  : store.patchTask(m[1], m[2], patch);
22414
- return json(res, 200, { task: wireTask(task), bot: publicBot(store.bot(m[1])) });
22599
+ return json(res, 200, { task: wireTask(task, store.bot(m[1]).modelSelection), bot: publicBot(store.bot(m[1])) });
22415
22600
  }
22416
22601
  if (m && method === "DELETE") {
22417
22602
  const bot = store.bot(m[1]);
@@ -22483,7 +22668,7 @@ const handleRequest = async (req, res) => {
22483
22668
  const task = store.retitleTask(m[1], m[2], startedFrom, title) ?? store.taskByThread(m[1], m[2]);
22484
22669
  if (!task)
22485
22670
  return json(res, 404, { error: "no such task" });
22486
- return json(res, 200, { task: wireTask(task) });
22671
+ return json(res, 200, { task: wireTask(task, store.bot(m[1]).modelSelection) });
22487
22672
  }
22488
22673
  finally {
22489
22674
  titleRegenerations.delete(m[2]);
@@ -23061,7 +23246,17 @@ const handleRequest = async (req, res) => {
23061
23246
  return json(res, 400, { error: "That does not look like an API key." });
23062
23247
  const url = typeof body?.url === "string" && body.url.trim() ? body.url.trim() : (saved && "url" in saved ? saved.url : undefined);
23063
23248
  res.setHeader("cache-control", "no-store");
23064
- return json(res, 200, await checkProviderKey({ provider: kind, key, url }));
23249
+ const verdict = await checkProviderKey({ provider: kind, key, url });
23250
+ // A Test is a real use the person chose: the engines on this key agree
23251
+ // with its verdict. Claude runs its key through its CLI, not here.
23252
+ if (kind !== "anthropic") {
23253
+ const base = providerBaseUrl(kind, url);
23254
+ if (verdict.ok)
23255
+ noteKeyAccepted(base, key);
23256
+ else if (verdict.reason === "rejected")
23257
+ noteKeyRejected(base, key);
23258
+ }
23259
+ return json(res, 200, verdict);
23065
23260
  }
23066
23261
  if (method === "GET" && path === "/api/decisions") {
23067
23262
  const rawLimit = url.searchParams.get("limit");
@@ -24091,6 +24286,12 @@ const handleRequest = async (req, res) => {
24091
24286
  browserCleanup.abort(request);
24092
24287
  throw error;
24093
24288
  }
24289
+ // A saved or cleared key starts over: neither it nor the key it
24290
+ // replaced stays marked as rejected.
24291
+ const resetKeys = PROVIDER_KEY_KINDS.flatMap((kind) => {
24292
+ const next = patch[kind]?.key;
24293
+ return next === undefined ? [] : [cfg[kind]?.key, next];
24294
+ });
24094
24295
  let configWriteCommitted = false;
24095
24296
  const externalSecretStorage = url.searchParams.get("secretStorage") === "external";
24096
24297
  try {
@@ -24156,6 +24357,9 @@ const handleRequest = async (req, res) => {
24156
24357
  }
24157
24358
  throw error;
24158
24359
  }
24360
+ for (const key of resetKeys)
24361
+ if (key)
24362
+ forgetKey(key);
24159
24363
  // Desktops already counting down move to the new window at once, in
24160
24364
  // every isolation mode; busy work still defers its expiry.
24161
24365
  if (patch.localVm?.idleTimeoutMinutes !== undefined) {
@@ -24772,7 +24976,7 @@ const handleRequest = async (req, res) => {
24772
24976
  catch (e) {
24773
24977
  const status = e?.status ?? 500;
24774
24978
  const candidateCode = e?.code;
24775
- const code = typeof candidateCode === "string" && ["guarded_busy", "guarded_branch", "guarded_permissions", "guarded_request_changed", "guarded_request_untracked"].includes(candidateCode)
24979
+ const code = typeof candidateCode === "string" && ["guarded_busy", "guarded_branch", "guarded_permissions", "guarded_request_changed", "guarded_request_untracked", "outside_workspace"].includes(candidateCode)
24776
24980
  ? candidateCode : undefined;
24777
24981
  return json(res, status, { error: e instanceof Error ? e.message : String(e), ...(code ? { code } : {}) });
24778
24982
  }
@@ -24922,12 +25126,22 @@ restoreChannelMessages();
24922
25126
  if (movedAutoPins)
24923
25127
  console.log(`Works on: moved ${movedAutoPins} auto-pinned thread(s) to their bot's current Works on`);
24924
25128
  }
24925
- server.listen(PORT, "127.0.0.1", () => {
25129
+ server.listen(PORT, "127.0.0.1", async () => {
24926
25130
  companyRuntimeReady();
24927
25131
  console.log(`openmausbot server on http://127.0.0.1:${PORT}`);
25132
+ // Words queued before the restart start first, ahead of anything sent from
25133
+ // now on. Their turns wait for the engines read at start like every turn.
24928
25134
  followupsReady = true;
24929
25135
  drainQueuedSends();
24930
25136
  drainQueuedChannelSends();
25137
+ // The work below wakes delegators, and a woken delegator keeps its session
25138
+ // only on an engine that records what it was handed, which that read finds
25139
+ // out: it starts once the read is done.
25140
+ if (enginesRead) {
25141
+ await enginesRead;
25142
+ if (companyShutdown)
25143
+ return;
25144
+ }
24931
25145
  // Startup work uses the same turn dispatcher and local tool endpoint as
24932
25146
  // ordinary chat. Start only once every registry is initialized and the
24933
25147
  // endpoint is listening; earlier dispatch can hit uninitialized bindings.