openmausbot 0.1.96 → 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.
- package/dist/assets/ActivityPanel-H6D3SD39.js +1 -0
- package/dist/assets/BotSettingsDialog-Dqz_AiBZ.js +7 -0
- package/dist/assets/CloudBackendPicker-iezpJFpF.js +1 -0
- package/dist/assets/ComputerPanel-DZPj-IWu.js +1 -0
- package/dist/assets/DesktopViewer-utONms1p.js +5 -0
- package/dist/assets/InspectorPanel-OVaBVrzC.js +5 -0
- package/dist/assets/KeyboardShortcutsModal-gk9mvHj_.js +1 -0
- package/dist/assets/LocalVmWorkspace-DLIvd2kC.js +1 -0
- package/dist/assets/MacLocalControl-bJv8yiPB.js +1 -0
- package/dist/assets/NewBotDialog-1qGOAdUM.js +2 -0
- package/dist/assets/RemoteAgentSettingsPanel-DqKr0HkO.js +1 -0
- package/dist/assets/RoutinesPage-BM2D4qGi.js +1 -0
- package/dist/assets/RoutinesSection-Dny9ra-c.js +1 -0
- package/dist/assets/SettingsModal-gC6XJJmt.js +11 -0
- package/dist/assets/TeamMapPage-B6fuu4r_.js +8 -0
- package/dist/assets/TriggersPanel-j0qTP2CO.js +1 -0
- package/dist/assets/VisibilitySection-DT0FqPvu.js +12 -0
- package/dist/assets/WebhooksPanel--YQyCjKO.js +3 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-Dkbd2hcj.js → abnfDiagram-O67JEVCF-BsjcFX0-.js} +1 -1
- package/dist/assets/activity-BxOkayvD.js +1 -0
- package/dist/assets/{arc-Duj5Coy2.js → arc-CJNA94c6.js} +1 -1
- package/dist/assets/architectureDiagram-NJMV4G6O-dz2ud7lP.js +36 -0
- package/dist/assets/{blockDiagram-BEXU5L5S-CWDlTpXe.js → blockDiagram-BEXU5L5S-ChBEALoR.js} +1 -1
- package/dist/assets/brain-CFZ7PEXc.js +1 -0
- package/dist/assets/{c4Diagram-YGBWAQC7-B0t15PDc.js → c4Diagram-YGBWAQC7-KZNRKDQe.js} +1 -1
- package/dist/assets/channel-WhFyujhZ.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BJSCL-HV.js → chunk-2Q5K7J3B-BLLwmCET.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D3_ILccd.js → chunk-3FUC2YCW-CQxyreBZ.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-D3dmwT_S.js → chunk-5VM5RSS4-D6sFMW0u.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Br73HFPD.js → chunk-GTNCS2PH-BaZ1YgLF.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-uSsjxubC.js → chunk-GWA4HPMP-CcalphmC.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C40VWFyr.js → chunk-JWPE2WC7-CSk8R3ve.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-D6WNNYwU.js → chunk-NETBCI7D-CTmLjA57.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-DOEhq64P.js → chunk-WEXAMYUT-CM9Z77Ne.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-BqJEi6Pi.js → chunk-XXDRQBXY-B1fxpMS2.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-ByU_GnxT.js → classDiagram-v2-NBCMYWYE-DCCyoLpj.js} +1 -1
- package/dist/assets/computer-control-NLov352p.js +1 -0
- package/dist/assets/cose-bilkent-JH36ORCC-KyImBKvE.js +1 -0
- package/dist/assets/{cynefin-OW5HDTMX-CZWO7aYj.js → cynefin-OW5HDTMX-fsZc6MGa.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C70-w0la.js → cynefinDiagram-VND7K2PF-CGmHsShB.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-CnhjhExB.js → dagre-6A5THRUB-Dah1h-CH.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-bnjWCNb4.js → diagram-22UHCM2B-BIu71OyV.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-A4uTWoFV.js → diagram-3UASUU5V-gxhUB_Mu.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Zi6m3NUl.js → diagram-ATOU4E4O-CBhusUBn.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DtCcmd5k.js → diagram-CDSNMT55-BdPrc86x.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DofuETS3.js → diagram-MLGK6HIB-Clp110R_.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-Dw2T9PuU.js → diagram-MPIPVDR6-DJKfbugw.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DbrwPiem.js → ebnfDiagram-ZINNZB2B-DnsltoBu.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BvCt1dCb.js → elk-276RUBZZ-jWdq6gqH.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-Y-QKqPnU.js → erDiagram-OPXOYQCR-DArPVf8u.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-CDkL1B5w.js → flowDiagram-KWPJA3E3-D2HZ-2f7.js} +1 -1
- package/dist/assets/ganttDiagram-FUAMR5RP--o0KIxEo.js +292 -0
- package/dist/assets/{gitGraphDiagram-X574FWY7-nL9iFTCv.js → gitGraphDiagram-X574FWY7-CsoXr2D7.js} +1 -1
- package/dist/assets/image-plus-BbVjGHin.js +1 -0
- package/dist/assets/index-BrPW5HDO.css +1 -0
- package/dist/assets/index-C3NqfdVU.js +585 -0
- package/dist/assets/{index-DreDnjOu.js → index-CJTp9JEp.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-C7aHjzsf.js → infoDiagram-VRGFBTTK-g-6lNp7r.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-KUZsNBTh.js → ishikawaDiagram-OU5B5YK6-B1d6qVTV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CGUuodFl.js → journeyDiagram-ZHPQQLJL-BTPuiU6T.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-Cy6r868Z.js → kanban-definition-PNTS6WVX-bDDuRJvP.js} +1 -1
- package/dist/assets/{linear-Bl669148.js → linear-BVVKbEMy.js} +1 -1
- package/dist/assets/{mermaid.core-86Xqz5Mr.js → mermaid.core-JJdoKyKB.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-G4DzHLrd.js → mindmap-definition-NLK3R4M7-B4aE3T0q.js} +1 -1
- package/dist/assets/module-DCUsy2BQ.js +3 -0
- package/dist/assets/page-visible-BBo_Zm8g.js +1 -0
- package/dist/assets/{pegDiagram-GJSIUBJH-K4eruzY0.js → pegDiagram-GJSIUBJH-DFKQWpR7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DYdR9PPq.js → pieDiagram-5QR66LMP-Iv7OKVJi.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BCAh0nEy.js → quadrantDiagram-O4NWA36T-U2YnfwBk.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-p0dGRc4g.js → railroadDiagram-XR7U4H2S-CuPy6RqM.js} +1 -1
- package/dist/assets/remote-desktop-panel-BNEj7tUx.js +1 -0
- package/dist/assets/{requirementDiagram-PLB6GJNP-tLeBG0aL.js → requirementDiagram-PLB6GJNP-bMI5AK7T.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-QRuN16rV.js → sankeyDiagram-IPEJSGJF-BKcFE2WQ.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-ueT1nVoa.js → sequenceDiagram-PO4LG4MO-Bn9q88kl.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-wRS2zWg2.js → sizeCapture-INFHLROL-Cvmdq9HD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CYzA_hE1.js → stateDiagram-v2-GCMORJYK-JJQYw3eQ.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-kDnv6LMc.js → swimlanes-2SLR337P-BkSCBEwp.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CmTirbVT.js +8 -0
- package/dist/assets/table-file.worker-KkMx-SNx.js +10 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-D1y2UbbH.js → timeline-definition-EJHVYXUP-DsaM4GHr.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-IqB4v9HX.js → usecaseDiagram-POWQR4AR-3FOwLq7B.js} +1 -1
- package/dist/assets/user-DcZBnAuv.js +1 -0
- package/dist/assets/{vennDiagram-UO4OBE2U-BEi7KYVF.js → vennDiagram-UO4OBE2U-DyNjJsKt.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-Dkybo-Yf.js → wardleyDiagram-VNRHLVJA-Y4ZMcnBC.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BAPFJLpg.js → xychartDiagram-PMCCYNJV-BXZpg955.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +20 -9
- package/dist-server/container-mcp.js +8 -0
- package/dist-server/desktop-entry.mjs +54 -0
- package/dist-server/drivers/agents-proxy.js +26 -9
- package/dist-server/index.js +3734 -2360
- package/dist-server/openmausbot.js +1664 -1553
- package/dist-server/pair-cli.js +1664 -1553
- package/dist-server/permission-proxy.js +2 -4
- package/dist-server/server/atomic.js +70 -2
- package/dist-server/server/auto-approve.js +1 -1
- package/dist-server/server/auto-vm-claims.js +13 -1
- package/dist-server/server/automatic-recovery.js +1 -2
- package/dist-server/server/boat.js +68 -84
- package/dist-server/server/bot-overview.js +1 -1
- package/dist-server/server/browser-engine.js +38 -1
- package/dist-server/server/browser-live.js +112 -25
- package/dist-server/server/browser-runtime.js +31 -1
- package/dist-server/server/checkpoints.js +275 -44
- package/dist-server/server/cli-setup.js +3 -3
- package/dist-server/server/cloud-computer-tools.js +11 -4
- package/dist-server/server/cloud-home-start.js +4 -4
- package/dist-server/server/cloud-home.js +3 -9
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/computer-engine-removal.js +70 -0
- package/dist-server/server/computer-selection.js +18 -0
- package/dist-server/server/config.js +29 -12
- package/dist-server/server/connector-card-text.js +15 -0
- package/dist-server/server/connector-scope-hint.js +83 -0
- package/dist-server/server/default-model-selection.js +23 -14
- package/dist-server/server/delegations.js +11 -5
- package/dist-server/server/delta-context.js +6 -1
- package/dist-server/server/drivers/acp/core.js +11 -1
- package/dist-server/server/drivers/acp/grok.js +47 -5
- package/dist-server/server/drivers/acp/opencode-go.js +16 -21
- package/dist-server/server/drivers/agents-call.js +1 -1
- package/dist-server/server/drivers/agents-catalog.js +27 -7
- package/dist-server/server/drivers/agents-proxy.js +1 -0
- package/dist-server/server/drivers/builtIn.js +0 -2
- package/dist-server/server/drivers/chat-mcp-tools.js +5 -3
- package/dist-server/server/drivers/claude-login-auth.js +1 -1
- package/dist-server/server/drivers/claude.js +84 -41
- package/dist-server/server/drivers/codex-device-auth.js +27 -319
- package/dist-server/server/drivers/codex.js +2 -2
- package/dist-server/server/drivers/device-auth.js +342 -0
- package/dist-server/server/drivers/openai-chat.js +110 -10
- package/dist-server/server/drivers/pi.js +184 -49
- package/dist-server/server/fleet-agent.js +18 -5
- package/dist-server/server/harness/http.js +5 -2
- package/dist-server/server/harness/registry.js +19 -0
- package/dist-server/server/harness-capabilities.js +7 -4
- package/dist-server/server/hosted-models.js +3 -1
- package/dist-server/server/index.js +823 -333
- package/dist-server/server/key-rejections.js +57 -0
- package/dist-server/server/live-call.js +11 -6
- package/dist-server/server/managed-policy.js +1 -1
- package/dist-server/server/message-file.js +7 -4
- package/dist-server/server/model-catalog/presets.js +8 -0
- package/dist-server/server/peer-roster.js +10 -2
- package/dist-server/server/permission-proxy.js +4 -0
- package/dist-server/server/provider-key-check.js +5 -1
- package/dist-server/server/request-auth.js +1 -1
- package/dist-server/server/room-turn-end.js +15 -0
- package/dist-server/server/room-turn-timeout.js +28 -0
- package/dist-server/server/routes/live.js +3 -3
- package/dist-server/server/routes/thread-models.js +36 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +3 -3
- package/dist-server/server/screen-frame-gate.js +1 -3
- package/dist-server/server/skills.js +3 -2
- package/dist-server/server/store.js +273 -61
- package/dist-server/server/surface.js +29 -49
- package/dist-server/server/system-prompt.js +10 -10
- package/dist-server/server/team-backup.js +19 -4
- package/dist-server/server/thread-model.js +25 -0
- package/dist-server/server/turn-continuation.js +168 -0
- package/dist-server/server/webhook-ingress.js +8 -3
- package/dist-server/server/webhooks.js +40 -5
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/ask-question.js +0 -70
- package/dist-server/shared/cloud-computer.js +4 -6
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/device-sign-in.js +57 -0
- package/dist-server/shared/failed-turn.js +1 -4
- package/dist-server/shared/place-view.js +409 -0
- package/dist-server/shared/team-backup.js +2 -0
- package/dist-server/shared/thread-model.js +13 -0
- package/dist-server/shared/tool-scope-support.js +1 -2
- package/dist-server/vps-container-mcp.js +8 -0
- package/dist-server/workspace-backup.worker.js +41 -22
- package/package.json +1 -1
- package/dist/assets/DesktopViewer-DsnA23KM.js +0 -5
- package/dist/assets/architectureDiagram-NJMV4G6O-uYUsDawP.js +0 -36
- package/dist/assets/channel-BZxwC-Gw.js +0 -1
- package/dist/assets/cose-bilkent-JH36ORCC-D-Uglxqq.js +0 -1
- package/dist/assets/ganttDiagram-FUAMR5RP-CKV5D2F6.js +0 -292
- package/dist/assets/index-Du7o6Dsf.js +0 -610
- package/dist/assets/index-KxG4bWSr.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +0 -8
- package/dist-server/server/drivers/boatagent.js +0 -443
|
@@ -52,7 +52,9 @@ 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
|
+
import { roomTurnEnd } from "./room-turn-end.js";
|
|
57
|
+
import { pickComputer } from "./computer-selection.js";
|
|
56
58
|
import * as boat from "./boat.js";
|
|
57
59
|
import { cloudComputerRpc } from "./cloud-computer-tools.js";
|
|
58
60
|
import { TeamComputers, teamComputerAssignment, teamComputerCreate, teamComputerOwner } from "./team-computers.js";
|
|
@@ -65,6 +67,7 @@ import { liveCardKind, liveDecisionRefusal } from "../shared/live-approval.js";
|
|
|
65
67
|
import { boatAccountResourceChangeError, cloudBackendChangeError, vpsAliasResourceChangeError, } from "./cloud-backend.js";
|
|
66
68
|
import * as composio from "./composio.js";
|
|
67
69
|
import { connectorCallFromFrame, connectorRefusalText, connectorUnrecognizedText, evaluateConnectorTools } from "./connector-verdict.js";
|
|
70
|
+
import { connectorCardText } from "./connector-card-text.js";
|
|
68
71
|
import { chiefOfStaffSystemPrompt } from "./chief-of-staff.js";
|
|
69
72
|
import { buildRecall } from "./recall.js";
|
|
70
73
|
import { createMemoryUpkeep, upkeepEnabled } from "./memory-upkeep.js";
|
|
@@ -72,7 +75,7 @@ import { appendAboutMe, commitLearned, planLearned } from "./profile-learned.js"
|
|
|
72
75
|
import { canAccessTeam, canReachPeer, coordinatorSupervises, livePeerRoster, livePeerRosterBlock, peerAllowed, peerName, peerRosterSystemPrompt, peerStatus, peerStatusWords, reachablePeers, resolveTeammate, roomPeerRosterSystemPrompt, roomRosterLine, PEER_ACCESS_HELP } from "./peer-roster.js";
|
|
73
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";
|
|
74
77
|
import { attachForTurn, saveBotAttachment } from "./bot-attachment.js";
|
|
75
|
-
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";
|
|
76
79
|
import { sweepThreadEventLogs } from "./thread-retention.js";
|
|
77
80
|
import { ComputerControl } from "./computer-control.js";
|
|
78
81
|
import { augmentedPath, findCliCandidates, resetPathCache } from "./env-path.js";
|
|
@@ -81,7 +84,8 @@ import { appendUsage, parseUsageRange, readUsage, summarizeUsage, usageCsv, USAG
|
|
|
81
84
|
import { loadPlanUsage, planAccountsFromInstances } from "./plan-usage.js";
|
|
82
85
|
import { GroupUsageReader } from "./group-thread-usage.js";
|
|
83
86
|
import { ledgerCost } from "./model-prices.js";
|
|
84
|
-
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";
|
|
85
89
|
import { assertWithinBudget, noteSpend, spendAlertText, spendState, takeSpendAlert } from "./spend.js";
|
|
86
90
|
import { fleetAvailable, fleetRequest, fleetSocketPath } from "./fleet-client.js";
|
|
87
91
|
import { entitled } from "./enterprise.js";
|
|
@@ -97,7 +101,7 @@ import { McpOAuthError, McpSignInError, McpOAuthManager, mcpOAuthRedirectUri, wi
|
|
|
97
101
|
import { probeMcpServer } from "./mcp-probe.js";
|
|
98
102
|
import { GROUP_GOAL_MAX_TURNS, groupGoalAssignmentKey, groupGoalCompletionTurnId, groupGoalCoordinatorInstructions, groupGoalWorkerInstructions, parseGroupGoalDecision, resolveGroupGoalMember, selectGroupGoalCoordinator, } from "./group-goal-run.js";
|
|
99
103
|
import { BUILT_IN_DRIVERS } from "./drivers/builtIn.js";
|
|
100
|
-
import { openCodeProviderKeysAllowed,
|
|
104
|
+
import { openCodeProviderKeysAllowed, setOpenCodeProviderKeyPolicy } from "./drivers/acp/opencode-go.js";
|
|
101
105
|
import { getOrCreateChannel, mirrorActivity, mirrorExchange, mirrorReply } from "./comms-visibility.js";
|
|
102
106
|
import { readMessageText, recallMessages, recentMessages, searchMessagesAsync, closeMessageSearch, closeMessageDb, chatFollowups, cancelledChatFollowup, settleChatFollowups, threadsReferencing } from "./message-db.js";
|
|
103
107
|
import { briefCrossingLabel, claimRecallCrossings, recallCrossingLabel } from "./recall-disclosure.js";
|
|
@@ -118,17 +122,19 @@ import { ProviderRegistry } from "./harness/registry.js";
|
|
|
118
122
|
import { ManagedDesktopProviders } from "./managed-desktop.js";
|
|
119
123
|
import { computerKindForResource, ManagedDesktopPolicy } from "./managed-policy.js";
|
|
120
124
|
import { hostedModelPolicy, HOSTED_MODEL_POLICY_HEADER, HOSTED_PROVIDER_SETTINGS_ERROR } from "./hosted-models.js";
|
|
121
|
-
import {
|
|
125
|
+
import { CLOUD_HOME_SECRET_KEYS, CLOUD_IGNORED_KEYS, CLOUD_PAIRING_PATH, cloudHomeConfiguration, cloudHomeOffersPlace, cloudHomePlaceRefusal, createCloudPairing, firstCloudTurnPatch, readSignedBody, } from "./cloud-home.js";
|
|
122
126
|
import { CLOUD_PERSONAL_REFUSAL, settleCloudOwnership } from "./cloud-owner.js";
|
|
123
127
|
import { createCloudMoveRoutes, workspaceShared } from "./cloud-move-http.js";
|
|
124
128
|
import { RESTART_EXIT_CODE } from "./restart.js";
|
|
125
129
|
import { holdIncludedServices } from "./included-services.js";
|
|
126
130
|
import { selectDefaultModelSelection, withNewBotEffort } from "./default-model-selection.js";
|
|
131
|
+
import { threadModelFallback } from "./thread-model.js";
|
|
132
|
+
import { computerEngineMoveText, removedComputerInstanceIds, writeComputerEngineMoveLines } from "./computer-engine-removal.js";
|
|
127
133
|
import { cancelPeerApprovalsFor, cancelPeerApprovalsForThread, dismissStalePeerCards, peerApprovalFailure, requestPeerApproval, resolvePeerComms } from "./peer-approval.js";
|
|
128
134
|
import { peerDeliveryReceipt } from "./peer-delivery.js";
|
|
129
135
|
import { peerProvenanceNote, withPeerProvenance } from "./peer-provenance.js";
|
|
130
136
|
import { decideRoomPost, emptyRoomPostBudget } from "./room-post-budget.js";
|
|
131
|
-
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";
|
|
132
138
|
import * as tts from "./tts/index.js";
|
|
133
139
|
import { createDecider, deciderIncludedHere, deciderReady, deciderSavePatch, describeDecider } from "./decider/index.js";
|
|
134
140
|
import { decideRoomResponder } from "./decider/room-routing.js";
|
|
@@ -136,6 +142,7 @@ import { createLiveSession, liveAttachUrl, LiveSessionError } from "./live-call.
|
|
|
136
142
|
import { LiveCallController, LiveCallSignedOutError } from "./live-call-controller.js";
|
|
137
143
|
import { narrateTool, toUtterances } from "./tts/speech-text.js";
|
|
138
144
|
import { turnStartLogLine } from "./turn-log.js";
|
|
145
|
+
import { makeCapContinuationSubscriber } from "./turn-continuation.js";
|
|
139
146
|
import { buildRecoveryText, buildTurnContext, engineIsFresh, NATIVELY_REPLAYING_DRIVER_KINDS, peerMessageText } from "./turn-context.js";
|
|
140
147
|
import { Handoffs, handedStateUsable, recordHanded, renderUnseen, sessionStart, unseenMessages, withUnseenMessages } from "./delta-context.js";
|
|
141
148
|
import { extractTurnImages } from "./turn-images.js";
|
|
@@ -168,7 +175,7 @@ import { CalendarCallManager } from "./calendar-calls.js";
|
|
|
168
175
|
import { BUILT_IN_BROWSER_SYSTEM_PROMPT } from "./browser-engine.js";
|
|
169
176
|
import { BrowserRuntime, browserRuntimeEnv } from "./browser-runtime.js";
|
|
170
177
|
import { BrowserLive } from "./browser-live.js";
|
|
171
|
-
import { agentBrowserFrame, agentBrowserIntegration, browserEngineEncryptionKey, prepareBrowserSessionState, clearBrowserSessionState, closeBrowserSession, ensureChrome, installAgentBrowserBinary, resolveAgentBrowserBinary, browserEngineStatus, browserSessionId, describeBrowserEngine, } from "./browser-engine.js";
|
|
178
|
+
import { agentBrowserFrame, agentBrowserIntegration, browserEngineEncryptionKey, prepareBrowserSessionState, clearBrowserSessionState, closeBrowserSession, ensureChrome, installAgentBrowserBinary, resolveAgentBrowserBinary, browserEngineStatus, browserSessionId, describeBrowserEngine, setBrowserViewport, } from "./browser-engine.js";
|
|
172
179
|
import { createScreenFrameSource } from "./screen-frame-source.js";
|
|
173
180
|
import { screenFrameHash, screenSurfaceForTool, screenTouchingTool, settledFrameIsNews } from "./screen-frame-gate.js";
|
|
174
181
|
import { asSchedule, RoutineRequestService } from "./routine-requests.js";
|
|
@@ -208,10 +215,11 @@ import { createTeamBackup, importTeamBackup } from "./team-backup.js";
|
|
|
208
215
|
import { MAX_TEAM_BACKUP_BYTES } from "../shared/team-backup.js";
|
|
209
216
|
import { shouldMountLocalComputer } from "./local-routing.js";
|
|
210
217
|
import { autoLocalVmAttachable } from "./container-computer.js";
|
|
211
|
-
import { startAutoVmClaim } from "./auto-vm-claims.js";
|
|
218
|
+
import { lazyClaimWaitMs, startAutoVmClaim } from "./auto-vm-claims.js";
|
|
212
219
|
import { computerFreeAfterText, computerParkedText, computerStoppedWaitingText, computerWaitingText } from "./computer-wait.js";
|
|
213
220
|
import { modelContextWindow } from "./model-context-window.js";
|
|
214
|
-
import {
|
|
221
|
+
import { cloudPlaceRefusal, computerToolsRefusal, parseSurface, PlaceUnavailableError, placeUnavailable, resolveSurface, surfaceLabel, surfaceOfComputerKind, surfacePrompt } from "./surface.js";
|
|
222
|
+
import { cloudRefusal } from "../shared/place-view.js";
|
|
215
223
|
import { PendingTurnCancellations, ProviderTurnGenerationRegistry, RetiredTurnRegistry, guardTurnDispatch, isTurnAdmissionBlocked, isTurnEventQuarantined, } from "./turn-dispatch-guard.js";
|
|
216
224
|
import { createGracefulShutdown } from "./graceful-shutdown.js";
|
|
217
225
|
import { acquireDataDirLeaseForProcess } from "./data-dir-lease.js";
|
|
@@ -239,11 +247,13 @@ import { createHostedSlackRoutes } from "./routes/hosted-slack.js";
|
|
|
239
247
|
import { createBotPresetRoutes } from "./routes/bot-presets.js";
|
|
240
248
|
import { createBotMemoryRoutes } from "./routes/bot-memory.js";
|
|
241
249
|
import { createDeciderRoutes } from "./routes/decider.js";
|
|
250
|
+
import { createThreadModelRoutes } from "./routes/thread-models.js";
|
|
242
251
|
import { createDesktopViewer, desktopViewerUrl } from "./routes/desktop-viewer.js";
|
|
243
252
|
import { localDesktopTarget, localVmViewerStatus, viewerTargetId } from "./desktop-viewer-targets.js";
|
|
244
253
|
import { createAntigravityLeftoverRoutes } from "./routes/antigravity-leftovers.js";
|
|
245
254
|
import { findAntigravityLeftovers, removeAntigravityLeftovers } from "./drivers/antigravity-temp.js";
|
|
246
255
|
import { createLiveRoutes } from "./routes/live.js";
|
|
256
|
+
import { withScopeHint } from "./connector-scope-hint.js";
|
|
247
257
|
const PORT = Number(process.env.OMB_PORT || process.env.OGB_PORT || 8799);
|
|
248
258
|
const WEBHOOK_PORT = Number(process.env.OMB_WEBHOOK_PORT || PORT + 1);
|
|
249
259
|
// Behind a proxy or tunnel, the base URL senders should use (docs/self-hosting.md).
|
|
@@ -558,7 +568,7 @@ function cloudLendingTurn(capability) {
|
|
|
558
568
|
generation: capability.generation,
|
|
559
569
|
thread: store.messagesFor(capability.threadId),
|
|
560
570
|
starters: cloudThreadStarters(capability.threadId),
|
|
561
|
-
reportsFromOthers: store.messagesFor(capability.threadId).some(
|
|
571
|
+
reportsFromOthers: store.messagesFor(capability.threadId).some(cloudReportFromOthers),
|
|
562
572
|
ownerPerson: cloudProvenOwnerPerson,
|
|
563
573
|
cardAnswerer: cloudCardAnswerer,
|
|
564
574
|
routineRun: () => {
|
|
@@ -601,11 +611,13 @@ function cloudThreadStarters(threadId) {
|
|
|
601
611
|
function cloudLendingView(capability) {
|
|
602
612
|
return CLOUD_HOME !== null && cloudHomeLendingRefusal(cloudLendingTurn(capability)) === null;
|
|
603
613
|
}
|
|
604
|
-
/** On a Cloud home: a
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
function
|
|
614
|
+
/** On a Cloud home: a line that brings someone else's words into the
|
|
615
|
+
* conversation it lands in: a "post" webhook's payload, or a routine's
|
|
616
|
+
* report (its lifecycle card, carrying what the run said) of a routine the
|
|
617
|
+
* owner did not write, as it stands now. */
|
|
618
|
+
function cloudReportFromOthers(line) {
|
|
619
|
+
if (line.webhookPost)
|
|
620
|
+
return true;
|
|
609
621
|
if (line.kind !== "routine.run" || !line.routineRun)
|
|
610
622
|
return false;
|
|
611
623
|
const routine = routines?.listRoutines().find((candidate) => candidate.id === line.routineRun.routineId);
|
|
@@ -627,7 +639,7 @@ function cloudOwnerOnlyThread(threadId) {
|
|
|
627
639
|
const lastKey = last ? `${last.id}:${last.card?.answeredBy ? JSON.stringify(last.card.answeredBy) : ""}` : undefined;
|
|
628
640
|
if (cached && cached.length === thread.length && cached.last === lastKey)
|
|
629
641
|
return cached.owner;
|
|
630
|
-
const owner = ownerOnlyConversation(thread, cloudProvenOwnerPerson, cloudCardAnswerer) && !thread.some(
|
|
642
|
+
const owner = ownerOnlyConversation(thread, cloudProvenOwnerPerson, cloudCardAnswerer) && !thread.some(cloudReportFromOthers);
|
|
631
643
|
if (ownerOnlyCache.size > 5_000)
|
|
632
644
|
ownerOnlyCache.clear();
|
|
633
645
|
ownerOnlyCache.set(threadId, { length: thread.length, last: lastKey, owner });
|
|
@@ -776,16 +788,11 @@ function approvalShape(shape) {
|
|
|
776
788
|
/** Who opens a routine's results conversation on a Cloud home: the writer
|
|
777
789
|
* of this request, else the owner for a routine that is theirs (they wrote
|
|
778
790
|
* it as it stands, or they are its writer: cloud-owner.ts), else its last
|
|
779
|
-
* writer, else nobody.
|
|
780
|
-
* devices can create, edit or rotate one (admin scope), so it runs at the
|
|
781
|
-
* bot's own level, in its folder, as on the desktop. Its payload still never
|
|
782
|
-
* reaches the lent Mac: cloud-lending.ts refuses every webhook run. */
|
|
791
|
+
* writer, else nobody. */
|
|
783
792
|
function routineOpener(routineId) {
|
|
784
793
|
if (routineWriterInFlight)
|
|
785
794
|
return routineWriterInFlight;
|
|
786
795
|
const routine = routines?.listRoutines().find((candidate) => candidate.id === routineId);
|
|
787
|
-
if (!routine && CLOUD_OWNER_KEY && webhooks.list().some((hook) => hook.id === routineId))
|
|
788
|
-
return CLOUD_OWNER_KEY;
|
|
789
796
|
if (routine && CLOUD_OWNER_KEY && cloudRoutineAuthors?.authored(routineId, routine))
|
|
790
797
|
return CLOUD_OWNER_KEY;
|
|
791
798
|
const writer = cloudRoutineAuthors?.writer(routineId);
|
|
@@ -832,12 +839,13 @@ function openerFrom(fromThreadId) {
|
|
|
832
839
|
return person;
|
|
833
840
|
return person && !cloudOwnerPerson(person) ? person : CLOUD_NOBODY_KEY;
|
|
834
841
|
}
|
|
835
|
-
/** The model a thread a bot opens from one of its own threads starts on
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
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. */
|
|
841
849
|
function parentThreadModel(botId, parentThreadId) {
|
|
842
850
|
// The stored profile, never a per-thread projection: the comparison is
|
|
843
851
|
// against the defaults the new thread would otherwise get.
|
|
@@ -868,7 +876,7 @@ function guestWorkThread(from, target, fromThreadId, brief) {
|
|
|
868
876
|
* to have written, or a conversation or room a revoked session opened. What
|
|
869
877
|
* the owner is told about one, and when its engine cannot be confined. */
|
|
870
878
|
const ROUTINE_FROM_BEFORE = "This routine was made before this update. Open it and save it once to run it with full access.";
|
|
871
|
-
const CONVERSATION_FROM_BEFORE = "This conversation is from before
|
|
879
|
+
const CONVERSATION_FROM_BEFORE = "This conversation is from before My Cloud was only yours, so this bot works in it without a shell. Start a new conversation to use it fully.";
|
|
872
880
|
function confinedWhy(threadId) {
|
|
873
881
|
const run = activeRoutineRunForThread(threadId);
|
|
874
882
|
const writer = run ? cloudRoutineAuthors?.writer(run.routineId) : undefined;
|
|
@@ -877,7 +885,7 @@ function confinedWhy(threadId) {
|
|
|
877
885
|
function guestEngineRefusal(threadId) {
|
|
878
886
|
const why = confinedWhy(threadId);
|
|
879
887
|
return why === ROUTINE_FROM_BEFORE ? `This bot's engine can't run it that way. ${why}`
|
|
880
|
-
: "This conversation is from before
|
|
888
|
+
: "This conversation is from before My Cloud was only yours, and this bot's engine can't work in it. Start a new conversation.";
|
|
881
889
|
}
|
|
882
890
|
/** On a Cloud home: a turn a guest drives, which runs in Ask whatever the
|
|
883
891
|
* bot's own level (no Full access, no Auto reviewer, no saved commands), with
|
|
@@ -1528,8 +1536,6 @@ const openCodeKeysAllowed = () => openCodeProviderKeysAllowed({
|
|
|
1528
1536
|
sharedSignIn: sharedSignIn(signInAllowList()),
|
|
1529
1537
|
});
|
|
1530
1538
|
setOpenCodeProviderKeyPolicy(openCodeKeysAllowed);
|
|
1531
|
-
// Keys the owner saved for OpenCode in Settings reach it on every server.
|
|
1532
|
-
setOpenCodeOwnProviderKeys(() => openCodeProviderKeys(cfg));
|
|
1533
1539
|
let openCodeKeysLastAllowed = openCodeKeysAllowed();
|
|
1534
1540
|
/** OpenCode lists a provider's models only while it may read that
|
|
1535
1541
|
* provider's key. When enrollment or the sign-in list changes the answer,
|
|
@@ -1667,6 +1673,9 @@ const managedPolicy = new ManagedDesktopPolicy({ onChange: () => {
|
|
|
1667
1673
|
broadcast({ kind: "config", ...configStatus() });
|
|
1668
1674
|
resyncOpenCodeProviderKeys();
|
|
1669
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() }));
|
|
1670
1679
|
/** A computer kind this server will not use, refused before anything is
|
|
1671
1680
|
* prepared: a Cloud home never offers this computer or a Local VM
|
|
1672
1681
|
* (cloud-home.ts), and an enrolled organisation may disallow any kind. */
|
|
@@ -1677,8 +1686,6 @@ function computerPlaceRefusal(kind) {
|
|
|
1677
1686
|
const managed = managedPolicy.computerRefusal(kind);
|
|
1678
1687
|
return managed ? { message: managed, code: "managed_policy" } : undefined;
|
|
1679
1688
|
}
|
|
1680
|
-
/** Cloud chosen with no Boat account: a Cloud home suggests the browser, not a Local VM. */
|
|
1681
|
-
const BOAT_NOT_CONFIGURED = boatNotConfiguredMessage(Boolean(CLOUD_HOME));
|
|
1682
1689
|
/** Why the organisation refuses this instance for bots, or undefined. */
|
|
1683
1690
|
function policyModelRefusal(instance) {
|
|
1684
1691
|
const engine = BUILT_IN_DRIVERS.find(driver => driver.driverKind === instance.driverKind)?.metadata.displayName;
|
|
@@ -1794,6 +1801,11 @@ function externalRuntimeMayCall(method, path) {
|
|
|
1794
1801
|
return path === "/api/internal/agents" || /^\/api\/internal\/delegations\/[\w-]+$/.test(path);
|
|
1795
1802
|
return method === "POST" && (path === "/api/internal/ask-bot" || path === "/api/internal/delegate-bot");
|
|
1796
1803
|
}
|
|
1804
|
+
/** An external runtime polls check/wait_delegation for what it hands off, so
|
|
1805
|
+
* the result must not also wake the bot's own engine on the pinned thread. */
|
|
1806
|
+
function externalCompletion(capability) {
|
|
1807
|
+
return capability.generation === EXTERNAL_RUNTIME_GENERATION ? { completionOwner: "external" } : {};
|
|
1808
|
+
}
|
|
1797
1809
|
function externalRuntimeCapability(header) {
|
|
1798
1810
|
const grant = authorizeExternalRuntime(EXTERNAL_RUNTIMES_FILE, header, id => store.bot(id));
|
|
1799
1811
|
if (!grant)
|
|
@@ -2051,6 +2063,9 @@ function agentsIntegration(botId, threadId, depth, skillAuthoring, generation, r
|
|
|
2051
2063
|
const speaking = botForThread(botId, threadId) ?? store.bot(botId);
|
|
2052
2064
|
return tts.voiceReady(cfg, speaking?.voice) && speaking?.voiceNotes !== false ? "1" : "0";
|
|
2053
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",
|
|
2054
2069
|
},
|
|
2055
2070
|
};
|
|
2056
2071
|
}
|
|
@@ -2087,14 +2102,63 @@ class ComputerWaitParked extends Error {
|
|
|
2087
2102
|
this.generation = generation;
|
|
2088
2103
|
}
|
|
2089
2104
|
}
|
|
2090
|
-
/** A place
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2094
|
-
function
|
|
2105
|
+
/** A failed place never sticks: an Auto-recorded pin to it (a
|
|
2106
|
+
* select_computer switch, or where an earlier Auto turn landed) is the
|
|
2107
|
+
* machine's memory, not a choice, so the next message runs on Auto. A
|
|
2108
|
+
* person's pin stays. */
|
|
2109
|
+
function clearFailedAutoPin(botId, threadId, place) {
|
|
2110
|
+
const pinned = store.taskByThread(botId, threadId);
|
|
2111
|
+
if (pinned?.surface === place && pinned.surfaceSource === "auto") {
|
|
2112
|
+
store.patchTask(botId, threadId, { surface: undefined, surfaceSource: undefined });
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
/** A place the turn was told to use failed to attach, read as one state of
|
|
2116
|
+
* shared/place-view.ts: a cloud computer's refusal by its code or words, any
|
|
2117
|
+
* other place by its own words. Its source decides the one way on. A
|
|
2118
|
+
* cancelled setup, a parked computer wait and an already-read place failure
|
|
2119
|
+
* pass through as they are. The one wrapper for a bot thread and a room
|
|
2120
|
+
* turn, at dispatch and at a cloud computer's start on its first call. */
|
|
2121
|
+
async function asPlaceFailure(error, place, source, bot, backend = "box") {
|
|
2095
2122
|
if (error instanceof DirectTurnSetupCancelled || error instanceof ComputerWaitParked || error instanceof PlaceUnavailableError)
|
|
2096
2123
|
return error;
|
|
2097
|
-
|
|
2124
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
2125
|
+
const cause = failure.message.trim().replace(/[\s.]+$/, "");
|
|
2126
|
+
if (place !== "cloud" || backend === "vps") {
|
|
2127
|
+
return placeUnavailable(place, {
|
|
2128
|
+
state: "place-failed", params: { bot: bot.name, cause: `${cause.charAt(0).toUpperCase()}${cause.slice(1)}.` }, source,
|
|
2129
|
+
}, Boolean(CLOUD_HOME));
|
|
2130
|
+
}
|
|
2131
|
+
// A Boat refusal keeps the provider's own status and code (boat.ts
|
|
2132
|
+
// boatRefusal); a failed read of the account keeps its status.
|
|
2133
|
+
const { boatCode, boatStatus, status } = failure;
|
|
2134
|
+
const providerStatus = typeof boatStatus === "number" ? boatStatus : typeof status === "number" ? status : undefined;
|
|
2135
|
+
const read = cloudRefusal({
|
|
2136
|
+
message: failure.message,
|
|
2137
|
+
...(typeof boatCode === "string" ? { code: boatCode } : {}),
|
|
2138
|
+
...(providerStatus !== undefined ? { status: providerStatus } : {}),
|
|
2139
|
+
}, boat.boatAccount(cfg)?.included === true);
|
|
2140
|
+
const row = { state: read.state, params: { bot: bot.name, ...read.params }, source };
|
|
2141
|
+
if (row.state === "cc-at-once")
|
|
2142
|
+
row.params.holders = await cloudComputerHolders(bot.id);
|
|
2143
|
+
return placeUnavailable(place, row, Boolean(CLOUD_HOME));
|
|
2144
|
+
}
|
|
2145
|
+
/** The bots that hold this account's cloud computers, other than `botId`,
|
|
2146
|
+
* from this installation's own Boat list. Empty when it can't be read. */
|
|
2147
|
+
async function cloudComputerHolders(botId) {
|
|
2148
|
+
try {
|
|
2149
|
+
const listed = await boat.listManagedBoats(cfg, managedBoatOwners(), { adoptLegacy: false });
|
|
2150
|
+
return [...new Set(listed.instances
|
|
2151
|
+
.filter((instance) => instance.ownerBotId && instance.ownerBotId !== botId && instance.ownerName)
|
|
2152
|
+
.map((instance) => instance.ownerName))];
|
|
2153
|
+
}
|
|
2154
|
+
catch {
|
|
2155
|
+
return [];
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
/** Cloud chosen with no cloud computers here: on a Cloud home they are the
|
|
2159
|
+
* plan's, so they are only unavailable; anywhere else they need a Boat key. */
|
|
2160
|
+
function boatNotConfigured(bot, source) {
|
|
2161
|
+
return placeUnavailable("cloud", { state: CLOUD_HOME ? "cc-unavailable" : "cc-needs-key", params: { bot: bot.name }, source }, Boolean(CLOUD_HOME));
|
|
2098
2162
|
}
|
|
2099
2163
|
const directTurnDispatchClaims = new Map();
|
|
2100
2164
|
const directTurnGenerationByThread = new Map();
|
|
@@ -2520,6 +2584,7 @@ const browserRuntime = new BrowserRuntime({
|
|
|
2520
2584
|
browserLive.closeForSession(session);
|
|
2521
2585
|
return closed;
|
|
2522
2586
|
},
|
|
2587
|
+
applyViewport: (spec) => setBrowserViewport(spec.command, spec.env),
|
|
2523
2588
|
});
|
|
2524
2589
|
const browserLive = new BrowserLive({ runtime: browserRuntime });
|
|
2525
2590
|
// Temporary profiles last for this server run, but are never saved to disk.
|
|
@@ -2704,7 +2769,7 @@ function controlIntegration(botId, threadId, generation, machine = {}) {
|
|
|
2704
2769
|
depth: 0,
|
|
2705
2770
|
kind: "computer",
|
|
2706
2771
|
...(localVmTarget ? { localVmTarget } : {}),
|
|
2707
|
-
...(boxId ? { boxId } : {}),
|
|
2772
|
+
...(boxId !== undefined ? { boxId } : {}),
|
|
2708
2773
|
...(teamComputerTurns.get(threadId) ? { teamComputerId: teamComputerTurns.get(threadId).computerId } : {}),
|
|
2709
2774
|
skillAuthoring: false,
|
|
2710
2775
|
createdBots: 0,
|
|
@@ -2767,17 +2832,89 @@ function askBotAndWait(targetBotId, message, depth, fromBotId, fromThreadId, tar
|
|
|
2767
2832
|
}).catch((err) => finish({ status: "error", text: `(couldn't start that bot: ${err instanceof Error ? err.message : String(err)})` }));
|
|
2768
2833
|
});
|
|
2769
2834
|
}
|
|
2770
|
-
// New bots honor setup's saved choice;
|
|
2771
|
-
// While enrolled, a Company model that can run beats
|
|
2772
|
-
// engine, and the organisation's policy is respected;
|
|
2773
|
-
|
|
2835
|
+
// New bots honor setup's saved choice; without one, an engine that can run a
|
|
2836
|
+
// turn now (Claude first). While enrolled, a Company model that can run beats
|
|
2837
|
+
// a signed-out personal engine, and the organisation's policy is respected;
|
|
2838
|
+
// otherwise inert. `null` leaves the saved choice out (moveOffComputerEngine).
|
|
2839
|
+
async function defaultSelection(saved = cfg.defaultModelSelection ?? null) {
|
|
2774
2840
|
if (hostedModels)
|
|
2775
|
-
return hostedModels.select(
|
|
2776
|
-
return selectDefaultModelSelection(await registry.describe(),
|
|
2841
|
+
return hostedModels.select(saved ?? undefined);
|
|
2842
|
+
return selectDefaultModelSelection(await registry.describe(), saved ?? undefined, {
|
|
2777
2843
|
company: (instanceId) => managedDesktop.owns(instanceId),
|
|
2778
2844
|
refusal: (instance) => policyModelRefusal(instance),
|
|
2779
2845
|
});
|
|
2780
2846
|
}
|
|
2847
|
+
/** The Computer engine was removed: it ran a whole turn on Boat's own agent.
|
|
2848
|
+
* Everything still set to it moves, once, to the engine a new bot gets
|
|
2849
|
+
* (defaultSelection, the one picker), or the one a new bot would get without
|
|
2850
|
+
* the saved choice when that cannot run: the saved default and new-bot
|
|
2851
|
+
* defaults, each bot and conversation (Store.retireInstances keeps each one
|
|
2852
|
+
* where it worked), and an automatic-recovery backup, which could never run
|
|
2853
|
+
* and is cleared. Each moved conversation says so in one plain line. When no
|
|
2854
|
+
* engine is available yet it runs again the next time the engines are read
|
|
2855
|
+
* (describeInstances): an install, a sign-in, a key or a Company engine. At
|
|
2856
|
+
* start it sees what a new bot made then would see; a policy that arrives
|
|
2857
|
+
* later refuses an engine for a moved bot the way it does for any bot. A
|
|
2858
|
+
* hosted workspace's reconcile has already moved every bot onto its own
|
|
2859
|
+
* models. */
|
|
2860
|
+
async function moveOffComputerEngine() {
|
|
2861
|
+
if (hostedModels)
|
|
2862
|
+
return;
|
|
2863
|
+
const removed = removedComputerInstanceIds(cfg.instances);
|
|
2864
|
+
const named = (selection) => Boolean(selection && removed.has(selection.instanceId));
|
|
2865
|
+
if (named(cfg.automaticRecovery?.backup)) {
|
|
2866
|
+
saveConfig({ automaticRecovery: { enabled: false } });
|
|
2867
|
+
cfg.automaticRecovery = { enabled: false };
|
|
2868
|
+
}
|
|
2869
|
+
const defaults = named(cfg.defaultModelSelection) || named(cfg.newBotDefaults?.profile.modelSelection);
|
|
2870
|
+
if (!defaults && !store.bots.some(bot => named(bot.modelSelection) || bot.fallback?.some(named) ||
|
|
2871
|
+
store.tasks(bot.id).some(task => named(task.modelSelection))))
|
|
2872
|
+
return;
|
|
2873
|
+
let replacement = named(cfg.defaultModelSelection) ? { instanceId: "", model: "" } : await defaultSelection();
|
|
2874
|
+
if (!replacement.instanceId)
|
|
2875
|
+
replacement = await defaultSelection(null);
|
|
2876
|
+
if (!replacement.instanceId) {
|
|
2877
|
+
console.warn("[engines] the Computer engine was removed and no other engine is available yet; its bots move when one is");
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
if (defaults) {
|
|
2881
|
+
// One write keeps both saved defaults on the replacement (saveConfig
|
|
2882
|
+
// syncs them), and the copies in memory follow at once, so a bot made
|
|
2883
|
+
// before the next start never lands on the removed engine.
|
|
2884
|
+
const newBotDefaults = cfg.newBotDefaults && newBotDefaultsSchema.parse({
|
|
2885
|
+
...cfg.newBotDefaults, profile: { ...cfg.newBotDefaults.profile, modelSelection: replacement },
|
|
2886
|
+
});
|
|
2887
|
+
saveConfig(newBotDefaults ? { newBotDefaults } : { defaultModelSelection: replacement });
|
|
2888
|
+
cfg.defaultModelSelection = replacement;
|
|
2889
|
+
if (newBotDefaults)
|
|
2890
|
+
cfg.newBotDefaults = newBotDefaults;
|
|
2891
|
+
}
|
|
2892
|
+
const instance = registry.get(replacement.instanceId);
|
|
2893
|
+
const engine = instance?.displayName ?? instance?.driverKind ?? replacement.instanceId;
|
|
2894
|
+
const moves = store.retireInstances(removed, replacement, {
|
|
2895
|
+
driverKind: instance?.driverKind,
|
|
2896
|
+
canWorkOnCloud: canWorkOnCloud(cloudEngine(instance)),
|
|
2897
|
+
keepCloud: (bot) => boat.boatConfigured(cfg) && bot.cloudBackend !== "vps" && !inheritedTeamComputer(bot),
|
|
2898
|
+
});
|
|
2899
|
+
writeComputerEngineMoveLines(moves, (move) => {
|
|
2900
|
+
const bot = store.bot(move.botId);
|
|
2901
|
+
if (bot)
|
|
2902
|
+
store.appendMessage(move.threadId, { role: "bot", kind: "activity", tool: { name: computerEngineMoveText(move, bot.name, engine), ok: true } });
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
let computerEngineMoveRunning = null;
|
|
2906
|
+
/** The move, once at a time; it returns at once when nothing names the
|
|
2907
|
+
* removed engine any more. A failure is logged and never rejects, so it never
|
|
2908
|
+
* stops this server from listening. Bots whose save failed are unchanged and
|
|
2909
|
+
* move the next time the engines are read (describeInstances) or a turn
|
|
2910
|
+
* reaches one of them (startTurn), so a server no app reads the engines from
|
|
2911
|
+
* still moves them. */
|
|
2912
|
+
function retryComputerEngineMove() {
|
|
2913
|
+
computerEngineMoveRunning ??= moveOffComputerEngine()
|
|
2914
|
+
.catch((error) => console.warn(`[engines] moving bots off the removed Computer engine failed: ${error instanceof Error ? error.message : String(error)}`))
|
|
2915
|
+
.finally(() => { computerEngineMoveRunning = null; });
|
|
2916
|
+
return computerEngineMoveRunning;
|
|
2917
|
+
}
|
|
2781
2918
|
function checkedModelSelection(raw, current, requireAvailableModel = false) {
|
|
2782
2919
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2783
2920
|
return { ok: false, status: 400, error: "modelSelection must be an object" };
|
|
@@ -2879,6 +3016,62 @@ function checkedTaskModelSwitch(current, raw, updateBotDefault, resetApprovalToA
|
|
|
2879
3016
|
}
|
|
2880
3017
|
return checked;
|
|
2881
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
|
+
}
|
|
2882
3075
|
function checkedExportSkillNames(value, bots) {
|
|
2883
3076
|
// Sharing instructions is explicit: ordinary package exports include none.
|
|
2884
3077
|
if (value === undefined)
|
|
@@ -3075,13 +3268,27 @@ function checkedMemberIds(value) {
|
|
|
3075
3268
|
return { ok: true, memberIds };
|
|
3076
3269
|
}
|
|
3077
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;
|
|
3078
3276
|
/** Preset bots for New bot, from shared files and the organization (presets.ts). */
|
|
3079
3277
|
const presetStore = createPresetStore();
|
|
3080
|
-
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);
|
|
3081
3279
|
const teamComputers = new TeamComputers(join(DATA_DIR, "team-computers.json"), ENVIRONMENT_ID);
|
|
3082
3280
|
let followupsReady = false;
|
|
3083
3281
|
const sendSequencer = new SendSequencer();
|
|
3084
|
-
|
|
3282
|
+
// Before the new-bot default is read: a saved default may name the engine.
|
|
3283
|
+
await retryComputerEngineMove();
|
|
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; });
|
|
3085
3292
|
store.seedIfEmpty();
|
|
3086
3293
|
hostedModels?.reconcile(store);
|
|
3087
3294
|
// Skills library boot sweep (features.skillsLibrary): migrate per-bot
|
|
@@ -3135,11 +3342,13 @@ if (browserCleanupReferencesReconciled)
|
|
|
3135
3342
|
* than the desktop window did. Stripped here rather than at each call site
|
|
3136
3343
|
* so a new broadcast cannot forget. */
|
|
3137
3344
|
let activeCoordinationForThread = (_threadId) => false;
|
|
3138
|
-
|
|
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) => {
|
|
3139
3348
|
// Time-based snoozes heal on read against the server clock — no client
|
|
3140
3349
|
// timer, no device skew. The 0 sentinel ("until new activity") is not a
|
|
3141
3350
|
// time and survives reads; only a wake event in the store clears it.
|
|
3142
|
-
const { snoozedUntil, ...base } = toWireTask(task);
|
|
3351
|
+
const { snoozedUntil, ...base } = toWireTask(task, botModel);
|
|
3143
3352
|
const coordinated = { ...base, waitingForTeammates: activeCoordinationForThread(task.threadId) && !task.busy };
|
|
3144
3353
|
const asleep = snoozedUntil === 0 || (snoozedUntil !== undefined && snoozedUntil > Date.now());
|
|
3145
3354
|
return asleep ? { ...coordinated, snoozedUntil } : coordinated;
|
|
@@ -3153,7 +3362,7 @@ const wireBot = (bot) => {
|
|
|
3153
3362
|
? { ...rest, approvalMode: "ask", autoApprove: false }
|
|
3154
3363
|
: rest;
|
|
3155
3364
|
return { ...visible, waitingForTeammates: activeCoordinationForThread(bot.threadId) && !threadBusy(bot.id, bot.threadId),
|
|
3156
|
-
avatarUrl: visible.avatarUrl ?? null, ...(tasks ? { tasks: tasks.map(wireTask) } : {}) };
|
|
3365
|
+
avatarUrl: visible.avatarUrl ?? null, ...(tasks ? { tasks: tasks.map((task) => wireTask(task, bot.modelSelection)) } : {}) };
|
|
3157
3366
|
};
|
|
3158
3367
|
function toolScopeForTurn(botId) {
|
|
3159
3368
|
const bot = store.bot(botId);
|
|
@@ -3168,7 +3377,7 @@ function toolScopeForTurn(botId) {
|
|
|
3168
3377
|
* can validate it before sending the confirmation that makes it effective. */
|
|
3169
3378
|
const wireTrustedApprovalBot = (bot) => {
|
|
3170
3379
|
const { resumeCursors: _resumeCursors, tasks, approvalGrant: _approvalGrant, lastProfileRequestId: _lastProfileRequestId, lastTeamSetupReceipt: _lastTeamSetupReceipt, packageBase: _packageBase, assignedSkills: _assignedSkills, ...rest } = bot;
|
|
3171
|
-
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)) } : {}) };
|
|
3172
3381
|
};
|
|
3173
3382
|
/** A settings-based preview, not a receipt of a dispatched turn. No
|
|
3174
3383
|
* provisioning or credentials are needed to inspect it. The selected engine
|
|
@@ -3190,7 +3399,6 @@ function previewSystemPrompt(bot) {
|
|
|
3190
3399
|
const computerPromptKind = resolveComputerPromptKind({
|
|
3191
3400
|
kind: previewComputer === "vm" ? "vm" : previewComputer === "cloud"
|
|
3192
3401
|
? bot.cloudBackend === "vps" ? "vps" : "box" : previewComputer === "local" ? "local" : null,
|
|
3193
|
-
driverKind: instance?.driverKind,
|
|
3194
3402
|
vmPrivate: localVmMode(cfg) === "per-bot",
|
|
3195
3403
|
});
|
|
3196
3404
|
const peers = reachablePeers(store.bots, bot);
|
|
@@ -3230,7 +3438,7 @@ function previewSystemPrompt(bot) {
|
|
|
3230
3438
|
computer: previewPlan.computer && previewPlan.computer !== "off" && computerPromptKind ? previewPlan.computer : null,
|
|
3231
3439
|
browser: previewPlan.computer === undefined ? false : previewPlan.browser,
|
|
3232
3440
|
}, { note: previewPlan.note, cloudHome: Boolean(CLOUD_HOME) }) },
|
|
3233
|
-
{ id: "cloud-home", label: "
|
|
3441
|
+
{ id: "cloud-home", label: "My Cloud", text: CLOUD_HOME ? cloudHomePrompt(agentsMounted && lendingEnabled()) : "" },
|
|
3234
3442
|
{ id: "composio", label: "Connected apps", text: caps?.composioMcp && bot.composio !== false && composio.configured(cfg) ? composioSystemPrompt(bot.connectorTools) + describeConnectorScopes(bot.connectorScopes) : "" },
|
|
3235
3443
|
{ id: "mcp", label: "MCP servers", text: caps?.customMcp ? customMcpPrompt(Object.keys(engineMcpServers(bot))) : "" },
|
|
3236
3444
|
{ id: "browser", label: "Browser", text: previewPlan.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
@@ -3823,7 +4031,7 @@ const storedAvatarExists = (avatarUrl) => attachmentExists(avatarUrl.slice("/api
|
|
|
3823
4031
|
const publicBot = (bot) => ({
|
|
3824
4032
|
...wireBot(bot),
|
|
3825
4033
|
...messagePage(bot.threadId, DEFAULT_PAGE),
|
|
3826
|
-
tasks: store.tasks(bot.id).map(wireTask),
|
|
4034
|
+
tasks: store.tasks(bot.id).map((task) => wireTask(task, bot.modelSelection)),
|
|
3827
4035
|
});
|
|
3828
4036
|
const publicBotQueuedMessages = () => queuedSteerSnapshot((botId, threadId) => Boolean(store.taskByThread(botId, threadId)));
|
|
3829
4037
|
// busyBotId names only the speaker that currently owns the provider process.
|
|
@@ -4015,6 +4223,15 @@ function updateChannel(groupId, value) {
|
|
|
4015
4223
|
throw Object.assign(new Error("invalid default responder"), { status: 400 });
|
|
4016
4224
|
patch.defaultResponder = responder;
|
|
4017
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
|
+
}
|
|
4018
4235
|
if (body.cwd !== undefined) {
|
|
4019
4236
|
if (existing.dm)
|
|
4020
4237
|
throw Object.assign(new Error("direct-message channels cannot have a working folder"), { status: 400 });
|
|
@@ -4335,7 +4552,14 @@ function publicGroupState(record) {
|
|
|
4335
4552
|
usage = groupUsageReader.forThread(group.threadId);
|
|
4336
4553
|
}
|
|
4337
4554
|
catch { /* accounting must not block chat */ }
|
|
4338
|
-
return {
|
|
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
|
+
};
|
|
4339
4563
|
}
|
|
4340
4564
|
function beginGroupTurnOperation(groupId, threadId, botIds = []) {
|
|
4341
4565
|
const operation = {
|
|
@@ -5449,6 +5673,10 @@ bus.subscribe((event) => {
|
|
|
5449
5673
|
else if (event.type !== "session.exited")
|
|
5450
5674
|
watchdog.touch(event.threadId);
|
|
5451
5675
|
});
|
|
5676
|
+
// Automatic continuity: a turn that died on its budget or on tool errors gets
|
|
5677
|
+
// a `Continue:` task on the same bot, seeded with the persisted handoff — the
|
|
5678
|
+
// work moves forward without a person noticing and re-dispatching.
|
|
5679
|
+
bus.subscribe(makeCapContinuationSubscriber({ store, startTurn }));
|
|
5452
5680
|
// Memory journal turn boundary (server/memory-journal.ts). A bot's own
|
|
5453
5681
|
// file-tool writes to MEMORY.md and memory/ have no hook to tap, so the
|
|
5454
5682
|
// diff against the baseline taken at dispatch is made when the turn
|
|
@@ -5945,7 +6173,7 @@ function inheritedTeamComputer(bot) {
|
|
|
5945
6173
|
return teamComputers.forBot(bot);
|
|
5946
6174
|
}
|
|
5947
6175
|
function teamComputerPrompt(computer) {
|
|
5948
|
-
return computer ? `Your team shares the
|
|
6176
|
+
return computer ? `Your team shares the cloud computer ${JSON.stringify(computer.name)}. Its desktop files and desktop browser logins are shared with other Auto bots in your team; only one turn may drive it at a time. The separate built-in Browser is not this desktop and does not automatically share its logins.` : "";
|
|
5949
6177
|
}
|
|
5950
6178
|
function botComputerControlKey(bot) {
|
|
5951
6179
|
const computer = inheritedTeamComputer(bot);
|
|
@@ -5956,21 +6184,26 @@ async function computerCallGate(internalCapability) {
|
|
|
5956
6184
|
const snapshot = botComputerControlSnapshot(botId, internalCapability.teamComputerId);
|
|
5957
6185
|
const slot = autoVmClaims.get(internalCapability.threadId);
|
|
5958
6186
|
const lazyClaim = slot && slot.owner.generation === internalCapability.generation ? slot : undefined;
|
|
5959
|
-
|
|
5960
|
-
|
|
6187
|
+
const waitMs = snapshot.held ? null : lazyClaimWaitMs(lazyClaim, LAZY_VM_CLAIM_GRACE_MS);
|
|
6188
|
+
if (lazyClaim && waitMs !== null) {
|
|
6189
|
+
// First screen tools/call on a lazily-attached computer (issue
|
|
5961
6190
|
// #1361): fire the exclusive claim — once — and give it a moment
|
|
5962
6191
|
// to land. A free, ready VM claims in the time of one container
|
|
5963
6192
|
// inspect, so this call then proceeds with an honest answer;
|
|
5964
6193
|
// only a claim still queued behind another holder answers held
|
|
5965
|
-
// below, and then the contention text is true.
|
|
5966
|
-
//
|
|
6194
|
+
// below, and then the contention text is true. A cloud computer's
|
|
6195
|
+
// claim creates or wakes it, so every call waits on that start
|
|
6196
|
+
// (graceMs) instead of being told someone else holds it. Keyed on
|
|
6197
|
+
// the slot, never on the thread's turn-computer entry: a bind this
|
|
5967
6198
|
// turn abandoned earlier (a VPS that turned out to be asleep)
|
|
5968
6199
|
// must not hide the unclaimed VM and let the call through.
|
|
5969
6200
|
startAutoVmClaim(autoVmClaims, internalCapability.threadId, internalCapability.generation);
|
|
6201
|
+
let timer;
|
|
5970
6202
|
await Promise.race([
|
|
5971
6203
|
lazyClaim.begin ?? Promise.resolve(),
|
|
5972
|
-
new Promise((resolve) => setTimeout(resolve,
|
|
6204
|
+
new Promise((resolve) => { timer = setTimeout(resolve, waitMs); }),
|
|
5973
6205
|
]);
|
|
6206
|
+
clearTimeout(timer);
|
|
5974
6207
|
}
|
|
5975
6208
|
if (!snapshot.held && lazyClaim?.failed === true) {
|
|
5976
6209
|
// A rejected lazy claim (gate finding F1, issue #1361): the
|
|
@@ -5990,10 +6223,11 @@ async function computerCallGate(internalCapability) {
|
|
|
5990
6223
|
}
|
|
5991
6224
|
if (!snapshot.held && lazyClaim?.lazy && lazyClaim.begin && !lazyClaim.claimed) {
|
|
5992
6225
|
// The claim fired and is still waiting on the exclusive bind:
|
|
5993
|
-
// another turn genuinely holds this desktop right now.
|
|
6226
|
+
// another turn genuinely holds this desktop right now. A slow claim
|
|
6227
|
+
// says what it is still doing instead.
|
|
5994
6228
|
return {
|
|
5995
6229
|
held: true, helpOpen: false,
|
|
5996
|
-
blockedReason: "Another thread is using this computer. This call was not performed. Pause computer work until that thread finishes, then take a fresh screenshot before acting.",
|
|
6230
|
+
blockedReason: lazyClaim.pendingReason ?? "Another thread is using this computer. This call was not performed. Pause computer work until that thread finishes, then take a fresh screenshot before acting.",
|
|
5997
6231
|
};
|
|
5998
6232
|
}
|
|
5999
6233
|
const computer = turnComputerResources.get(internalCapability.threadId);
|
|
@@ -6019,11 +6253,6 @@ function teamComputerInUse(computer) {
|
|
|
6019
6253
|
[...teamComputerTurns.values()].some(turn => turn.computerId === computer.id) ||
|
|
6020
6254
|
store.bots.some(bot => computer.section !== null && sectionKey(bot.section) === computer.section && (botHasActiveTurn(bot.id) || Boolean(routines?.activeRunForBot(bot.id)) || computerControl.snapshot(bot.id).held));
|
|
6021
6255
|
}
|
|
6022
|
-
function assertTeamControlCanBeTaken(computerId) {
|
|
6023
|
-
if ([...teamComputerTurns.values()].some(turn => turn.computerId === computerId && turn.remoteAgent)) {
|
|
6024
|
-
throw Object.assign(new Error("Stop the Computer engine's active turn before taking control of this shared desktop"), { status: 409 });
|
|
6025
|
-
}
|
|
6026
|
-
}
|
|
6027
6256
|
function claimTeamComputerLifecycle(computer) {
|
|
6028
6257
|
if (computerProviderConfigTransitions.has("box"))
|
|
6029
6258
|
throw Object.assign(new Error(providerTransitionMessage("box")), { status: 409 });
|
|
@@ -6060,9 +6289,9 @@ async function teamComputersPayload() {
|
|
|
6060
6289
|
}
|
|
6061
6290
|
/** The same named Boat identity and whole-turn lease in chats and rooms.
|
|
6062
6291
|
* Assignment authorizes waking, never replacing a missing paid machine. */
|
|
6063
|
-
async function attachTeamBoat(computer, botId, owner, canMount
|
|
6292
|
+
async function attachTeamBoat(computer, botId, owner, canMount) {
|
|
6064
6293
|
if (!canMount)
|
|
6065
|
-
throw new Error("This model cannot use the team's
|
|
6294
|
+
throw new Error("This model cannot use the team's cloud computer; choose a model provider with computer tools or an explicit bot destination");
|
|
6066
6295
|
if (!boat.boatConfigured(cfg))
|
|
6067
6296
|
throw new Error("The team's Boat account is not configured; reconnect it in Settings");
|
|
6068
6297
|
const ownerId = teamComputerOwner(computer.id);
|
|
@@ -6071,46 +6300,51 @@ async function attachTeamBoat(computer, botId, owner, canMount, remoteAgent) {
|
|
|
6071
6300
|
if (computerControl.snapshot(ownerId).held)
|
|
6072
6301
|
throw new Error("Release human control of the team computer before starting another turn");
|
|
6073
6302
|
await bindTurnComputer(owner, `computer:box-bot:${ownerId}`, true);
|
|
6074
|
-
teamComputerTurns.set(owner.threadId, { owner, computerId: computer.id, botId
|
|
6303
|
+
teamComputerTurns.set(owner.threadId, { owner, computerId: computer.id, botId });
|
|
6075
6304
|
let machine = await boat.findBoat(cfg, ownerId);
|
|
6076
6305
|
if (!machine)
|
|
6077
|
-
throw new Error("The team's
|
|
6306
|
+
throw new Error("The team's cloud computer is missing; explicitly create or retry it from the Team map");
|
|
6078
6307
|
await bindTurnComputer(owner, `computer:box:${machine.id}`, true);
|
|
6079
|
-
const action = boat.boatTurnLifecycleAction(
|
|
6308
|
+
const action = boat.boatTurnLifecycleAction(typeof machine.state === "string" ? machine.state : null);
|
|
6080
6309
|
if (action === "wake")
|
|
6081
6310
|
machine = await boat.readyBoat(cfg, ownerId);
|
|
6082
|
-
if (!machine || boat.boatTurnLifecycleAction(
|
|
6311
|
+
if (!machine || boat.boatTurnLifecycleAction(typeof machine.state === "string" ? machine.state : null) !== "attach") {
|
|
6083
6312
|
throw new Error("The team computer is not ready; check it in the Team map");
|
|
6084
6313
|
}
|
|
6085
6314
|
if (turnResourceOwners.get(owner.threadId)?.generation !== owner.generation ||
|
|
6086
6315
|
!turnResources.owns(`computer:box:${machine.id}`, owner))
|
|
6087
6316
|
throw new Error("This computer turn ended while its machine was starting");
|
|
6088
6317
|
return {
|
|
6089
|
-
mount: cloudComputerMount(botId, owner, machine.id
|
|
6318
|
+
mount: cloudComputerMount(botId, owner, machine.id),
|
|
6090
6319
|
capture: () => boat.screenshotBoat(cfg, ownerId, machine.id),
|
|
6091
6320
|
};
|
|
6092
6321
|
}
|
|
6093
|
-
/** The
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
*
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6322
|
+
/** The engine fact the cloud-computer rule reads (shared/cloud-computer.ts),
|
|
6323
|
+
* and the model's name for a refusal to say. */
|
|
6324
|
+
function cloudEngine(instance, model) {
|
|
6325
|
+
return {
|
|
6326
|
+
computerMcp: instance?.adapter.capabilities.computerMcp,
|
|
6327
|
+
name: model?.trim() || instance?.displayName || instance?.driverKind || "This model",
|
|
6328
|
+
};
|
|
6329
|
+
}
|
|
6330
|
+
/** How a turn's engine reaches a cloud computer. Every engine keeps its own
|
|
6331
|
+
* model and sign-in and gets the Boat as one more stdio computer server
|
|
6332
|
+
* (harness-mcp-proxy computer), in the same slot a Local VM or VPS uses: one
|
|
6333
|
+
* mechanism on a desktop, a headless server and an OMB Cloud. The agent
|
|
6334
|
+
* process holds only a turn-scoped capability naming this Boat, never a Boat
|
|
6335
|
+
* credential. */
|
|
6336
|
+
function cloudComputerMount(botId, owner, boxId) {
|
|
6337
|
+
return { localComputer: cloudComputerTools(botId, owner, boxId) };
|
|
6338
|
+
}
|
|
6339
|
+
/** The cloud computer's stdio tools for this turn. `boxId` null: the Boat is
|
|
6340
|
+
* the one the first computer call's claim creates or wakes (mountBotBoat). */
|
|
6341
|
+
function cloudComputerTools(botId, owner, boxId) {
|
|
6106
6342
|
const control = controlIntegration(botId, owner.threadId, owner.generation, { boxId });
|
|
6107
6343
|
return {
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
platform: "linux",
|
|
6113
|
-
},
|
|
6344
|
+
command: process.execPath,
|
|
6345
|
+
args: [SPAWNED_PROXIES.harnessMcp, "computer"],
|
|
6346
|
+
env: { ...AGENTS_NODE_FLAG, OMB_HARNESS_URL: `http://127.0.0.1:${PORT}`, OMB_MCP_TOKEN: control.token },
|
|
6347
|
+
platform: "linux",
|
|
6114
6348
|
};
|
|
6115
6349
|
}
|
|
6116
6350
|
/** "Works on: This computer", strict. One mount for a bot thread and a group
|
|
@@ -6137,8 +6371,12 @@ async function mountHostComputer(owner, botId, providerSupportsLocal) {
|
|
|
6137
6371
|
* is claimed on the first computer call, through the computer-control gate
|
|
6138
6372
|
* (the Local VM's seam, #1361), never at mount: turns that never touch the
|
|
6139
6373
|
* computer tools run side by side. `start` may provision or start the
|
|
6140
|
-
* container; without it the VPS is only inspected.
|
|
6141
|
-
*
|
|
6374
|
+
* container; without it the VPS is only inspected. Unlike the bot's cloud
|
|
6375
|
+
* computer (mountBotBoat), the start stays here, at dispatch: the VPS tools
|
|
6376
|
+
* are a bridge that execs into the running container as soon as the engine
|
|
6377
|
+
* launches it (runMcpBridge), and a self-hosted container uses no plan
|
|
6378
|
+
* hours. The caller has already recorded the thread with vpsThreadStarted
|
|
6379
|
+
* and ends it on a problem. */
|
|
6142
6380
|
async function mountBotVps(bot, owner, opts) {
|
|
6143
6381
|
const vpsResource = `computer:vps:${vpsSshAlias(cfg)}:${bot.id}`;
|
|
6144
6382
|
const remote = opts.start
|
|
@@ -6169,54 +6407,113 @@ async function mountBotVps(bot, owner, opts) {
|
|
|
6169
6407
|
},
|
|
6170
6408
|
};
|
|
6171
6409
|
}
|
|
6172
|
-
/**
|
|
6173
|
-
*
|
|
6174
|
-
*
|
|
6175
|
-
*
|
|
6176
|
-
*
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6410
|
+
/** How long one computer call waits on a cloud computer that is being
|
|
6411
|
+
* created or woken before it is told to look again. Under the 60 seconds an
|
|
6412
|
+
* engine may give one tool call (Codex's default); a call that runs out finds
|
|
6413
|
+
* the start still going and waits again, inside the start's own budget.
|
|
6414
|
+
* OMB_CLOUD_COMPUTER_START_WAIT_MS shortens it for tests. */
|
|
6415
|
+
const CLOUD_COMPUTER_START_WAIT_MS = Math.max(1_000, Number(process.env.OMB_CLOUD_COMPUTER_START_WAIT_MS) || 45_000);
|
|
6416
|
+
/** How long the start itself may take once this turn has the computer to
|
|
6417
|
+
* itself: above a slow first start (about a minute; Boat's own ready wait is
|
|
6418
|
+
* 90 seconds). Past it the start has failed, and the turn says so and ends,
|
|
6419
|
+
* instead of every call answering "still starting" until the watchdog.
|
|
6420
|
+
* OMB_CLOUD_COMPUTER_START_BUDGET_MS shortens it for tests. */
|
|
6421
|
+
const CLOUD_COMPUTER_START_BUDGET_MS = Math.max(1_000, Number(process.env.OMB_CLOUD_COMPUTER_START_BUDGET_MS) || 150_000);
|
|
6422
|
+
const CLOUD_COMPUTER_STARTING = "The cloud computer is still starting. This call was not performed. Take a fresh screenshot in a moment.";
|
|
6423
|
+
const CLOUD_COMPUTER_DID_NOT_START = "the cloud computer didn't start in time";
|
|
6424
|
+
/** The bot's own Boat as a computer its engine uses, mounted like the VPS
|
|
6425
|
+
* (mountBotVps): the tools mount now, and the Boat is created or woken by
|
|
6426
|
+
* the turn's first computer call, through the computer-control gate (the
|
|
6427
|
+
* Local VM's seam, #1361), never at mount. A turn that never touches the
|
|
6428
|
+
* screen ("hi") makes no Boat call at all, so it costs no plan hours. The
|
|
6429
|
+
* claim is attachBotBoat itself, with its explicit-Cloud consent: the person
|
|
6430
|
+
* chose this place, and the bot is now using it. */
|
|
6431
|
+
function mountBotBoat(bot, owner, opts) {
|
|
6432
|
+
const slot = {
|
|
6433
|
+
owner,
|
|
6434
|
+
lazy: true,
|
|
6435
|
+
startsComputer: true,
|
|
6436
|
+
label: "the cloud computer",
|
|
6437
|
+
graceMs: CLOUD_COMPUTER_START_WAIT_MS,
|
|
6438
|
+
// Until this turn has the computer to itself another conversation does,
|
|
6439
|
+
// and a call is told so (the gate's contention text); from then on the
|
|
6440
|
+
// claim is starting it.
|
|
6441
|
+
onRejected: opts.onRejected,
|
|
6442
|
+
claim: async () => {
|
|
6443
|
+
let attached;
|
|
6444
|
+
try {
|
|
6445
|
+
attached = await attachBotBoat(bot, owner, {
|
|
6446
|
+
budgetMs: CLOUD_COMPUTER_START_BUDGET_MS,
|
|
6447
|
+
onSeated: () => { slot.pendingReason = CLOUD_COMPUTER_STARTING; },
|
|
6448
|
+
});
|
|
6449
|
+
if (!attached)
|
|
6450
|
+
throw new Error("the cloud computer could not be created or reached");
|
|
6451
|
+
}
|
|
6452
|
+
catch (error) {
|
|
6453
|
+
throw opts.source ? await asPlaceFailure(error, "cloud", opts.source, bot, "box") : error;
|
|
6454
|
+
}
|
|
6455
|
+
slot.boxId = attached.boxId;
|
|
6456
|
+
opts.onClaimed(attached.capture);
|
|
6457
|
+
},
|
|
6458
|
+
};
|
|
6459
|
+
autoVmClaims.set(owner.threadId, slot);
|
|
6460
|
+
return cloudComputerTools(bot.id, owner, null);
|
|
6461
|
+
}
|
|
6462
|
+
/** The Boat a lazily mounted cloud computer's claim landed on, for this
|
|
6463
|
+
* exact turn; none while the claim is still running or after it failed. */
|
|
6464
|
+
function claimedBoatId(capability) {
|
|
6465
|
+
const slot = autoVmClaims.get(capability.threadId);
|
|
6466
|
+
return slot?.owner.generation === capability.generation && slot.claimed ? slot.boxId : undefined;
|
|
6467
|
+
}
|
|
6468
|
+
/** The bot's own Boat, for a turn whose place is Cloud. That choice is the
|
|
6469
|
+
* consent boundary: it may create a missing machine and wake an archived one
|
|
6470
|
+
* (~8s, and it un-pauses billing). Auto never reaches here. Callers have
|
|
6471
|
+
* already checked that the engine can work on it (cloudPlaceRefusal).
|
|
6472
|
+
* Every engine reaches it through mountBotBoat, on its first computer call:
|
|
6473
|
+
* told once this turn has the Boat to itself, it has `budgetMs` to find,
|
|
6474
|
+
* create or wake it. */
|
|
6475
|
+
async function attachBotBoat(bot, owner, start) {
|
|
6476
|
+
// Claim before any network await so setup itself cannot race another turn.
|
|
6477
|
+
await bindTurnComputer(owner, `computer:box-bot:${bot.id}`, true);
|
|
6478
|
+
start.onSeated();
|
|
6479
|
+
const machine = await withinBudget(startBotBoat(bot), start.budgetMs, CLOUD_COMPUTER_DID_NOT_START);
|
|
6480
|
+
if (!machine)
|
|
6481
|
+
return null;
|
|
6482
|
+
await bindTurnComputer(owner, `computer:box:${machine.id}`);
|
|
6483
|
+
return {
|
|
6484
|
+
boxId: machine.id,
|
|
6485
|
+
capture: () => boat.screenshotBoat(cfg, bot.id, machine.id),
|
|
6486
|
+
};
|
|
6487
|
+
}
|
|
6488
|
+
/** `work`, or a rejection with `message` once `ms` pass first. The work is
|
|
6489
|
+
* not cancelled: it ends on its own requests' deadlines, and the race has
|
|
6490
|
+
* already handled a rejection it brings later. */
|
|
6491
|
+
function withinBudget(work, ms, message) {
|
|
6492
|
+
let timer;
|
|
6493
|
+
const expired = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), ms); });
|
|
6494
|
+
return Promise.race([work, expired]).finally(() => clearTimeout(timer));
|
|
6495
|
+
}
|
|
6496
|
+
/** Find the bot's Boat, creating or waking it: the Boat once it can be
|
|
6497
|
+
* attached, or null. */
|
|
6498
|
+
async function startBotBoat(bot) {
|
|
6499
|
+
let b = await boat.findBoat(cfg, bot.id);
|
|
6500
|
+
const lifecycleOf = () => boat.boatTurnLifecycleAction(typeof b?.state === "string" ? b.state : null);
|
|
6501
|
+
let lifecycle = lifecycleOf();
|
|
6198
6502
|
if (lifecycle === "provision") {
|
|
6199
|
-
broadcast({ kind: "computer", botId: bot.id, state: "provisioning" });
|
|
6503
|
+
broadcast({ kind: "computer", botId: bot.id, state: "provisioning", place: "cloud" });
|
|
6200
6504
|
await boat.provisionBoat(cfg, bot.id, bot.name);
|
|
6201
6505
|
b = await boat.findBoat(cfg, bot.id);
|
|
6202
|
-
lifecycle = lifecycleOf(
|
|
6506
|
+
lifecycle = lifecycleOf();
|
|
6203
6507
|
}
|
|
6204
6508
|
// an archived boat answers every action with an error until it resumes —
|
|
6205
6509
|
// wake it here, once, instead of letting the agent discover it one failed
|
|
6206
6510
|
// tool call at a time.
|
|
6207
6511
|
if (lifecycle === "wake") {
|
|
6208
|
-
broadcast({ kind: "computer", botId: bot.id, state: "waking" });
|
|
6512
|
+
broadcast({ kind: "computer", botId: bot.id, state: "waking", place: "cloud" });
|
|
6209
6513
|
b = (await boat.readyBoat(cfg, bot.id)) ?? b;
|
|
6210
|
-
lifecycle = lifecycleOf(
|
|
6514
|
+
lifecycle = lifecycleOf();
|
|
6211
6515
|
}
|
|
6212
|
-
|
|
6213
|
-
return null;
|
|
6214
|
-
const machine = b;
|
|
6215
|
-
await bindTurnComputer(owner, `computer:box:${machine.id}`, opts.remoteAgent);
|
|
6216
|
-
return {
|
|
6217
|
-
capture: () => boat.screenshotBoat(cfg, bot.id, machine.id),
|
|
6218
|
-
mount: cloudComputerMount(bot.id, owner, machine.id, opts.remoteAgent),
|
|
6219
|
-
};
|
|
6516
|
+
return b && lifecycle === "attach" ? b : null;
|
|
6220
6517
|
}
|
|
6221
6518
|
function managedBoatOwners() {
|
|
6222
6519
|
return [...store.bots.map((bot) => ({
|
|
@@ -6288,8 +6585,6 @@ function turnProvider(bot, runOn, threadId) {
|
|
|
6288
6585
|
const wants = turnSurfacePlan(bot, runOn, threadId).computer;
|
|
6289
6586
|
if (wants !== undefined && wants !== "cloud")
|
|
6290
6587
|
return null;
|
|
6291
|
-
if (registry.get(bot.modelSelection.instanceId)?.adapter.capabilities.remoteAgent === true)
|
|
6292
|
-
return "box";
|
|
6293
6588
|
return bot.cloudBackend === "vps" ? "vps" : wants === "cloud" ? "box" : null;
|
|
6294
6589
|
}
|
|
6295
6590
|
/** Preview requests carry the selected conversation, not whichever thread
|
|
@@ -6308,8 +6603,6 @@ async function computerPreviewSurface(bot, threadId) {
|
|
|
6308
6603
|
if (plan.computer !== undefined)
|
|
6309
6604
|
return plan.computer === "off" && plan.browser ? "browser" : plan.computer;
|
|
6310
6605
|
const instance = registry.get(bot.modelSelection.instanceId);
|
|
6311
|
-
if (instance?.adapter.capabilities.remoteAgent === true)
|
|
6312
|
-
return "cloud";
|
|
6313
6606
|
if (bot.cloudBackend === "vps") {
|
|
6314
6607
|
const remote = await vps.vpsComputerStatus(cfg, bot.id);
|
|
6315
6608
|
if (remote.ready)
|
|
@@ -6337,7 +6630,8 @@ async function selectableComputers(bot) {
|
|
|
6337
6630
|
const instance = registry.get(bot.modelSelection.instanceId);
|
|
6338
6631
|
const caps = instance?.adapter.capabilities;
|
|
6339
6632
|
const off = bot.computer === "off";
|
|
6340
|
-
|
|
6633
|
+
// A desktop the bot's tools cannot reach is not offered, and not probed.
|
|
6634
|
+
const toolsLeaveOut = computerToolsRefusal(bot.toolScope, "works-on", bot.name)?.message;
|
|
6341
6635
|
const surfaces = ["cloud", "vm", "local", "browser"].filter(surface => !CLOUD_HOME || cloudHomeOffersPlace(surface));
|
|
6342
6636
|
return Promise.all(surfaces.map(async (surface) => {
|
|
6343
6637
|
let ready = false;
|
|
@@ -6351,24 +6645,31 @@ async function selectableComputers(bot) {
|
|
|
6351
6645
|
try {
|
|
6352
6646
|
if (off)
|
|
6353
6647
|
reason = "Computer access is Off in this bot's settings.";
|
|
6648
|
+
else if (toolsLeaveOut && surface !== "browser")
|
|
6649
|
+
reason = toolsLeaveOut;
|
|
6354
6650
|
else if (surface === "cloud") {
|
|
6355
6651
|
if (bot.cloudBackend === "vps") {
|
|
6356
|
-
const status = canWorkOnCloud(cloudEngine(instance)
|
|
6652
|
+
const status = canWorkOnCloud(cloudEngine(instance)) ? await vps.vpsComputerStatus(cfg, bot.id) : null;
|
|
6357
6653
|
ready = status?.ready === true;
|
|
6358
6654
|
canStart = Boolean(status?.daemonUp && status.managed && status.container === "stopped" &&
|
|
6359
6655
|
status.image && status.imageMatches && status.network === "private" && status.mounts === "none" && status.security === "hardened");
|
|
6360
6656
|
canCreate = Boolean(status?.configured && status.daemonUp && status.container === "missing");
|
|
6361
6657
|
reason = status?.problem ?? reason;
|
|
6362
6658
|
}
|
|
6363
|
-
else if (boat.boatConfigured(cfg) && canWorkOnCloud(cloudEngine(instance)
|
|
6659
|
+
else if (boat.boatConfigured(cfg) && canWorkOnCloud(cloudEngine(instance))) {
|
|
6364
6660
|
const status = await boat.boatStatus(cfg, bot.id);
|
|
6365
|
-
const lifecycle = boat.boatTurnLifecycleAction(
|
|
6661
|
+
const lifecycle = boat.boatTurnLifecycleAction(status.box?.state ?? null);
|
|
6366
6662
|
ready = lifecycle === "attach";
|
|
6367
6663
|
canStart = lifecycle === "wake";
|
|
6368
6664
|
canCreate = lifecycle === "provision";
|
|
6369
6665
|
}
|
|
6666
|
+
else {
|
|
6667
|
+
// The words the person reads for the same place (shared/place-view.ts).
|
|
6668
|
+
reason = (cloudPlaceRefusal(cloudEngine(instance, bot.modelSelection.model), "works-on", bot.name)
|
|
6669
|
+
?? boatNotConfigured(bot, "works-on")).message;
|
|
6670
|
+
}
|
|
6370
6671
|
}
|
|
6371
|
-
else if (surface === "vm" &&
|
|
6672
|
+
else if (surface === "vm" && caps?.computerMcp) {
|
|
6372
6673
|
const target = localVmTargetForStatus(bot.id);
|
|
6373
6674
|
const status = await containerComputerStatus(undefined, undefined, target);
|
|
6374
6675
|
ready = status.ready;
|
|
@@ -7881,12 +8182,15 @@ function finalizeDelegationWatch(threadId, ok, reply = "", failureName = "Delega
|
|
|
7881
8182
|
// with the reply only visible in the thread (the "delegated and went
|
|
7882
8183
|
// silent" gap). Failures wake it too — the user must hear the task did
|
|
7883
8184
|
// not finish. Idle-checked and burst-capped so a busy source or a
|
|
7884
|
-
// re-delegating loop cannot spin up runs.
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
8185
|
+
// re-delegating loop cannot spin up runs. An external runtime polls
|
|
8186
|
+
// for the receipt instead; waking would answer it a second time.
|
|
8187
|
+
if (watched.completionOwner !== "external") {
|
|
8188
|
+
if (ok) {
|
|
8189
|
+
wakeDelegationSource(source, watched.sourceThreadId, targetName, undefined, watched.routineRunId);
|
|
8190
|
+
}
|
|
8191
|
+
else {
|
|
8192
|
+
wakeDelegationSource(source, watched.sourceThreadId, targetName, failureName || "the delegated turn did not finish", watched.routineRunId);
|
|
8193
|
+
}
|
|
7890
8194
|
}
|
|
7891
8195
|
}
|
|
7892
8196
|
}
|
|
@@ -7941,7 +8245,7 @@ bus.subscribe((event) => {
|
|
|
7941
8245
|
/** How a drained delegation becomes a real turn on the target. Shared by
|
|
7942
8246
|
* the settle-time drain and the boot-time drain of what a previous process
|
|
7943
8247
|
* left queued. */
|
|
7944
|
-
const runDelegatedTurn = (toBotId, rawText, commsDepth, sourceThreadId, channel, taskId, sourceBotId, openedThreadId, oneWay) => {
|
|
8248
|
+
const runDelegatedTurn = (toBotId, rawText, commsDepth, sourceThreadId, channel, taskId, sourceBotId, openedThreadId, oneWay, completionOwner) => {
|
|
7945
8249
|
// startTurn REJECTS on an ordinary condition — busy target, deleted bot,
|
|
7946
8250
|
// unavailable provider. Unhandled, that rejection is fatal to the
|
|
7947
8251
|
// harness (Node's default), which in the packaged app kills the server
|
|
@@ -7975,6 +8279,7 @@ const runDelegatedTurn = (toBotId, rawText, commsDepth, sourceThreadId, channel,
|
|
|
7975
8279
|
routineRunId: activeRoutineRunForThread(sourceThreadId)?.id,
|
|
7976
8280
|
startedAtMs: Date.now(),
|
|
7977
8281
|
...(oneWay ? { oneWay: true } : {}),
|
|
8282
|
+
...(completionOwner ? { completionOwner } : {}),
|
|
7978
8283
|
});
|
|
7979
8284
|
}
|
|
7980
8285
|
let failureReported = false;
|
|
@@ -8469,10 +8774,9 @@ const screenPollers = new Map();
|
|
|
8469
8774
|
const SCREEN_POLL_MS = 6000;
|
|
8470
8775
|
const SCREEN_MIN_GAP_MS = 3000;
|
|
8471
8776
|
const SCREEN_SETTLE_TIMEOUT_MS = 10_000;
|
|
8472
|
-
/** `screenIsTheWork` starts the
|
|
8473
|
-
*
|
|
8474
|
-
*
|
|
8475
|
-
* shell-only turns are kept honest by the settle-time hash gate instead. */
|
|
8777
|
+
/** `screenIsTheWork` starts the poller already counting as screen usage: a
|
|
8778
|
+
* poller restarted mid-turn (a lazy claim swapping in its capture) carries
|
|
8779
|
+
* over whether the turn had touched its screen. */
|
|
8476
8780
|
function startScreenPoller(botId, threadId, captures, { screenIsTheWork = false } = {}) {
|
|
8477
8781
|
if (!captures.computer && !captures.browser)
|
|
8478
8782
|
return;
|
|
@@ -8564,10 +8868,9 @@ function shownScreenHash(threadId) {
|
|
|
8564
8868
|
* end state, not the previous action's. A turn that never touched the
|
|
8565
8869
|
* screen settles nothing — and skips the capture, which is one less
|
|
8566
8870
|
* command on the boat's single endpoint. A frame the reader can already see
|
|
8567
|
-
* settles nothing either
|
|
8568
|
-
*
|
|
8569
|
-
*
|
|
8570
|
-
* per-turn state survives the turn. */
|
|
8871
|
+
* settles nothing either, so a turn never ends on the same idle desktop it
|
|
8872
|
+
* already showed. Either way the poller is torn down here, so no per-turn
|
|
8873
|
+
* state survives the turn. */
|
|
8571
8874
|
async function finalScreenFrame(_botId, threadId) {
|
|
8572
8875
|
const entry = screenPollers.get(threadId);
|
|
8573
8876
|
const owner = turnResourceOwners.get(threadId);
|
|
@@ -8641,6 +8944,8 @@ async function startTurn(botId, text, opts) {
|
|
|
8641
8944
|
},
|
|
8642
8945
|
};
|
|
8643
8946
|
}
|
|
8947
|
+
// A thread whose own model can't run runs on its bot's from now on.
|
|
8948
|
+
healThreadModel(botId, threadId);
|
|
8644
8949
|
const bot = store.projectBotForTask(botId, threadId);
|
|
8645
8950
|
if (!bot)
|
|
8646
8951
|
throw Object.assign(new Error("no such task"), { status: 404 });
|
|
@@ -8695,6 +9000,10 @@ async function startTurn(botId, text, opts) {
|
|
|
8695
9000
|
const plan = turnSurfacePlan(bot, opts?.runOn, threadId);
|
|
8696
9001
|
const instance = registry.get(bot.modelSelection.instanceId);
|
|
8697
9002
|
if (!instance) {
|
|
9003
|
+
// Still on the removed Computer engine: its move failed or found no
|
|
9004
|
+
// engine. Try it again now, so the next message runs on the new engine.
|
|
9005
|
+
if (removedComputerInstanceIds(cfg.instances).has(bot.modelSelection.instanceId))
|
|
9006
|
+
void retryComputerEngineMove();
|
|
8698
9007
|
throw Object.assign(new Error(`provider instance "${bot.modelSelection.instanceId}" is unavailable — pick another model in settings`), { status: 409 });
|
|
8699
9008
|
}
|
|
8700
9009
|
const policyRefusal = policyModelRefusal(instance);
|
|
@@ -8878,6 +9187,11 @@ async function startTurn(botId, text, opts) {
|
|
|
8878
9187
|
turnContext.delete(threadId);
|
|
8879
9188
|
void (async () => {
|
|
8880
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
|
+
}
|
|
8881
9195
|
// Readiness can wait on the network. Admit the task first so its busy
|
|
8882
9196
|
// state, Stop action and watchdog own that wait just like VM setup.
|
|
8883
9197
|
if (opts?.runOn === "cloud") {
|
|
@@ -8988,7 +9302,11 @@ async function startTurn(botId, text, opts) {
|
|
|
8988
9302
|
const userTurnText = promptWithReply(skillAuthoring ? expandLearnTurnText(setupText) : setupText, opts?.replyTo, cfg.profile?.name?.trim() || "User");
|
|
8989
9303
|
// Decided again at dispatch when setup outlasted a soul edit (config).
|
|
8990
9304
|
const decideContext = (config) => {
|
|
8991
|
-
|
|
9305
|
+
// A Codex thread keeps the last effort it was sent and no turn can clear
|
|
9306
|
+
// one: back on the default, a thread that holds a level is rebuilt from
|
|
9307
|
+
// the transcript rather than resumed on that level.
|
|
9308
|
+
const effortHeld = !effort && typeof handed?.effort === "string";
|
|
9309
|
+
const handedStale = Boolean(handed && (!unseen || effortHeld || (externalUpdate && handed.config !== config)));
|
|
8992
9310
|
const { block: unseenBlock, placed } = unseen && !handedStale ? renderUnseen(unseen) : { block: "", placed: [] };
|
|
8993
9311
|
const { turnText: contextTurnText, resume } = buildTurnContext({
|
|
8994
9312
|
text: userTurnText,
|
|
@@ -9021,6 +9339,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9021
9339
|
resumeCursor, sessionReset: !resume, recoveryText, recoveryIsReplay,
|
|
9022
9340
|
handoff: strictResume ? {
|
|
9023
9341
|
botId: bot.id, instanceId, config, resumeCursor: typeof resumeCursor === "string" ? resumeCursor : undefined,
|
|
9342
|
+
...(instance.driverKind === "codex" ? { effort: effort ?? null } : {}),
|
|
9024
9343
|
started: sessionStart(contextOrder, contextTurnText !== userTurnText ? windowIds : [], carried),
|
|
9025
9344
|
recovery: sessionStart(contextOrder, recoveryText !== undefined ? windowIds : [], carried),
|
|
9026
9345
|
resumed: sessionStart(contextOrder, [], [...placed, ...carried]),
|
|
@@ -9093,8 +9412,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9093
9412
|
// because moving a live session would break resume.
|
|
9094
9413
|
// A cloud run keeps the bot's own engine on this host, with the cloud
|
|
9095
9414
|
// computer as one more tool, so it works in the same folder as any
|
|
9096
|
-
// other turn
|
|
9097
|
-
// workspace files).
|
|
9415
|
+
// other turn.
|
|
9098
9416
|
// On a Cloud home a conversation a guest opened is pinned to its own
|
|
9099
9417
|
// folder when it first runs, not the bot's project folder the owner's
|
|
9100
9418
|
// conversations share (what it leaves there reaches none of them). One
|
|
@@ -9135,9 +9453,6 @@ async function startTurn(botId, text, opts) {
|
|
|
9135
9453
|
// landed. A team computer or a cloud routine is not this conversation's
|
|
9136
9454
|
// choice, so those ignore the pin.
|
|
9137
9455
|
const dispatchTask = store.taskByThread(bot.id, threadId);
|
|
9138
|
-
if (plan.computer !== undefined && plan.computer !== "cloud" && instance.adapter.capabilities.remoteAgent === true) {
|
|
9139
|
-
throw new Error("the Computer engine works on the cloud computer — set Works on to Cloud, or choose another engine");
|
|
9140
|
-
}
|
|
9141
9456
|
const wants = plan.computer;
|
|
9142
9457
|
// A place the organisation disallows, or a Cloud home never offers, is
|
|
9143
9458
|
// refused before anything is prepared; Auto below simply skips them.
|
|
@@ -9154,12 +9469,14 @@ async function startTurn(botId, text, opts) {
|
|
|
9154
9469
|
: opts?.runOn === "cloud" ? "routine"
|
|
9155
9470
|
: plan.pinned ? (dispatchTask?.surfaceSource === "auto" ? "auto-pin" : "pin")
|
|
9156
9471
|
: "works-on";
|
|
9157
|
-
// An engine that cannot use the cloud computer
|
|
9158
|
-
//
|
|
9472
|
+
// An engine that cannot use the cloud computer, or a Tool selection
|
|
9473
|
+
// that leaves its tools out, is refused here, before anything is
|
|
9474
|
+
// created or woken — never handed to another engine.
|
|
9159
9475
|
if (wants === "cloud" && placeSource) {
|
|
9160
|
-
const unsupported =
|
|
9476
|
+
const unsupported = cloudPlaceRefusal(cloudEngine(instance, bot.modelSelection.model), placeSource, bot.name)
|
|
9477
|
+
?? computerToolsRefusal(toolScope, placeSource, bot.name);
|
|
9161
9478
|
if (unsupported)
|
|
9162
|
-
throw
|
|
9479
|
+
throw unsupported;
|
|
9163
9480
|
}
|
|
9164
9481
|
let previewCapture = null;
|
|
9165
9482
|
let browserCapture = null;
|
|
@@ -9285,7 +9602,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9285
9602
|
// the newer turn. The gate's refusals stay fail-closed for any screen
|
|
9286
9603
|
// call racing this teardown; the turn-failed buzz and incident follow
|
|
9287
9604
|
// the dispatch-failure rules (person-started turns only).
|
|
9288
|
-
const surfaceLazyClaimRejection = (label) => (failure) => {
|
|
9605
|
+
const surfaceLazyClaimRejection = (label) => (failure, error) => {
|
|
9289
9606
|
if (activeInternalGenerationByThread.get(threadId) !== resourceOwner.generation || !threadBusy(bot.id, threadId))
|
|
9290
9607
|
return;
|
|
9291
9608
|
// The wait ceiling parked this turn (#1651): register the resume and
|
|
@@ -9304,11 +9621,16 @@ async function startTurn(botId, text, opts) {
|
|
|
9304
9621
|
void interruptDirectThread(bot.id, threadId, { preserveComputerResume: true }).catch(() => { });
|
|
9305
9622
|
return;
|
|
9306
9623
|
}
|
|
9307
|
-
|
|
9624
|
+
// A place that failed (mountBotBoat) is already read as its one line
|
|
9625
|
+
// and one way on, and never sticks as an Auto-recorded pin.
|
|
9626
|
+
const placeFailure = error instanceof PlaceUnavailableError ? error : undefined;
|
|
9627
|
+
if (placeFailure)
|
|
9628
|
+
clearFailedAutoPin(bot.id, threadId, placeFailure.place);
|
|
9629
|
+
const message = placeFailure?.message ?? `computer unavailable — ${label} could not be claimed for this turn (${failure})`;
|
|
9308
9630
|
store.appendMessage(threadId, {
|
|
9309
9631
|
role: "bot",
|
|
9310
9632
|
kind: "activity",
|
|
9311
|
-
tool: failedTurnTool(message),
|
|
9633
|
+
tool: failedTurnTool(message, placeFailure ? { place: placeFailure.row } : {}),
|
|
9312
9634
|
});
|
|
9313
9635
|
if (opts?.automationSource === undefined && !opts?.commsDepth && !opts?.cardContinuation) {
|
|
9314
9636
|
notify(buildNotification("turn-failed", bot, threadId, redactSecretsInText(message), { avatarUrl: bot.avatarUrl }));
|
|
@@ -9317,8 +9639,26 @@ async function startTurn(botId, text, opts) {
|
|
|
9317
9639
|
// completion fold must not report this failure a second time.
|
|
9318
9640
|
resourceOwner.lazyClaimFailureReported = true;
|
|
9319
9641
|
}
|
|
9642
|
+
// A routine, a delegation or a resume reports this exactly as it
|
|
9643
|
+
// reports a place that failed at dispatch: with this cause, before
|
|
9644
|
+
// the interrupt settles the turn as merely "interrupted".
|
|
9645
|
+
opts?.onDispatchError?.(message);
|
|
9320
9646
|
void interruptDirectThread(bot.id, threadId).catch(() => { });
|
|
9321
9647
|
};
|
|
9648
|
+
/** A lazily claimed remote desktop (the VPS, the bot's Boat) streams
|
|
9649
|
+
* from the moment its claim lands. The dispatch-site poller start saw
|
|
9650
|
+
* no capture (the claim runs after dispatch), so restart the poller
|
|
9651
|
+
* with this one, keeping any browser capture and whether this turn
|
|
9652
|
+
* already touched its screen. Same still-running guard as dispatch: a
|
|
9653
|
+
* poller started after its own turn.completed would never stop. */
|
|
9654
|
+
const adoptClaimedCapture = (capture) => {
|
|
9655
|
+
previewCapture = capture;
|
|
9656
|
+
if (!threadBusy(bot.id, threadId))
|
|
9657
|
+
return;
|
|
9658
|
+
const touched = screenPollers.get(threadId)?.touched ?? false;
|
|
9659
|
+
stopScreenPoller(bot.id, threadId);
|
|
9660
|
+
startScreenPoller(bot.id, threadId, { computer: capture, ...(browserCapture ? { browser: browserCapture } : {}) }, { screenIsTheWork: touched });
|
|
9661
|
+
};
|
|
9322
9662
|
// Explicit destinations are strict. In particular, Local VM must never
|
|
9323
9663
|
// fall through to host CUA and accidentally click on the user's Mac.
|
|
9324
9664
|
// The Local VM attach, shared by explicit "Local VM" and by Auto. Explicit
|
|
@@ -9327,7 +9667,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9327
9667
|
// be recreated on demand after idling away — and never creates a first
|
|
9328
9668
|
// VM on its own; every failure there is a quiet "not this place".
|
|
9329
9669
|
const attachLocalVm = async (strict) => {
|
|
9330
|
-
if (!mountsComputerMcp
|
|
9670
|
+
if (!mountsComputerMcp) {
|
|
9331
9671
|
if (!strict)
|
|
9332
9672
|
return false;
|
|
9333
9673
|
throw new Error("this model cannot use the Local VM — choose Claude or an ACP model provider, or select another computer destination");
|
|
@@ -9383,7 +9723,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9383
9723
|
// guard as dispatch: a poller started after its own
|
|
9384
9724
|
// turn.completed would never be torn down.
|
|
9385
9725
|
if (previewCapture && threadBusy(bot.id, threadId)) {
|
|
9386
|
-
const touched = screenPollers.get(threadId)?.touched ??
|
|
9726
|
+
const touched = screenPollers.get(threadId)?.touched ?? false;
|
|
9387
9727
|
stopScreenPoller(bot.id, threadId);
|
|
9388
9728
|
startScreenPoller(bot.id, threadId, { computer: previewCapture, ...(browserCapture ? { browser: browserCapture } : {}) }, { screenIsTheWork: touched });
|
|
9389
9729
|
}
|
|
@@ -9430,7 +9770,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9430
9770
|
// the person explicitly opted this bot into remote lifecycle actions.
|
|
9431
9771
|
// On Auto an engine that cannot use a computer simply skips it.
|
|
9432
9772
|
if ((wants === "cloud" || (wants === undefined && managedPolicy.computerAllowed("vps"))) && cloudBackend === "vps" &&
|
|
9433
|
-
canWorkOnCloud(cloudEngine(instance)
|
|
9773
|
+
canWorkOnCloud(cloudEngine(instance))) {
|
|
9434
9774
|
// The VPS "computer" is the desktop inside this bot's managed
|
|
9435
9775
|
// container, and only screen work needs that desktop to itself.
|
|
9436
9776
|
// So the lease is claimed on the first computer call, through the
|
|
@@ -9447,12 +9787,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9447
9787
|
// Local VM's lazy claim does.
|
|
9448
9788
|
onClaimed: (vpsCapture) => {
|
|
9449
9789
|
pinAutoSurface("cloud");
|
|
9450
|
-
|
|
9451
|
-
if (threadBusy(bot.id, threadId)) {
|
|
9452
|
-
const touched = screenPollers.get(threadId)?.touched ?? false;
|
|
9453
|
-
stopScreenPoller(bot.id, threadId);
|
|
9454
|
-
startScreenPoller(bot.id, threadId, { computer: vpsCapture, ...(browserCapture ? { browser: browserCapture } : {}) }, { screenIsTheWork: touched });
|
|
9455
|
-
}
|
|
9790
|
+
adoptClaimedCapture(vpsCapture);
|
|
9456
9791
|
},
|
|
9457
9792
|
onRejected: surfaceLazyClaimRejection("the VPS computer"),
|
|
9458
9793
|
});
|
|
@@ -9469,30 +9804,31 @@ async function startTurn(botId, text, opts) {
|
|
|
9469
9804
|
}
|
|
9470
9805
|
}
|
|
9471
9806
|
if (teamComputer) {
|
|
9472
|
-
const attached = await attachTeamBoat(teamComputer, bot.id, resourceOwner, canWorkOnCloud(cloudEngine(instance)
|
|
9807
|
+
const attached = await attachTeamBoat(teamComputer, bot.id, resourceOwner, canWorkOnCloud(cloudEngine(instance)));
|
|
9473
9808
|
Object.assign(integrations, attached.mount);
|
|
9474
9809
|
previewCapture = attached.capture;
|
|
9475
9810
|
computerKind = "box";
|
|
9476
9811
|
}
|
|
9477
|
-
// Cloud is strict when selected.
|
|
9478
|
-
// whose whole turn runs on its Boat, attaches one, and only one that
|
|
9479
|
-
// is already running. Every other engine reaches the Boat only when
|
|
9812
|
+
// Cloud is strict when selected. A turn reaches the Boat only when
|
|
9480
9813
|
// this conversation is on it (Works on, a pin, a cloud routine, or
|
|
9481
9814
|
// select_computer): an Auto turn never reads the Boat account, so it
|
|
9482
9815
|
// adds no Boat call, cannot be stopped by a Boat being deleted, and
|
|
9483
|
-
// never records a place nobody chose.
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9816
|
+
// never records a place nobody chose. Even then the Boat is created
|
|
9817
|
+
// or woken only by the bot's first computer call (mountBotBoat): a
|
|
9818
|
+
// chat that never touches the screen uses no plan hours.
|
|
9819
|
+
if (!teamComputer && cloudBackend === "box" && boat.boatConfigured(cfg) && wants === "cloud") {
|
|
9820
|
+
integrations.localComputer = mountBotBoat(bot, resourceOwner, {
|
|
9821
|
+
// The row a failed attach always wrote: the place's one line and
|
|
9822
|
+
// its one way on, read where the place came from (an
|
|
9823
|
+
// Auto-recorded pin is cleared, as dispatch clears it).
|
|
9824
|
+
source: placeSource,
|
|
9825
|
+
onClaimed: adoptClaimedCapture,
|
|
9826
|
+
onRejected: surfaceLazyClaimRejection("the cloud computer"),
|
|
9827
|
+
});
|
|
9828
|
+
computerKind = "box";
|
|
9493
9829
|
}
|
|
9494
9830
|
if (wants === "cloud" && cloudBackend === "box" && !boat.boatConfigured(cfg)) {
|
|
9495
|
-
throw
|
|
9831
|
+
throw boatNotConfigured(bot, placeSource ?? "works-on");
|
|
9496
9832
|
}
|
|
9497
9833
|
if (wants === "cloud" && cloudBackend === "box" && computerKind !== "box") {
|
|
9498
9834
|
throw new Error("the cloud computer could not be created or reached");
|
|
@@ -9503,21 +9839,20 @@ async function startTurn(botId, text, opts) {
|
|
|
9503
9839
|
// they are.
|
|
9504
9840
|
if (!namedPlace || !placeSource)
|
|
9505
9841
|
throw error;
|
|
9506
|
-
throw asPlaceFailure(error, namedPlace, placeSource);
|
|
9842
|
+
throw await asPlaceFailure(error, namedPlace, placeSource, bot, cloudBackend);
|
|
9507
9843
|
}
|
|
9508
9844
|
// Auto-only host fallback. Electron owns cua-driver/TCC attribution;
|
|
9509
9845
|
// the harness only reads its already-running connection descriptor.
|
|
9510
9846
|
// Auto reaches a Local VM this bot already has before it ever touches the
|
|
9511
9847
|
// host's own desktop: on a headless server that VM is the only desktop
|
|
9512
9848
|
// there is, and a person who prepared one meant it to be used.
|
|
9513
|
-
if (wants === undefined && !integrations.
|
|
9849
|
+
if (wants === undefined && !integrations.localComputer && !computerPlaceRefusal("localVm") && await attachLocalVm(false))
|
|
9514
9850
|
computerKind = "vm";
|
|
9515
9851
|
// An unattended run on a bot with a VPS configured never lands on the
|
|
9516
9852
|
// host's own desktop instead: a scheduled job clicking on someone's
|
|
9517
9853
|
// laptop is worse than a scheduled job that fails and says why.
|
|
9518
9854
|
const unattendedVps = cloudBackend === "vps" && Boolean(opts?.automationSource);
|
|
9519
|
-
if (!integrations.
|
|
9520
|
-
!integrations.localComputer &&
|
|
9855
|
+
if (!integrations.localComputer &&
|
|
9521
9856
|
wants === undefined &&
|
|
9522
9857
|
!unattendedVps &&
|
|
9523
9858
|
!computerPlaceRefusal("thisComputer") &&
|
|
@@ -9552,7 +9887,6 @@ async function startTurn(botId, text, opts) {
|
|
|
9552
9887
|
}
|
|
9553
9888
|
if (wants === undefined &&
|
|
9554
9889
|
cloudBackend === "vps" &&
|
|
9555
|
-
!integrations.computer &&
|
|
9556
9890
|
!integrations.localComputer &&
|
|
9557
9891
|
autoVpsProblem &&
|
|
9558
9892
|
!computerSelectionTurns.has(threadId)) {
|
|
@@ -9560,7 +9894,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9560
9894
|
? "This scheduled run tried to start the VPS computer and could not reach it. Check the VPS connection in Settings → API keys."
|
|
9561
9895
|
: bot.autoStartVps
|
|
9562
9896
|
? "Check the VPS connection in Settings → API keys."
|
|
9563
|
-
: "Open Computer and enable Start VPS automatically, or choose Cloud to start it manually.";
|
|
9897
|
+
: "Open Computer and enable Start VPS automatically, or choose Cloud computer to start it manually.";
|
|
9564
9898
|
throw new Error(`${autoVpsProblem}. ${hint}`);
|
|
9565
9899
|
}
|
|
9566
9900
|
// Agent control tools include peer comms and the secure credential
|
|
@@ -9683,9 +10017,8 @@ async function startTurn(botId, text, opts) {
|
|
|
9683
10017
|
// Pin on use (issue #1650): every computer that registers a claim
|
|
9684
10018
|
// slot — a ready Local VM, the VPS, the host fallback, a VM created
|
|
9685
10019
|
// for this turn — records its pin inside the claim itself, so a turn
|
|
9686
|
-
// that only mounted tools records nothing. The
|
|
9687
|
-
//
|
|
9688
|
-
// claim) pin here.
|
|
10020
|
+
// that only mounted tools records nothing. The built-in browser (no
|
|
10021
|
+
// seat to claim) pins here.
|
|
9689
10022
|
if (autoPinAllowed()) {
|
|
9690
10023
|
const claimSlot = autoVmClaims.get(threadId);
|
|
9691
10024
|
const pinsAtClaim = claimSlot?.owner.generation === resourceOwner.generation;
|
|
@@ -9706,7 +10039,6 @@ async function startTurn(botId, text, opts) {
|
|
|
9706
10039
|
}
|
|
9707
10040
|
const computerPromptKind = resolveComputerPromptKind({
|
|
9708
10041
|
kind: computerKind,
|
|
9709
|
-
driverKind: instance.driverKind,
|
|
9710
10042
|
vmPrivate: localVmMode(cfg) === "per-bot",
|
|
9711
10043
|
});
|
|
9712
10044
|
const coordinationNode = opts?.coordination ? roomHandoffs.nodes.get(opts.coordination.id) : undefined;
|
|
@@ -9723,7 +10055,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9723
10055
|
{ id: "computer", label: "Computer", text: computerPrompt(computerPromptKind) },
|
|
9724
10056
|
{ id: "team-computer", label: "Team computer", text: teamComputerPrompt(teamComputer) },
|
|
9725
10057
|
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: mountedComputer, browser: Boolean(integrations.browser) }, { pinned: plan.pinned, note: plan.note, canSelect: computerSelectionTurns.has(threadId), cloudHome: Boolean(CLOUD_HOME) }) },
|
|
9726
|
-
{ id: "cloud-home", label: "
|
|
10058
|
+
{ id: "cloud-home", label: "My Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
|
|
9727
10059
|
// gated on the integration, not the key: the hint only goes to a
|
|
9728
10060
|
// bot whose driver actually mounted the tools
|
|
9729
10061
|
{ id: "composio", label: "Connected apps", text: integrations.composio ? composioSystemPrompt(liveBot?.connectorTools ?? bot.connectorTools) + describeConnectorScopes((liveBot ?? bot).connectorScopes) : "" },
|
|
@@ -9814,6 +10146,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9814
10146
|
mentionTurn: tagged.length > 0,
|
|
9815
10147
|
integrations,
|
|
9816
10148
|
mcpFromUserConfig: claudeUserMcpEnabled(cfg) && !managedPolicy.restrictsMcp(),
|
|
10149
|
+
mcpCallTimeoutMs: mcpCallTimeoutMinutes(cfg) * 60_000,
|
|
9817
10150
|
cwd,
|
|
9818
10151
|
}), () => !directTurnClaimExists(bot.id, dispatchClaimId, threadId), async () => {
|
|
9819
10152
|
await instance.adapter.interruptTurn(threadId).catch(() => { });
|
|
@@ -9856,7 +10189,7 @@ async function startTurn(botId, text, opts) {
|
|
|
9856
10189
|
// keep polling the boat forever, carrying dead per-turn state. busy
|
|
9857
10190
|
// is flipped false in the fold, so it is the honest "still running".
|
|
9858
10191
|
if ((previewCapture || browserCapture) && threadBusy(bot.id, threadId)) {
|
|
9859
|
-
startScreenPoller(bot.id, threadId, { ...(previewCapture ? { computer: previewCapture } : {}), ...(browserCapture ? { browser: browserCapture } : {}) }
|
|
10192
|
+
startScreenPoller(bot.id, threadId, { ...(previewCapture ? { computer: previewCapture } : {}), ...(browserCapture ? { browser: browserCapture } : {}) });
|
|
9860
10193
|
}
|
|
9861
10194
|
// An adapter may publish completion synchronously just before its
|
|
9862
10195
|
// dispatch promise resolves. The event could not use the turn-id map
|
|
@@ -9950,16 +10283,10 @@ async function startTurn(botId, text, opts) {
|
|
|
9950
10283
|
drainDelegationWakes();
|
|
9951
10284
|
return;
|
|
9952
10285
|
}
|
|
9953
|
-
// A failed place never sticks
|
|
9954
|
-
//
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
if (e instanceof PlaceUnavailableError) {
|
|
9958
|
-
const pinned = store.taskByThread(bot.id, threadId);
|
|
9959
|
-
if (pinned?.surface === e.place && pinned.surfaceSource === "auto") {
|
|
9960
|
-
store.patchTask(bot.id, threadId, { surface: undefined, surfaceSource: undefined });
|
|
9961
|
-
}
|
|
9962
|
-
}
|
|
10286
|
+
// A failed place never sticks; a person's pin stays, and the message
|
|
10287
|
+
// names the composer.
|
|
10288
|
+
if (e instanceof PlaceUnavailableError)
|
|
10289
|
+
clearFailedAutoPin(bot.id, threadId, e.place);
|
|
9963
10290
|
let message = e instanceof Error ? e.message : String(e);
|
|
9964
10291
|
const current = store.projectBotForTask(bot.id, threadId);
|
|
9965
10292
|
const backups = cfg.automaticRecovery?.enabled
|
|
@@ -10014,7 +10341,7 @@ async function startTurn(botId, text, opts) {
|
|
|
10014
10341
|
role: "bot",
|
|
10015
10342
|
kind: "activity",
|
|
10016
10343
|
turnSucceeded: false,
|
|
10017
|
-
tool: failedTurnTool(message),
|
|
10344
|
+
tool: failedTurnTool(message, e instanceof PlaceUnavailableError ? { place: e.row } : {}),
|
|
10018
10345
|
});
|
|
10019
10346
|
// Worth a buzz for the same reason a routine failure is, and the rule
|
|
10020
10347
|
// notify.ts encodes: the bot is not working, and the cause is usually
|
|
@@ -10057,6 +10384,9 @@ function routineSourceOwner(run) {
|
|
|
10057
10384
|
? { bot, group: undefined, threadId: task.threadId }
|
|
10058
10385
|
: null;
|
|
10059
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.
|
|
10060
10390
|
const threadId = run.sourceThreadId?.trim();
|
|
10061
10391
|
if (!threadId)
|
|
10062
10392
|
return null;
|
|
@@ -10077,6 +10407,21 @@ function routineSourceOwner(run) {
|
|
|
10077
10407
|
const group = store.groupByThread(threadId);
|
|
10078
10408
|
return group?.memberIds.includes(bot.id) ? { bot, group, threadId } : null;
|
|
10079
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
|
+
}
|
|
10080
10425
|
function routineSourceThread(run) {
|
|
10081
10426
|
return routineSourceOwner(run)?.threadId ?? null;
|
|
10082
10427
|
}
|
|
@@ -10275,13 +10620,18 @@ routines = new RoutineManager({
|
|
|
10275
10620
|
}
|
|
10276
10621
|
return groupIsWorking(group) || coordinator.busy ? "busy" : "ready";
|
|
10277
10622
|
},
|
|
10278
|
-
createTask: (botId, title, activate = false,
|
|
10623
|
+
createTask: (botId, title, activate = false, run) => {
|
|
10279
10624
|
const task = store.createTask(botId, title, activate);
|
|
10280
10625
|
// On a Cloud home a run's conversation is opened, like its results
|
|
10281
10626
|
// conversation, by whoever wrote the routine: a run of one that is
|
|
10282
10627
|
// nobody's is confined to a folder of its own, never the bot's project.
|
|
10283
|
-
|
|
10284
|
-
|
|
10628
|
+
// A webhook's run is the owner's: only their own devices can create,
|
|
10629
|
+
// edit or rotate a webhook, so it works at the bot's own level, in its
|
|
10630
|
+
// folder, as on the desktop. Its payload still never reaches the lent
|
|
10631
|
+
// Mac: cloud-lending.ts refuses every webhook run by its trigger.
|
|
10632
|
+
if (task && CLOUD_HOME && run) {
|
|
10633
|
+
threadStarters.set(task.threadId, run.triggerSource === "webhook" ? CLOUD_OWNER_KEY : routineOpener(run.routineId));
|
|
10634
|
+
}
|
|
10285
10635
|
// The store's frame announces it. A run's task that stays in the
|
|
10286
10636
|
// background leaves the open thread alone, so that frame carries no
|
|
10287
10637
|
// transcript: a whole thread on every scheduled run once passed the phone
|
|
@@ -10305,21 +10655,32 @@ routines = new RoutineManager({
|
|
|
10305
10655
|
return Boolean(bot && !bot.hidden && task && !task.routineRunId);
|
|
10306
10656
|
},
|
|
10307
10657
|
resolveResultsThread: (routine, forceNew) => {
|
|
10308
|
-
//
|
|
10309
|
-
//
|
|
10310
|
-
//
|
|
10311
|
-
|
|
10312
|
-
|
|
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;
|
|
10313
10675
|
const threadId = store.createTask(routine.botId, `${routine.name} · Results`, false)?.threadId;
|
|
10314
|
-
|
|
10315
|
-
|
|
10316
|
-
if (threadId && CLOUD_HOME)
|
|
10317
|
-
threadStarters.set(threadId, routineOpener(routine.id));
|
|
10676
|
+
if (threadId)
|
|
10677
|
+
threadStarters.set(threadId, opener);
|
|
10318
10678
|
return threadId;
|
|
10319
10679
|
},
|
|
10320
10680
|
discardResultsThread: (botId, threadId) => {
|
|
10321
10681
|
const task = store.taskByThread(botId, threadId);
|
|
10322
|
-
|
|
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) {
|
|
10323
10684
|
store.deleteTask(botId, threadId);
|
|
10324
10685
|
handoffs.forget(threadId);
|
|
10325
10686
|
}
|
|
@@ -10491,31 +10852,35 @@ if (recoveryOwners.length > 0) {
|
|
|
10491
10852
|
// after the user confirms a durable card. Keeping this beside the scheduler
|
|
10492
10853
|
// makes the card resolvable after an app restart without involving the model.
|
|
10493
10854
|
async function cloudRoutineReadiness(botId, threadId) {
|
|
10494
|
-
const
|
|
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;
|
|
10495
10860
|
if (!bot || bot.hidden)
|
|
10496
10861
|
return { ready: false, reason: "The routine's target bot no longer exists." };
|
|
10497
10862
|
if (!boat.boatConfigured(cfg)) {
|
|
10498
10863
|
return {
|
|
10499
10864
|
ready: false,
|
|
10500
|
-
reason:
|
|
10865
|
+
reason: `${boatNotConfigured(bot, "routine").message} For the bot's configured computer, including a self-hosted VPS, set run_on="maus" instead.`,
|
|
10501
10866
|
};
|
|
10502
10867
|
}
|
|
10503
10868
|
const instance = registry.get(bot.modelSelection.instanceId);
|
|
10504
10869
|
if (!instance)
|
|
10505
10870
|
return { ready: false, reason: "The target bot's model is unavailable. Choose another model in the bot's settings." };
|
|
10506
10871
|
// The same rule a cloud turn applies, before anything is provisioned.
|
|
10507
|
-
const unsupported =
|
|
10872
|
+
const unsupported = cloudPlaceRefusal(cloudEngine(instance, bot.modelSelection.model), "routine", bot.name);
|
|
10508
10873
|
if (unsupported)
|
|
10509
|
-
return { ready: false, reason: unsupported };
|
|
10874
|
+
return { ready: false, reason: unsupported.message };
|
|
10510
10875
|
try {
|
|
10511
10876
|
if ((await instance.snapshot()).state !== "available") {
|
|
10512
|
-
return { ready: false, reason: "The target bot's model is not ready to use the
|
|
10877
|
+
return { ready: false, reason: "The target bot's model is not ready to use the cloud computer." };
|
|
10513
10878
|
}
|
|
10514
10879
|
const teamComputer = inheritedTeamComputer(bot);
|
|
10515
10880
|
const ownerId = teamComputer ? teamComputerOwner(teamComputer.id) : bot.id;
|
|
10516
10881
|
const machine = await boat.findBoat(cfg, ownerId);
|
|
10517
10882
|
if (teamComputer && !machine) {
|
|
10518
|
-
return { ready: false, reason: "The team's
|
|
10883
|
+
return { ready: false, reason: "The team's cloud computer is missing; explicitly create or retry it from the Team map." };
|
|
10519
10884
|
}
|
|
10520
10885
|
// Explicit Cloud may provision or wake the bot's own Boat at dispatch.
|
|
10521
10886
|
// This probe only checks its identity/account, without starting billing.
|
|
@@ -11208,12 +11573,11 @@ const webhooks = new WebhookManager({
|
|
|
11208
11573
|
post: (botId, threadId, text) => {
|
|
11209
11574
|
if (!store.bot(botId))
|
|
11210
11575
|
return;
|
|
11211
|
-
store.appendMessage(threadId, { role: "bot", kind: "text", text });
|
|
11576
|
+
store.appendMessage(threadId, { role: "bot", kind: "text", text, webhookPost: true });
|
|
11212
11577
|
},
|
|
11213
|
-
//
|
|
11214
|
-
//
|
|
11215
|
-
//
|
|
11216
|
-
// 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.
|
|
11217
11581
|
resolvePostThread: (trigger, forceNew) => {
|
|
11218
11582
|
if (!forceNew && trigger.resultsThreadId)
|
|
11219
11583
|
return trigger.resultsThreadId;
|
|
@@ -11446,6 +11810,10 @@ setupRetry = 0) {
|
|
|
11446
11810
|
noteForeignTurn(bot.id, threadId, internalGeneration);
|
|
11447
11811
|
const resourceOwner = { threadId, generation: internalGeneration };
|
|
11448
11812
|
turnResourceOwners.set(threadId, resourceOwner);
|
|
11813
|
+
// Set when this member's cloud computer, claimed by its first computer
|
|
11814
|
+
// call, could not start (or parked waiting for its seat): the turn ends on
|
|
11815
|
+
// it, with this cause, instead of as merely interrupted.
|
|
11816
|
+
let roomClaimFailure;
|
|
11449
11817
|
let roomVmTarget = null;
|
|
11450
11818
|
let retainRoomVmLease = false;
|
|
11451
11819
|
let roomSpeaker;
|
|
@@ -11692,17 +12060,13 @@ setupRetry = 0) {
|
|
|
11692
12060
|
const roomPlaceRefusal = roomKind && computerPlaceRefusal(roomKind);
|
|
11693
12061
|
if (roomPlaceRefusal)
|
|
11694
12062
|
throw Object.assign(new Error(roomPlaceRefusal.message), { code: roomPlaceRefusal.code });
|
|
11695
|
-
// The Computer engine runs on the Boat; this computer has no tools it can
|
|
11696
|
-
// reach, exactly as a bot thread refuses it.
|
|
11697
|
-
if (roomPlan.computer === "local" && instance.adapter.capabilities.remoteAgent === true) {
|
|
11698
|
-
throw new Error("the Computer engine works on the cloud computer — set Works on to Cloud, or choose another engine");
|
|
11699
|
-
}
|
|
11700
12063
|
// The same one rule as a bot thread, before anything is provisioned: an
|
|
11701
12064
|
// engine that cannot use the cloud computer is refused, never swapped.
|
|
11702
12065
|
if (!roomTeamComputer && roomPlan.computer === "cloud") {
|
|
11703
|
-
const unsupported =
|
|
12066
|
+
const unsupported = cloudPlaceRefusal(cloudEngine(instance, readyBot.modelSelection.model), "room", readyBot.name)
|
|
12067
|
+
?? computerToolsRefusal(readyBot.toolScope, "room", readyBot.name);
|
|
11704
12068
|
if (unsupported)
|
|
11705
|
-
throw
|
|
12069
|
+
throw unsupported;
|
|
11706
12070
|
}
|
|
11707
12071
|
// One place per room turn as well: a team computer reached on Auto means
|
|
11708
12072
|
// no separate built-in browser.
|
|
@@ -11722,12 +12086,12 @@ setupRetry = 0) {
|
|
|
11722
12086
|
return false;
|
|
11723
12087
|
}
|
|
11724
12088
|
if (roomTeamComputer) {
|
|
11725
|
-
const attached = await attachTeamBoat(roomTeamComputer, readyBot.id, resourceOwner, canWorkOnCloud(cloudEngine(instance)
|
|
12089
|
+
const attached = await attachTeamBoat(roomTeamComputer, readyBot.id, resourceOwner, canWorkOnCloud(cloudEngine(instance)));
|
|
11726
12090
|
if (isCancelled?.() || groupSpeakers.get(threadId) !== roomSpeaker ||
|
|
11727
12091
|
activeInternalGenerationByThread.get(threadId) !== internalGeneration)
|
|
11728
12092
|
return false;
|
|
11729
12093
|
Object.assign(integrations, attached.mount);
|
|
11730
|
-
startScreenPoller(readyBot.id, threadId, { computer: attached.capture }
|
|
12094
|
+
startScreenPoller(readyBot.id, threadId, { computer: attached.capture });
|
|
11731
12095
|
}
|
|
11732
12096
|
// The speaker's own explicit place, mounted exactly as its bot thread
|
|
11733
12097
|
// mounts it. A channel has no conversation pin and no Auto fallback here:
|
|
@@ -11735,6 +12099,51 @@ setupRetry = 0) {
|
|
|
11735
12099
|
const roomSetupIsCurrent = () => !isCancelled?.() &&
|
|
11736
12100
|
groupSpeakers.get(threadId) === roomSpeaker &&
|
|
11737
12101
|
activeInternalGenerationByThread.get(threadId) === internalGeneration;
|
|
12102
|
+
// A lazily claimed desktop (the VPS, the member's Boat) shows in the room
|
|
12103
|
+
// from the moment its first computer call claims it.
|
|
12104
|
+
const showClaimedScreen = (capture) => {
|
|
12105
|
+
if (roomSetupIsCurrent() && store.group(readyGroup.id)?.busyBotId === readyBot.id) {
|
|
12106
|
+
roomScreenBotId = readyBot.id;
|
|
12107
|
+
startScreenPoller(readyBot.id, threadId, { computer: capture });
|
|
12108
|
+
}
|
|
12109
|
+
};
|
|
12110
|
+
// The member's cloud computer could not start on its first computer call:
|
|
12111
|
+
// the same row, report and parking a failure at setup gets (the catch at
|
|
12112
|
+
// the end of this function), then the member's turn ends. Fenced to this
|
|
12113
|
+
// member's still-running turn.
|
|
12114
|
+
const failRoomClaim = (failure, error) => {
|
|
12115
|
+
if (activeInternalGenerationByThread.get(threadId) !== internalGeneration || groupSpeakers.get(threadId) !== roomSpeaker)
|
|
12116
|
+
return;
|
|
12117
|
+
if (resourceOwner.computerParkedOn) {
|
|
12118
|
+
registerComputerResume({
|
|
12119
|
+
botId: bot.id,
|
|
12120
|
+
threadId,
|
|
12121
|
+
resource: resourceOwner.computerParkedOn,
|
|
12122
|
+
generation: internalGeneration,
|
|
12123
|
+
afterMessageId: store.activePath(threadId).findLast((entry) => entry.role === "user")?.id,
|
|
12124
|
+
});
|
|
12125
|
+
roomClaimFailure = { parked: true };
|
|
12126
|
+
if (orchestration)
|
|
12127
|
+
orchestration.result.stopReason = `${bot.name} parked waiting for the computer`;
|
|
12128
|
+
}
|
|
12129
|
+
else {
|
|
12130
|
+
// Read as a room member's place (mountBotBoat): its one line, with no
|
|
12131
|
+
// retry of its own.
|
|
12132
|
+
const placeFailure = error instanceof PlaceUnavailableError ? error : undefined;
|
|
12133
|
+
const message = placeFailure?.message ?? failure;
|
|
12134
|
+
roomClaimFailure = { parked: false, message };
|
|
12135
|
+
store.appendMessage(threadId, {
|
|
12136
|
+
role: "bot", kind: "activity",
|
|
12137
|
+
from: { botId: bot.id, name: bot.name, color: bot.color },
|
|
12138
|
+
tool: failedTurnTool(message, placeFailure ? { place: placeFailure.row } : {}),
|
|
12139
|
+
});
|
|
12140
|
+
onDispatchError?.(message);
|
|
12141
|
+
// Claude settles the interrupt as exit_before_result: not a second
|
|
12142
|
+
// incident for the same failure.
|
|
12143
|
+
resourceOwner.lazyClaimFailureReported = true;
|
|
12144
|
+
}
|
|
12145
|
+
void instance.adapter.interruptTurn(threadId).catch(() => { });
|
|
12146
|
+
};
|
|
11738
12147
|
// An explicit place that cannot be attached fails the member's turn with
|
|
11739
12148
|
// its cause and the one setting that changes it.
|
|
11740
12149
|
try {
|
|
@@ -11748,15 +12157,7 @@ setupRetry = 0) {
|
|
|
11748
12157
|
if (turnProvider(readyBot) === "vps") {
|
|
11749
12158
|
vpsThreadStarted(readyBot.id, threadId);
|
|
11750
12159
|
roomVpsBotId = readyBot.id;
|
|
11751
|
-
const mounted = await mountBotVps(readyBot, resourceOwner, {
|
|
11752
|
-
start: true,
|
|
11753
|
-
onClaimed: (capture) => {
|
|
11754
|
-
if (roomSetupIsCurrent() && store.group(readyGroup.id)?.busyBotId === readyBot.id) {
|
|
11755
|
-
roomScreenBotId = readyBot.id;
|
|
11756
|
-
startScreenPoller(readyBot.id, threadId, { computer: capture });
|
|
11757
|
-
}
|
|
11758
|
-
},
|
|
11759
|
-
});
|
|
12160
|
+
const mounted = await mountBotVps(readyBot, resourceOwner, { start: true, onClaimed: showClaimedScreen });
|
|
11760
12161
|
if (!roomSetupIsCurrent())
|
|
11761
12162
|
return false;
|
|
11762
12163
|
if (!("integration" in mounted))
|
|
@@ -11766,24 +12167,18 @@ setupRetry = 0) {
|
|
|
11766
12167
|
}
|
|
11767
12168
|
else {
|
|
11768
12169
|
if (!boat.boatConfigured(cfg))
|
|
11769
|
-
throw
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
return false;
|
|
11774
|
-
if (!attached)
|
|
11775
|
-
throw new Error("the cloud computer could not be created or reached");
|
|
11776
|
-
Object.assign(integrations, attached.mount);
|
|
11777
|
-
roomScreenBotId = readyBot.id;
|
|
11778
|
-
startScreenPoller(readyBot.id, threadId, { computer: attached.capture }, { screenIsTheWork: remoteAgent });
|
|
12170
|
+
throw boatNotConfigured(readyBot, "room");
|
|
12171
|
+
// As in a bot thread: created or woken by the member's first
|
|
12172
|
+
// computer call, never by the room message itself.
|
|
12173
|
+
integrations.localComputer = mountBotBoat(readyBot, resourceOwner, { source: "room", onClaimed: showClaimedScreen, onRejected: failRoomClaim });
|
|
11779
12174
|
roomComputerKind = "box";
|
|
11780
12175
|
}
|
|
11781
12176
|
}
|
|
11782
12177
|
// Room and Goal turns use the speaker's desktop, never the coordinator's.
|
|
11783
12178
|
// Claim the same lease as direct turns before asynchronous VM setup.
|
|
11784
12179
|
if (readyBot.computer === "vm") {
|
|
11785
|
-
if (instance.adapter.capabilities.computerMcp !== true
|
|
11786
|
-
throw
|
|
12180
|
+
if (instance.adapter.capabilities.computerMcp !== true) {
|
|
12181
|
+
throw placeUnavailable("vm", { state: "vm-cannot", params: { bot: readyBot.name, model: cloudEngine(instance, readyBot.modelSelection.model).name }, source: "room" }, Boolean(CLOUD_HOME));
|
|
11787
12182
|
}
|
|
11788
12183
|
// A distinct identity fences cleanup even in shared mode on the same room thread.
|
|
11789
12184
|
const target = { ...localVmTargetForThread(readyBot.id, threadId) };
|
|
@@ -11823,8 +12218,7 @@ setupRetry = 0) {
|
|
|
11823
12218
|
const place = roomPlan.computer;
|
|
11824
12219
|
if (place !== "cloud" && place !== "vm" && place !== "local")
|
|
11825
12220
|
throw error;
|
|
11826
|
-
|
|
11827
|
-
throw asPlaceFailure(error, place, "works-on", Infinity);
|
|
12221
|
+
throw await asPlaceFailure(error, place, "room", readyBot, turnProvider(readyBot) === "vps" ? "vps" : "box");
|
|
11828
12222
|
}
|
|
11829
12223
|
const roster = readyGroup.memberIds
|
|
11830
12224
|
.map((id) => store.bot(id))
|
|
@@ -11911,7 +12305,6 @@ setupRetry = 0) {
|
|
|
11911
12305
|
const recentLines = recentWork(recentWorkSources(bot), bot, { userName, currentThreadId: threadId, ...recentWorkFilter() });
|
|
11912
12306
|
const roomComputerPromptKind = resolveComputerPromptKind({
|
|
11913
12307
|
kind: roomTeamComputer || roomComputerKind === "box" ? "box" : roomVmTarget ? "vm" : roomComputerKind,
|
|
11914
|
-
driverKind: instance.driverKind,
|
|
11915
12308
|
vmPrivate: localVmMode(cfg) === "per-bot",
|
|
11916
12309
|
});
|
|
11917
12310
|
{
|
|
@@ -11932,7 +12325,7 @@ setupRetry = 0) {
|
|
|
11932
12325
|
{ id: "computer", label: "Computer", text: computerPrompt(roomComputerPromptKind) },
|
|
11933
12326
|
{ id: "team-computer", label: "Team computer", text: teamComputerPrompt(roomTeamComputer) },
|
|
11934
12327
|
{ id: "plan", label: "Surface", text: surfacePrompt({ computer: roomTeamComputer ? "cloud" : roomVmTarget ? "vm" : surfaceOfComputerKind(roomComputerKind), browser: Boolean(integrations.browser) }, { note: roomPlan.note, cloudHome: Boolean(CLOUD_HOME) }) },
|
|
11935
|
-
{ id: "cloud-home", label: "
|
|
12328
|
+
{ id: "cloud-home", label: "My Cloud", text: CLOUD_HOME ? cloudHomePrompt(Boolean(integrations.agents) && lendingEnabled()) : "" },
|
|
11936
12329
|
{ id: "browser", label: "Browser", text: integrations.browser ? BUILT_IN_BROWSER_SYSTEM_PROMPT : "" },
|
|
11937
12330
|
{ id: "recall", label: "Recall", text: integrations.agents && bot.memoryEnabled !== false ? SESSION_SEARCH_SYSTEM_PROMPT : "" },
|
|
11938
12331
|
teamAvailabilityPart(outsideRoom),
|
|
@@ -11951,6 +12344,10 @@ setupRetry = 0) {
|
|
|
11951
12344
|
{ id: "skill-instructions", label: "Skill instructions", text: renderSkillInstructions(selectedSkills, { includeRoot: Boolean(workspace) }) },
|
|
11952
12345
|
{ id: "playbooks", label: "Playbooks", text: installedPlaybookInstructions(text, bot.playbooks) },
|
|
11953
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;
|
|
11954
12351
|
// run the turn and wait for it to settle, folding the reply text so a
|
|
11955
12352
|
// chained @mention can be routed afterwards
|
|
11956
12353
|
// Claim only after setup succeeded. An unavailable, busy, unsupported, or
|
|
@@ -11990,7 +12387,10 @@ setupRetry = 0) {
|
|
|
11990
12387
|
else
|
|
11991
12388
|
markCancelledProviderHandshake(threadId, retirementOwner);
|
|
11992
12389
|
};
|
|
11993
|
-
const
|
|
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);
|
|
11994
12394
|
const outcome = await new Promise((resolve) => {
|
|
11995
12395
|
let done = false;
|
|
11996
12396
|
let unsub = () => { };
|
|
@@ -12025,13 +12425,10 @@ setupRetry = 0) {
|
|
|
12025
12425
|
if (e.type === "item.completed" && e.itemType === "assistant_text")
|
|
12026
12426
|
replyText += `\n${e.text}`;
|
|
12027
12427
|
else if (e.type === "turn.completed") {
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
else {
|
|
12033
|
-
finish("settled");
|
|
12034
|
-
}
|
|
12428
|
+
const end = roomTurnEnd(e, Boolean(orchestration), roomClaimFailure);
|
|
12429
|
+
if (orchestration && end.stopReason !== undefined)
|
|
12430
|
+
orchestration.result.stopReason = end.stopReason;
|
|
12431
|
+
finish(end.outcome);
|
|
12035
12432
|
}
|
|
12036
12433
|
// Waiting on a person is not turn work: hold the ceiling while an
|
|
12037
12434
|
// approval or question card is open, so deciding slowly does not
|
|
@@ -12081,6 +12478,7 @@ setupRetry = 0) {
|
|
|
12081
12478
|
cwd,
|
|
12082
12479
|
integrations,
|
|
12083
12480
|
mcpFromUserConfig: claudeUserMcpEnabled(cfg) && !managedPolicy.restrictsMcp(),
|
|
12481
|
+
mcpCallTimeoutMs: mcpCallTimeoutMinutes(cfg) * 60_000,
|
|
12084
12482
|
...(instance.instanceId === readyBot.modelSelection.instanceId
|
|
12085
12483
|
? memberTurnSelection(readyBot.modelSelection)
|
|
12086
12484
|
: { model: instance.models.default }),
|
|
@@ -12220,7 +12618,7 @@ setupRetry = 0) {
|
|
|
12220
12618
|
drainSecretResumes();
|
|
12221
12619
|
drainTeamSetupResumes();
|
|
12222
12620
|
}
|
|
12223
|
-
if (outcome === "provider_failed") {
|
|
12621
|
+
if (outcome === "provider_failed" || outcome === "parked") {
|
|
12224
12622
|
if (skillAuthoring)
|
|
12225
12623
|
skillAuthoringClaim.claimed = false;
|
|
12226
12624
|
return false;
|
|
@@ -12309,7 +12707,7 @@ setupRetry = 0) {
|
|
|
12309
12707
|
store.appendMessage(threadId, {
|
|
12310
12708
|
role: "bot", kind: "activity",
|
|
12311
12709
|
from: { botId: bot.id, name: bot.name, color: bot.color },
|
|
12312
|
-
tool: failedTurnTool(message),
|
|
12710
|
+
tool: failedTurnTool(message, error instanceof PlaceUnavailableError ? { place: error.row } : {}),
|
|
12313
12711
|
});
|
|
12314
12712
|
onDispatchError?.(message);
|
|
12315
12713
|
if (orchestration) {
|
|
@@ -14009,6 +14407,7 @@ function configStatus() {
|
|
|
14009
14407
|
// not a secret — the settings picker shows it; "" = follow the system
|
|
14010
14408
|
language: cfg.language ?? "",
|
|
14011
14409
|
rooms: { turnTimeoutMinutes: roomTurnTimeoutMinutes(cfg) },
|
|
14410
|
+
mcp: { callTimeoutMinutes: mcpCallTimeoutMinutes(cfg) },
|
|
14012
14411
|
automaticRecovery: cfg.automaticRecovery ?? { enabled: false },
|
|
14013
14412
|
// absent effort = no level is sent, so clients can tell it from any level
|
|
14014
14413
|
newBots: cfg.newBots?.effort ? { effort: cfg.newBots.effort } : {},
|
|
@@ -14124,6 +14523,10 @@ function persistMcpServers(next) {
|
|
|
14124
14523
|
}
|
|
14125
14524
|
async function describeInstances() {
|
|
14126
14525
|
const configs = providerConfigs();
|
|
14526
|
+
// Reading the engines is where this server learns one became available
|
|
14527
|
+
// (an install, a sign-in, a key or a Company engine), so a bot still on the
|
|
14528
|
+
// removed Computer engine moves now rather than at the next start.
|
|
14529
|
+
void retryComputerEngineMove();
|
|
14127
14530
|
return (await registry.describe()).map((instance) => {
|
|
14128
14531
|
const entry = configs[instance.instanceId];
|
|
14129
14532
|
const described = {
|
|
@@ -14360,15 +14763,22 @@ function serveStatic(res, path) {
|
|
|
14360
14763
|
try {
|
|
14361
14764
|
const data = readFileSync(file);
|
|
14362
14765
|
const type = MIME[extname(file)] ?? "application/octet-stream";
|
|
14363
|
-
|
|
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 } : {}) });
|
|
14364
14773
|
res.end(data);
|
|
14365
14774
|
return true;
|
|
14366
14775
|
}
|
|
14367
14776
|
catch {
|
|
14368
|
-
// SPA fallback
|
|
14777
|
+
// SPA fallback. Never immutable: a missing chunk answered with the page
|
|
14778
|
+
// must not stay cached under the chunk's URL.
|
|
14369
14779
|
try {
|
|
14370
14780
|
const data = readFileSync(join(STATIC_DIR, "index.html"));
|
|
14371
|
-
res.writeHead(200, { "content-type": "text/html", ...PAGE_HEADERS });
|
|
14781
|
+
res.writeHead(200, { "content-type": "text/html", "cache-control": "no-cache", ...PAGE_HEADERS });
|
|
14372
14782
|
res.end(data);
|
|
14373
14783
|
return true;
|
|
14374
14784
|
}
|
|
@@ -14483,6 +14893,15 @@ ROUTES.push(createBotMemoryRoutes({
|
|
|
14483
14893
|
},
|
|
14484
14894
|
}));
|
|
14485
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
|
+
}));
|
|
14486
14905
|
ROUTES.push(createAntigravityLeftoverRoutes({
|
|
14487
14906
|
hosted: Boolean(hostedModels),
|
|
14488
14907
|
isAntigravity: (instanceId) => registry.get(instanceId)?.driverKind === "antigravityAgent",
|
|
@@ -14552,7 +14971,7 @@ const liveCalls = new LiveCallController({
|
|
|
14552
14971
|
},
|
|
14553
14972
|
broadcast: (frame) => broadcast(frame, { adminOnly: true }),
|
|
14554
14973
|
settings: () => ({ key: cfg.live?.key ?? "", ...liveSettingsFor(cfg) }),
|
|
14555
|
-
createSession: ({ key, sdp, botId, threadId, voice }) => createLiveSession({ key, sdp, voice, bot: liveBotFor(botId), history: liveHistoryFor(threadId) }),
|
|
14974
|
+
createSession: ({ key, sdp, botId, threadId, voice }) => createLiveSession({ key, sdp, voice, bot: liveBotFor(botId), history: liveHistoryFor(threadId), cloudHome: Boolean(CLOUD_HOME) }),
|
|
14556
14975
|
// Node's WebSocket (undici) accepts headers in its second argument.
|
|
14557
14976
|
openSocket: (url, key) => new WebSocket(url, { headers: { authorization: `Bearer ${key}` } }),
|
|
14558
14977
|
attachUrl: (sessionId) => liveAttachUrl(sessionId),
|
|
@@ -15047,7 +15466,7 @@ const handleRequest = async (req, res) => {
|
|
|
15047
15466
|
// A Cloud home is one person's: only their own devices, which the
|
|
15048
15467
|
// Admin's pairing signs in with admin scope, may lend to it.
|
|
15049
15468
|
if (CLOUD_HOME && !auth.scopes.includes("admin"))
|
|
15050
|
-
return json(res, 403, { error: "Only your own computers can lend to
|
|
15469
|
+
return json(res, 403, { error: "Only your own computers can lend to My Cloud. On this computer, open My Cloud from Settings → OpenMausBot Cloud first." });
|
|
15051
15470
|
sharedComputers.register(registration, { session: auth.session.id, person: CLOUD_HOME ? CLOUD_HOME_LENDER : personKey(auth.session) }, secret);
|
|
15052
15471
|
return json(res, 200, { ok: true });
|
|
15053
15472
|
}
|
|
@@ -15230,9 +15649,13 @@ const handleRequest = async (req, res) => {
|
|
|
15230
15649
|
return json(res, 409, { error: "The user request ended before its computer was selected." });
|
|
15231
15650
|
if (unoffered)
|
|
15232
15651
|
return json(res, 409, { error: unoffered, options });
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
15652
|
+
// The computer this turn mounted is already selected, even before it
|
|
15653
|
+
// is up when its first call starts it (the bot's cloud computer):
|
|
15654
|
+
// switching to it would only restart the request.
|
|
15655
|
+
const claim = autoVmClaims.get(internalCapability.threadId);
|
|
15656
|
+
const startsOnFirstCall = claim?.owner.generation === internalCapability.generation && claim.startsComputer === true && !claim.failed;
|
|
15657
|
+
const picked = pickComputer(options, requested, current, startsOnFirstCall);
|
|
15658
|
+
const option = picked.option;
|
|
15236
15659
|
if (!option?.available)
|
|
15237
15660
|
return json(res, 409, { error: option?.reason ?? "No configured computer or browser is available. Open the Computer panel to set one up.", options });
|
|
15238
15661
|
if (source.selected) {
|
|
@@ -15240,8 +15663,11 @@ const handleRequest = async (req, res) => {
|
|
|
15240
15663
|
return json(res, 409, { error: "A computer switch is already pending. End this turn to continue there." });
|
|
15241
15664
|
}
|
|
15242
15665
|
else {
|
|
15243
|
-
if (
|
|
15244
|
-
return json(res, 200, { status: "ready", surface: current, message:
|
|
15666
|
+
if (picked.current) {
|
|
15667
|
+
return json(res, 200, { status: "ready", surface: current, message: option.ready
|
|
15668
|
+
? "This computer is already selected. Use its mounted tools."
|
|
15669
|
+
: "This computer is already selected. It starts on its first call; use its mounted tools." });
|
|
15670
|
+
}
|
|
15245
15671
|
source.selected = option.surface;
|
|
15246
15672
|
source.previousSurface = store.taskByThread(bot.id, bot.threadId)?.surface;
|
|
15247
15673
|
}
|
|
@@ -15343,17 +15769,19 @@ const handleRequest = async (req, res) => {
|
|
|
15343
15769
|
return json(res, 200, { result });
|
|
15344
15770
|
}
|
|
15345
15771
|
// The cloud computer's tools (harness-mcp-proxy computer): the turn's
|
|
15346
|
-
// capability names its Boat, and the Boat credential stays here.
|
|
15772
|
+
// capability names its Boat, and the Boat credential stays here. A
|
|
15773
|
+
// capability minted before the Boat existed (boxId null) acts on the
|
|
15774
|
+
// one its first call's claim created or woke (mountBotBoat).
|
|
15347
15775
|
if (method === "POST" && path === "/api/internal/computer/mcp") {
|
|
15348
15776
|
const body = await readInternalBody();
|
|
15349
|
-
|
|
15350
|
-
if (!boxId)
|
|
15777
|
+
if (internalCapability.boxId === undefined)
|
|
15351
15778
|
return json(res, 403, { error: "this turn has no cloud computer" });
|
|
15352
15779
|
const abort = new AbortController();
|
|
15353
15780
|
res.once("close", () => { if (!res.writableEnded)
|
|
15354
15781
|
abort.abort(); });
|
|
15355
15782
|
return json(res, 200, { result: await cloudComputerRpc(body, {
|
|
15356
|
-
cfg,
|
|
15783
|
+
cfg, signal: abort.signal,
|
|
15784
|
+
boxId: () => internalCapability.boxId ?? claimedBoatId(internalCapability),
|
|
15357
15785
|
gate: () => computerCallGate(internalCapability),
|
|
15358
15786
|
assertActive: requireActiveInternalCapability,
|
|
15359
15787
|
}) });
|
|
@@ -16194,7 +16622,8 @@ const handleRequest = async (req, res) => {
|
|
|
16194
16622
|
const guestThread = guestWorkThread(from, target, fromThreadId, message);
|
|
16195
16623
|
if (guestThread === null)
|
|
16196
16624
|
return json(res, 500, { error: "couldn't open a conversation for that work" });
|
|
16197
|
-
const queued = queueDelegation(commsBus, from, { toBotId, message, reason: "asked while busy", depth, approvalAlreadyGranted, ...(guestThread ? { targetThreadId: guestThread } : {})
|
|
16625
|
+
const queued = queueDelegation(commsBus, from, { toBotId, message, reason: "asked while busy", depth, approvalAlreadyGranted, ...(guestThread ? { targetThreadId: guestThread } : {}),
|
|
16626
|
+
...externalCompletion(internalCapability) }, MAX_COMMS_DEPTH, fromThreadId);
|
|
16198
16627
|
if ((queued.result !== "ok" || !queued.id) && guestThread)
|
|
16199
16628
|
store.deleteTask(toBotId, guestThread);
|
|
16200
16629
|
if (queued.result !== "ok" || !queued.id)
|
|
@@ -16362,6 +16791,7 @@ const handleRequest = async (req, res) => {
|
|
|
16362
16791
|
sourceThreadId: fromThreadId,
|
|
16363
16792
|
sourceBotId: currentFrom.id,
|
|
16364
16793
|
routineRunId: activeRoutineRunForThread(fromThreadId)?.id,
|
|
16794
|
+
...externalCompletion(internalCapability),
|
|
16365
16795
|
});
|
|
16366
16796
|
store.appendMessage(fromThreadId, {
|
|
16367
16797
|
role: "bot",
|
|
@@ -16566,7 +16996,7 @@ const handleRequest = async (req, res) => {
|
|
|
16566
16996
|
const guestThread = guestWorkThread(from, target, fromThreadId, reason ?? message);
|
|
16567
16997
|
if (guestThread === null)
|
|
16568
16998
|
return json(res, 500, { error: "couldn't open a conversation for that work" });
|
|
16569
|
-
const queued = queueDelegation(commsBus, from, { toBotId, message, reason, depth, ...(guestThread ? { targetThreadId: guestThread } : {}) }, MAX_COMMS_DEPTH, fromThreadId);
|
|
16999
|
+
const queued = queueDelegation(commsBus, from, { toBotId, message, reason, depth, ...(guestThread ? { targetThreadId: guestThread } : {}), ...externalCompletion(internalCapability) }, MAX_COMMS_DEPTH, fromThreadId);
|
|
16570
17000
|
if ((queued.result !== "ok" || !queued.id) && guestThread)
|
|
16571
17001
|
store.deleteTask(toBotId, guestThread);
|
|
16572
17002
|
if (queued.result !== "ok" || !queued.id) {
|
|
@@ -17546,8 +17976,15 @@ const handleRequest = async (req, res) => {
|
|
|
17546
17976
|
const policy = currentSender.outbound ?? DEFAULT_OUTBOUND_POLICY;
|
|
17547
17977
|
const threadId = internalCapability.threadId;
|
|
17548
17978
|
const { app } = describeTool(outboundTool);
|
|
17549
|
-
|
|
17979
|
+
// One entry per call, in the summary's order, so a phone can say
|
|
17980
|
+
// "Send to Linear? Create linear comment ×2" without parsing the
|
|
17981
|
+
// raw arguments out of the subtitle.
|
|
17982
|
+
const calls = outboundCalls.map((call) => {
|
|
17550
17983
|
const described = describeTool(call.slug);
|
|
17984
|
+
return { app: described.app, label: described.label };
|
|
17985
|
+
});
|
|
17986
|
+
const summary = outboundCalls.map((call, index) => {
|
|
17987
|
+
const described = calls[index];
|
|
17551
17988
|
const argsText = call.arguments === undefined ? "" : JSON.stringify(call.arguments);
|
|
17552
17989
|
return `${described.app ? `${described.app} · ` : ""}${described.label}${argsText ? `\n${argsText.slice(0, 400)}${argsText.length > 400 ? "… [arguments truncated]" : ""}` : ""}`;
|
|
17553
17990
|
}).join("\n\n");
|
|
@@ -17577,7 +18014,7 @@ const handleRequest = async (req, res) => {
|
|
|
17577
18014
|
tool: outboundTool,
|
|
17578
18015
|
held: HELD_NOTE["approval.held.outbound"],
|
|
17579
18016
|
heldCode: "approval.held.outbound",
|
|
17580
|
-
outboundRequest: { tool: outboundTool, app },
|
|
18017
|
+
outboundRequest: { tool: outboundTool, app, calls },
|
|
17581
18018
|
},
|
|
17582
18019
|
});
|
|
17583
18020
|
appendDecision(DATA_DIR, {
|
|
@@ -17672,7 +18109,8 @@ const handleRequest = async (req, res) => {
|
|
|
17672
18109
|
if (upstream.transportSessionId)
|
|
17673
18110
|
headers["mcp-session-id"] = upstream.transportSessionId;
|
|
17674
18111
|
res.writeHead(upstream.status, headers);
|
|
17675
|
-
|
|
18112
|
+
// A missing OAuth permission gets a note on what fixes it (MOCA-273).
|
|
18113
|
+
return res.end(Buffer.from(withScopeHint(upstream.bytes, upstream.contentType, connectorCalls.map((call) => call.slug))));
|
|
17676
18114
|
}
|
|
17677
18115
|
// ── computer control: proxies read the hold, bots plead for help ──
|
|
17678
18116
|
if (path === "/api/internal/computer-control") {
|
|
@@ -17772,6 +18210,9 @@ const handleRequest = async (req, res) => {
|
|
|
17772
18210
|
const message = store.appendMessage(threadId, {
|
|
17773
18211
|
role: "bot",
|
|
17774
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),
|
|
17775
18216
|
...(owner.group ? { from: { botId: owner.bot.id, name: owner.bot.name, color: owner.bot.color } } : {}),
|
|
17776
18217
|
connector: {
|
|
17777
18218
|
slug: item.slug,
|
|
@@ -18184,7 +18625,7 @@ const handleRequest = async (req, res) => {
|
|
|
18184
18625
|
return json(res, 200, {
|
|
18185
18626
|
bots: shownBots.map((bot) => memberBot({
|
|
18186
18627
|
...wireBot(bot),
|
|
18187
|
-
tasks: store.tasks(bot.id).map(wireTask),
|
|
18628
|
+
tasks: store.tasks(bot.id).map((task) => wireTask(task, bot.modelSelection)),
|
|
18188
18629
|
...messagePage(bot.threadId, limit),
|
|
18189
18630
|
}, visible)),
|
|
18190
18631
|
botQueuedMessages: visible.everything ? queued : Object.fromEntries(Object.entries(queued).filter(([threadId]) => visible.thread(threadId))),
|
|
@@ -19148,7 +19589,7 @@ const handleRequest = async (req, res) => {
|
|
|
19148
19589
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
19149
19590
|
return json(res, 400, { error: "body must be a JSON object" });
|
|
19150
19591
|
}
|
|
19151
|
-
const allowed = new Set(["title", "pinned"]);
|
|
19592
|
+
const allowed = new Set(["title", "pinned", "turnTimeoutMinutes"]);
|
|
19152
19593
|
if (Object.keys(body).some((key) => !allowed.has(key)))
|
|
19153
19594
|
return json(res, 400, { error: "unsupported channel thread setting" });
|
|
19154
19595
|
const existing = store.groupTaskByThread(group.id, m[2]);
|
|
@@ -19156,12 +19597,16 @@ const handleRequest = async (req, res) => {
|
|
|
19156
19597
|
return json(res, 404, { error: "no such channel task" });
|
|
19157
19598
|
const pinning = body.pinned !== undefined;
|
|
19158
19599
|
const renaming = body.title !== undefined;
|
|
19159
|
-
|
|
19600
|
+
const limiting = Object.prototype.hasOwnProperty.call(body, "turnTimeoutMinutes");
|
|
19601
|
+
if (!pinning && !renaming && !limiting)
|
|
19160
19602
|
return json(res, 400, { error: "unsupported channel thread setting" });
|
|
19161
19603
|
if (pinning && typeof body.pinned !== "boolean")
|
|
19162
19604
|
return json(res, 400, { error: "pinned must be a boolean" });
|
|
19163
19605
|
if (renaming && typeof body.title !== "string")
|
|
19164
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 });
|
|
19165
19610
|
// Echoing the current title lets a newer client pin on an older server
|
|
19166
19611
|
// without the old handler turning a missing title into "Untitled".
|
|
19167
19612
|
// That echo is not a rename. A real rename stays blocked while working.
|
|
@@ -19169,10 +19614,19 @@ const handleRequest = async (req, res) => {
|
|
|
19169
19614
|
const notYours = titleChange ? cloudThreadRefusal(auth, m[2]) : null;
|
|
19170
19615
|
if (notYours)
|
|
19171
19616
|
return json(res, 403, { error: notYours });
|
|
19172
|
-
|
|
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) {
|
|
19173
19621
|
return json(res, 409, { error: "this channel is working or waiting on you — finish that turn first" });
|
|
19174
19622
|
}
|
|
19175
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
|
+
}
|
|
19176
19630
|
if (pinning) {
|
|
19177
19631
|
const pinned = store.setGroupTaskPinned(group.id, m[2], body.pinned === true);
|
|
19178
19632
|
if (!pinned)
|
|
@@ -20006,10 +20460,12 @@ const handleRequest = async (req, res) => {
|
|
|
20006
20460
|
}
|
|
20007
20461
|
// patchBot persists first and emits the canonical bot change, which the
|
|
20008
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.
|
|
20009
20465
|
const bot = store.patchBot(existing.id, { modelSelection: checked.selection });
|
|
20010
20466
|
if (!bot)
|
|
20011
20467
|
return json(res, 404, { error: "no such bot" });
|
|
20012
|
-
store.patchTask(bot.id, selected.threadId, { modelSelection:
|
|
20468
|
+
store.patchTask(bot.id, selected.threadId, { modelSelection: undefined,
|
|
20013
20469
|
...hostedModels?.resetTask(selected.modelSelection, checked.selection) });
|
|
20014
20470
|
return json(res, 200, { bot: wireBot(bot) });
|
|
20015
20471
|
}
|
|
@@ -20695,8 +21151,10 @@ const handleRequest = async (req, res) => {
|
|
|
20695
21151
|
// and pins that already match stay; returning to Auto sweeps nothing.
|
|
20696
21152
|
if (computerSpecified && requestedComputer !== undefined)
|
|
20697
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.
|
|
20698
21156
|
if (normalizedSelection && selectedTask)
|
|
20699
|
-
store.patchTask(bot.id, selectedTask.threadId, { modelSelection:
|
|
21157
|
+
store.patchTask(bot.id, selectedTask.threadId, { modelSelection: undefined,
|
|
20700
21158
|
...hostedModels?.resetTask(selectedTask.modelSelection, normalizedSelection) });
|
|
20701
21159
|
if (existingBot && (bot.browserProfile !== beforeBrowserProfile || bot.browser !== beforeBrowserEnabled)) {
|
|
20702
21160
|
browserLive.closeForBot(bot.id);
|
|
@@ -21493,12 +21951,15 @@ const handleRequest = async (req, res) => {
|
|
|
21493
21951
|
if (phoneSecretSubmissions.hasThread(bot.threadId)) {
|
|
21494
21952
|
return json(res, 409, { error: "this task is securely saving a credential — try again when it finishes" });
|
|
21495
21953
|
}
|
|
21496
|
-
|
|
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)) {
|
|
21497
21958
|
return json(res, 409, {
|
|
21498
|
-
error: `provider instance "${
|
|
21959
|
+
error: `provider instance "${rerunSelection.instanceId}" is unavailable — pick another model in settings`,
|
|
21499
21960
|
});
|
|
21500
21961
|
}
|
|
21501
|
-
const rerunInstance = registry.get(
|
|
21962
|
+
const rerunInstance = registry.get(rerunSelection.instanceId);
|
|
21502
21963
|
const rerunRefusal = policyModelRefusal(rerunInstance);
|
|
21503
21964
|
if (rerunRefusal)
|
|
21504
21965
|
return json(res, 409, { error: rerunRefusal });
|
|
@@ -21929,7 +22390,7 @@ const handleRequest = async (req, res) => {
|
|
|
21929
22390
|
// Who opened it decides who may answer its cards on a shared workspace.
|
|
21930
22391
|
if (auth.kind === "session")
|
|
21931
22392
|
threadStarters.set(task.threadId, actorKey(auth));
|
|
21932
|
-
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) });
|
|
21933
22394
|
}
|
|
21934
22395
|
m = path.match(/^\/api\/bots\/([\w-]+)\/tasks\/([\w-]+)$/);
|
|
21935
22396
|
if (m && method === "POST") {
|
|
@@ -21947,7 +22408,7 @@ const handleRequest = async (req, res) => {
|
|
|
21947
22408
|
const switched = store.switchTask(bot.id, m[2]);
|
|
21948
22409
|
if (!switched)
|
|
21949
22410
|
return json(res, 404, { error: "no such task" });
|
|
21950
|
-
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)) };
|
|
21951
22412
|
// "0" is settings only, a positive page is a bounded transcript, and no
|
|
21952
22413
|
// parameter is the whole thread — the one branch that materialises it.
|
|
21953
22414
|
// Search results land through that branch (src/lib/focus-message.ts):
|
|
@@ -22029,7 +22490,7 @@ const handleRequest = async (req, res) => {
|
|
|
22029
22490
|
const task = store.refreshTaskPermissions(profile.id, m[2]);
|
|
22030
22491
|
if (!task)
|
|
22031
22492
|
return json(res, 404, { error: "no such task" });
|
|
22032
|
-
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)) });
|
|
22033
22494
|
}
|
|
22034
22495
|
const patch = {};
|
|
22035
22496
|
if (body.projectId !== undefined) {
|
|
@@ -22135,7 +22596,7 @@ const handleRequest = async (req, res) => {
|
|
|
22135
22596
|
const task = patch.modelSelection
|
|
22136
22597
|
? store.switchTaskModel(m[1], m[2], patch.modelSelection, body.updateBotDefault === true, body.resetApprovalToAsk === true, { ...patch, ...hostedModels?.resetTask(current.modelSelection, patch.modelSelection) })
|
|
22137
22598
|
: store.patchTask(m[1], m[2], patch);
|
|
22138
|
-
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])) });
|
|
22139
22600
|
}
|
|
22140
22601
|
if (m && method === "DELETE") {
|
|
22141
22602
|
const bot = store.bot(m[1]);
|
|
@@ -22207,7 +22668,7 @@ const handleRequest = async (req, res) => {
|
|
|
22207
22668
|
const task = store.retitleTask(m[1], m[2], startedFrom, title) ?? store.taskByThread(m[1], m[2]);
|
|
22208
22669
|
if (!task)
|
|
22209
22670
|
return json(res, 404, { error: "no such task" });
|
|
22210
|
-
return json(res, 200, { task: wireTask(task) });
|
|
22671
|
+
return json(res, 200, { task: wireTask(task, store.bot(m[1]).modelSelection) });
|
|
22211
22672
|
}
|
|
22212
22673
|
finally {
|
|
22213
22674
|
titleRegenerations.delete(m[2]);
|
|
@@ -22252,8 +22713,6 @@ const handleRequest = async (req, res) => {
|
|
|
22252
22713
|
const key = teamComputerOwner(found.id);
|
|
22253
22714
|
if (parsed.data.action === "take" && boatLifecycleBusyBots.has(key))
|
|
22254
22715
|
return json(res, 409, { error: "Wait for this computer's action to finish before taking control" });
|
|
22255
|
-
if (parsed.data.action === "take")
|
|
22256
|
-
assertTeamControlCanBeTaken(found.id);
|
|
22257
22716
|
if (parsed.data.controlLeaseId) {
|
|
22258
22717
|
const result = parsed.data.action === "take"
|
|
22259
22718
|
? computerControl.acquireLease(key, parsed.data.controlLeaseId)
|
|
@@ -22292,7 +22751,7 @@ const handleRequest = async (req, res) => {
|
|
|
22292
22751
|
if (method === "POST" && !computerId) {
|
|
22293
22752
|
const parsed = teamComputerCreate.safeParse(body);
|
|
22294
22753
|
if (!parsed.success)
|
|
22295
|
-
return json(res, 400, { error: "Provide requestId (UUID), a name, and acknowledgeCost: true to create a paid
|
|
22754
|
+
return json(res, 400, { error: "Provide requestId (UUID), a name, and acknowledgeCost: true to create a paid cloud computer" });
|
|
22296
22755
|
if (!boat.boatConfigured(cfg))
|
|
22297
22756
|
return json(res, 409, { error: "Configure Boat in Settings before creating a cloud computer" });
|
|
22298
22757
|
const computer = teamComputers.create(parsed.data.name, parsed.data.requestId);
|
|
@@ -22312,7 +22771,7 @@ const handleRequest = async (req, res) => {
|
|
|
22312
22771
|
}
|
|
22313
22772
|
if (method === "POST" && found && action && action !== "control") {
|
|
22314
22773
|
if (action === "provision" && body?.acknowledgeCost !== true)
|
|
22315
|
-
return json(res, 400, { error: "Confirm
|
|
22774
|
+
return json(res, 400, { error: "Confirm cloud computer creation or wake costs with acknowledgeCost: true" });
|
|
22316
22775
|
// A ready-only join never wakes, provisions or steals an agent's turn.
|
|
22317
22776
|
// The caller takes a separate explicit human-control lease first.
|
|
22318
22777
|
if (action === "join") {
|
|
@@ -22634,7 +23093,12 @@ const handleRequest = async (req, res) => {
|
|
|
22634
23093
|
// The bots' browser engine: install it on this machine (agent-browser +
|
|
22635
23094
|
// a Chrome for Testing, a one-time download), or ask how that is going.
|
|
22636
23095
|
// One install at a time; the config frame's browserEngine tells the rest.
|
|
23096
|
+
// JSON only, so a form posted from another local page cannot start it.
|
|
22637
23097
|
if (method === "POST" && path === "/api/browser-engine/install") {
|
|
23098
|
+
const mediaType = String(req.headers["content-type"] ?? "").split(";", 1)[0].trim().toLowerCase();
|
|
23099
|
+
if (mediaType !== "application/json") {
|
|
23100
|
+
return json(res, 415, { error: "content-type must be application/json" });
|
|
23101
|
+
}
|
|
22638
23102
|
if (!browserEngineInstall) {
|
|
22639
23103
|
const status = browserEngineStatus();
|
|
22640
23104
|
if (status.kind === "unavailable" && !status.installable)
|
|
@@ -22782,7 +23246,17 @@ const handleRequest = async (req, res) => {
|
|
|
22782
23246
|
return json(res, 400, { error: "That does not look like an API key." });
|
|
22783
23247
|
const url = typeof body?.url === "string" && body.url.trim() ? body.url.trim() : (saved && "url" in saved ? saved.url : undefined);
|
|
22784
23248
|
res.setHeader("cache-control", "no-store");
|
|
22785
|
-
|
|
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);
|
|
22786
23260
|
}
|
|
22787
23261
|
if (method === "GET" && path === "/api/decisions") {
|
|
22788
23262
|
const rawLimit = url.searchParams.get("limit");
|
|
@@ -23812,6 +24286,12 @@ const handleRequest = async (req, res) => {
|
|
|
23812
24286
|
browserCleanup.abort(request);
|
|
23813
24287
|
throw error;
|
|
23814
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
|
+
});
|
|
23815
24295
|
let configWriteCommitted = false;
|
|
23816
24296
|
const externalSecretStorage = url.searchParams.get("secretStorage") === "external";
|
|
23817
24297
|
try {
|
|
@@ -23877,6 +24357,9 @@ const handleRequest = async (req, res) => {
|
|
|
23877
24357
|
}
|
|
23878
24358
|
throw error;
|
|
23879
24359
|
}
|
|
24360
|
+
for (const key of resetKeys)
|
|
24361
|
+
if (key)
|
|
24362
|
+
forgetKey(key);
|
|
23880
24363
|
// Desktops already counting down move to the new window at once, in
|
|
23881
24364
|
// every isolation mode; busy work still defers its expiry.
|
|
23882
24365
|
if (patch.localVm?.idleTimeoutMinutes !== undefined) {
|
|
@@ -24285,9 +24768,6 @@ const handleRequest = async (req, res) => {
|
|
|
24285
24768
|
if (!currentBot)
|
|
24286
24769
|
return json(res, 404, { error: "no such bot" });
|
|
24287
24770
|
const controlKey = botComputerControlKey(currentBot);
|
|
24288
|
-
const teamComputer = inheritedTeamComputer(currentBot);
|
|
24289
|
-
if (action === "take" && teamComputer)
|
|
24290
|
-
assertTeamControlCanBeTaken(teamComputer.id);
|
|
24291
24771
|
const leaseResult = body.controlLeaseId === undefined
|
|
24292
24772
|
? null
|
|
24293
24773
|
: controlLeaseIdSchema.safeParse(body.controlLeaseId);
|
|
@@ -24464,12 +24944,12 @@ const handleRequest = async (req, res) => {
|
|
|
24464
24944
|
}
|
|
24465
24945
|
if (bot.computer !== "cloud" && !threadPreview) {
|
|
24466
24946
|
return json(res, 409, {
|
|
24467
|
-
error: "Choose Cloud before changing or opening this
|
|
24947
|
+
error: "Choose Cloud computer before changing or opening this cloud computer. Auto only checks existing computer state.",
|
|
24468
24948
|
});
|
|
24469
24949
|
}
|
|
24470
24950
|
if (m[2] === "remove") {
|
|
24471
24951
|
// Boats sleep and wake; only the VPS backend has a container to remove.
|
|
24472
|
-
return json(res, 409, { error: "
|
|
24952
|
+
return json(res, 409, { error: "a cloud computer has no container to remove — put it to sleep instead" });
|
|
24473
24953
|
}
|
|
24474
24954
|
const releaseComputerLifecycle = claimBotComputerLifecycle(botId);
|
|
24475
24955
|
try {
|
|
@@ -24496,7 +24976,7 @@ const handleRequest = async (req, res) => {
|
|
|
24496
24976
|
catch (e) {
|
|
24497
24977
|
const status = e?.status ?? 500;
|
|
24498
24978
|
const candidateCode = e?.code;
|
|
24499
|
-
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)
|
|
24500
24980
|
? candidateCode : undefined;
|
|
24501
24981
|
return json(res, status, { error: e instanceof Error ? e.message : String(e), ...(code ? { code } : {}) });
|
|
24502
24982
|
}
|
|
@@ -24646,12 +25126,22 @@ restoreChannelMessages();
|
|
|
24646
25126
|
if (movedAutoPins)
|
|
24647
25127
|
console.log(`Works on: moved ${movedAutoPins} auto-pinned thread(s) to their bot's current Works on`);
|
|
24648
25128
|
}
|
|
24649
|
-
server.listen(PORT, "127.0.0.1", () => {
|
|
25129
|
+
server.listen(PORT, "127.0.0.1", async () => {
|
|
24650
25130
|
companyRuntimeReady();
|
|
24651
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.
|
|
24652
25134
|
followupsReady = true;
|
|
24653
25135
|
drainQueuedSends();
|
|
24654
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
|
+
}
|
|
24655
25145
|
// Startup work uses the same turn dispatcher and local tool endpoint as
|
|
24656
25146
|
// ordinary chat. Start only once every registry is initialized and the
|
|
24657
25147
|
// endpoint is listening; earlier dispatch can hit uninitialized bindings.
|